Zappa で内部 ALB を作成するまでの道のり

記事タイトルとURLをコピーする

はじめに

こんにちは。アプリケーションサービス部の保田(ほだ)です。

たまに Python 製の軽量サーバーレスアプリケーションのデプロイツールである Zappa を使う場面があるのですが、誰も Frank Zappa の話をしないので少し寂しいです。

ちなみに Pound for a Brown という曲が好きです。

そんな訳で今回は Zappa の tips についてお話します。

要約

  • README.md には書いてないけど、zappa_settings.json に alb_vpc_config.Scheme という設定項目があり、ここを設定すれば内部 ALB も外部 ALB も作れる
"alb_vpc_config": {
    "CertificateArn": "arn:aws:acm:ap-northeast-1:012345678901:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "SubnetIds": ["subnet-xxxxx", "subnet-yyyy"],
    "SecurityGroupIds": ["sg-zzzzz"],
    "Scheme": "internal" // コレ( internet-facing | internal)
},
  • ただし、内部 ALB、外部 ALB 問わず、ここDelay の値を大きくしないと高確率でデプロイに失敗する

目次

詳細

内容としては上に書いたことがすべてですので、以降は細かいハマり所の解説を交えつつ理解を深めながら Zappa のお話をしていきます。

Zappa とは

github.com

まず、 Zappa は Python 製のサーバーレスアプリケーションのデプロイツールです。

API Gateway + Lambda が基本構成となっており、FlaskDjango と組みあわせてサーバーサイドのロジックを構築するのがよくある使い方となっています。

それ以外にも S3 へのファイルアップロードや CloudWatch Events による定期実行など、イベント駆動の場合にも対応しています。

後者のイベント駆動の場合は、 Lambda 関数のロジックとしては特に通常とコード量は変わりませんが、前者の Flask や Django と組み合わせた場合は劇的にコード量が削減できます。

削減できるというより、普通の Flask や Django の書き方さえ分かれば OK といった感じになります。

また、 CloudFormation でいうところのテンプレートにあたるものは zappa_settings.json というファイルで定義でき、これもシンプルに書けるように作られています。

詳しくは公式の README.md を見て頂ければと思います。

前提といくつかの注意事項

現在(以降も執筆時点 2021/08/27の意味で使います)で最新の Zappa を使うとします。

また、 Flask は現在 2 系まで出ていますが、 1 系じゃないと Zappa と Flask の双方が依存するライブラリ Werkzeug のバージョンが合致せず pip install 時にエラーが出るので注意してください。

github.com

そして、Zappa が依存するライブラリの一つである troposphere も最新版は 3 系なのですが、 3 系だと Zappa と合わないみたいなので注意です。

まとめると、以下のライブラリを使っていることを前提とします。

Flask==1.1.4
zappa==0.53.0
troposphere==2.7.1

概して Zappa 側が色々追い付いていないような感じがありますね。

また、サーバーサイドのロジックは以下とし、ファイル名は main.py とします。

from flask import Flask
 
 
app = Flask(__name__)
 
 
@app.route('/v1/', methods=['GET'])
def v1():
    return 'ok'
 

lambda_hanlder(event, context) の関数はどこ?と思うかもしれませんが、それは Zappa のライブラリ側にありそこから上で書いたコードがよしなに呼び出されるという感じですね。

外部 ALB + Lambda の構成を作る

さて説明の都合上、まず外部 ALB + Lambda の構成を作ります。

Advanced Settings を参考にすれば、 ALB + Lambda の構成は例えば以下のように定義できます。

{
    "dev": {
        "app_function": "main.app",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "project_name": "zappa-example",
        "runtime": "python3.8",
        "s3_bucket": "zappa-xxxxxx",
        "apigateway_enabled": false, // API Gateway を無効に
        "alb_enabled": true, // ALB を有効に
        "alb_vpc_config": { // ALB の基本設定
            "CertificateArn": "arn:aws:acm:ap-northeast-1:012345678901:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // 証明書
            "SubnetIds": ["subnet-xxxxx", "subnet-yyyy"], // 紐づけるサブネット
            "SecurityGroupIds": ["sg-zzzzz"], // セキュリティグループ
        }
    }
}

ではデプロイしてみましょう。

$ zappa deploy dev

Calling deploy for stage dev..
Downloading and installing dependencies..
 - pyyaml==5.4.1: Downloading
100%|██████████████████████████████████████| 662k/662k [00:00<00:00, 7.28MB/s]
 - markupsafe==2.0.1: Downloading
100%|████████████████████████████████████| 30.6k/30.6k [00:00<00:00, 4.46MB/s]
Packaging project as zip.
Uploading zappa-example-dev-1630036951.zip (6.0MiB)..
100%|████████████████████████████████████| 6.32M/6.32M [00:05<00:00, 1.16MB/s]
Deploying ALB infrastructure...
Waiting for load balancer [arn:aws:elasticloadbalancing:ap-northeast-1:012345678901:loadbalancer/app/zappa-example-dev/xxxxxxxxxxxxxxxx] to become active..
Oh no! An error occurred! :(

==============

Traceback (most recent call last):
  File "/home/xxxx/.local/share/virtualenvs/zappa-example-xxxx/lib/python3.8/site-packages/zappa/cli.py", line 3422, in handle
    sys.exit(cli.handle())
  File "/home/xxxx/.local/share/virtualenvs/zappa-example-xxxx/lib/python3.8/site-packages/zappa/cli.py", line 588, in handle
    self.dispatch_command(self.command, stage)
  File "/home/xxxx/.local/share/virtualenvs/zappa-example-xxxx/lib/python3.8/site-packages/zappa/cli.py", line 630, in dispatch_command
    self.deploy(self.vargs["zip"], self.vargs["docker_image_uri"])
  File "/home/xxxx/.local/share/virtualenvs/zappa-example-xxxx/lib/python3.8/site-packages/zappa/cli.py", line 947, in deploy
    self.zappa.deploy_lambda_alb(**kwargs)
  File "/home/xxxx/.local/share/virtualenvs/zappa-example-xxxx/lib/python3.8/site-packages/zappa/core.py", line 1609, in deploy_lambda_alb
    waiter.wait(LoadBalancerArns=[load_balancer_arn], WaiterConfig={"Delay": 3})
  File "/home/xxxx/.local/share/virtualenvs/zappa-example-xxxx/lib/python3.8/site-packages/botocore/waiter.py", line 53, in wait
    Waiter.wait(self, **kwargs)
  File "/home/xxxx/.local/share/virtualenvs/zappa-example-xxxx/lib/python3.8/site-packages/botocore/waiter.py", line 362, in wait
    raise WaiterError(
botocore.exceptions.WaiterError: Waiter LoadBalancerAvailable failed: Max attempts exceeded. Previously accepted state: For expression "LoadBalancers[].State.Code" we matched expected path: "provisioning" at least once

==============

Need help? Found a bug? Let us know! :D
File bug reports on GitHub here: https://github.com/Zappa/Zappa
And join our Slack channel here: https://zappateam.slack.com
Love!,
 ~ Team Zappa!

多分エラーが起きると思います。

原因は、ここ です。

waiter.wait(LoadBalancerArns=[load_balancer_arn], WaiterConfig={"Delay": 3})

引用した部分の少し上の処理を見ていただくと分かりますが、 Zappa 側で boto3 を使って ALB を作成 しています。

# Create load balancer
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elbv2.html#ElasticLoadBalancingv2.Client.create_load_balancer
kwargs = dict(
    Name=lambda_name,
    Subnets=alb_vpc_config["SubnetIds"],
    SecurityGroups=alb_vpc_config["SecurityGroupIds"],
    Scheme=alb_vpc_config["Scheme"],
    # TODO: Tags might be a useful means of stock-keeping zappa-generated assets.
    # Tags=[],
    Type="application",
    # TODO: can be ipv4 or dualstack (for ipv4 and ipv6) ipv4 is required for internal Scheme.
    IpAddressType="ipv4",
)
response = self.elbv2_client.create_load_balancer(**kwargs)

そして、ALB のリスナーに Lambda 関数を登録するために、作成した ALB が Active になるまでの待機処理が実行されます。

boto3.amazonaws.com

要するにデフォルトでは 15 秒おきに最大 40 回、 DescribeLoadBalancers API を実行して、ステータスが Active になるのを待つ処理になっています。

つまるところ、上記で引用した処理ではステータスをチェックする時間間隔 Delay が 3 秒になっており、最大の待機時間を超えてしまったみたいです。

おそらく調子(?)が良ければこれでも間に合う(だからプルリクがマージされてる)と思うのですが、手元で動かしてみる限り全然ダメなので、直接書き直してやるしかありません。

試しに 5 秒にするだけで一応成功しましたが、デフォルトが 15 秒なのでどうせなら WaiterConfig 自体消してもいいんじゃないかなと思います。

  • zappa/core.py
waiter.wait(LoadBalancerArns=[load_balancer_arn])

これで、(一度環境を作ってしまった場合は zappa undeploy dev で環境を消してから※)デプロイし直せば成功するはずです。

※ 初回デプロイ時以降は zappa update dev のように実行するコマンドが変わります。 が、上記のエラーで失敗した場合は update コマンドを実行しても ALB が Active になっていれば OK! みたいな形でデプロイが終わってしまう(本当はリスナーとして Lambda を登録しないと使えるようにならない)ようです。

だから一度環境を消して、作り直す必要があったんですね。

Internal ALB の場合もできるんです

それではようやく本題です。

先ほどの zappa_settings.json の ALB に関する設定情報を見て頂くと、問答無用で外部 ALB(internet-facing)しか使えないように見えます。

"alb_vpc_config": { // ALB の基本設定
    "CertificateArn": "arn:aws:acm:ap-northeast-1:012345678901:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // 証明書
    "SubnetIds": ["subnet-xxxxx", "subnet-yyyy"], // 紐づけるサブネット
    "SecurityGroupIds": ["sg-zzzzz"], // セキュリティグループ
}

しかし、 ALB を作成しているところのソースコード を見ると、内部 ALB(internal)も出来るように見えます。

# Create load balancer
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elbv2.html#ElasticLoadBalancingv2.Client.create_load_balancer
kwargs = dict(
    Name=lambda_name,
    Subnets=alb_vpc_config["SubnetIds"],
    SecurityGroups=alb_vpc_config["SecurityGroupIds"],
    Scheme=alb_vpc_config["Scheme"],
    # TODO: Tags might be a useful means of stock-keeping zappa-generated assets.
    # Tags=[],
    Type="application",
    # TODO: can be ipv4 or dualstack (for ipv4 and ipv6) ipv4 is required for internal Scheme.
    IpAddressType="ipv4",
)
response = self.elbv2_client.create_load_balancer(**kwargs)

本家ソースコードのコメント内でも言及されていますが、boto3 の create_load_balancer を使っているだけです。

したがって上記の Scheme'internet-facing' なら外部 ALB、 'internal' なら内部 ALB が作れる、ということになります。

実際の zappa_settings.json としては例えば次のようになります。

{
    "dev": {
        "app_function": "main.app",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "project_name": "zappa-example",
        "runtime": "python3.8",
        "s3_bucket": "zappa-xxxxxx",
        "apigateway_enabled": false,
        "alb_enabled": true,
        "alb_vpc_config": {
            "CertificateArn": "arn:aws:acm:ap-northeast-1:012345678901:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "SubnetIds": ["subnet-xxxxx", "subnet-yyyy"],
            "SecurityGroupIds": ["sg-zzzzz"],
            "Scheme": "internal" // Schema を定義( internet-facing | internal)
        }
    }
}

Lambda 自体は(実用上の要件に合うかは別として) VPC 内になくても大丈夫です。

ちなみに本家 GitHub の Issue でも「内部 ALB 作れるようになってるけどまだ README.md には書いてないね」と言われています。

github.com

デプロイ時は waiter の設定値を弄らないと高確率で失敗するのは外部 ALB の場合でも同じです。

また、一度外部 ALB で構築した場合は後からこの Scheme を変えても外部 ALB から内部 ALB に作り変えてくれません。

これまた一旦環境を削除してから再度構築する必要があります。

さいごに

Zappa の最新版のリリースが 2020 年なので、更新頑張れ~~という感じですね。