はじめに
現状のプロダクトでは Production 環境にデプロイされた後にさらに動作確認をしています。Staging 環境の QA で追加・既存機能のアプリケーションの動作を確認していますが、それは Staging 環境での担保であって、インフラリソースが異なる Production 環境では担保されていないと考えています。そのため、デプロイ後の Production でも動作を確認している格好です。
では何が問題なのか? 私は問題の一つに、Production と Staging でのインフラリソースの差異が上げられると考えています。
- Production に必要なインフラリソースがそもそも無い
- アプリケーションに必要な環境変数が足りない・間違っている
- etc...
上記の問題はインフラリソースを漏れなく管理できているかが焦点になっていますが、人間なのでどうしてもプロビジョニングをし忘れたり、設定をミスしたりします。 これを 0 にするのは、コストが高いと考えています。
ではどうするのか? Production に近いインフラで動作を確認して、ユーザーに影響が出る前にコケさせたい 💦
私的には、ブルーグリーンデプロイ戦略が良さそうだなと考えています。 実際に稼働している環境をグリーン、その複製をブルーとして、ブルー環境でテストして良好であればグリーンに切り替えるような方法です。
- Production(Green)と同じインフラ構成でテストできる
- Blue、Green をスイッチするだけで本番適用が可能でダウンタイムが少ない
- etc..
前置き長くなりましたが、 今回は仕事でもプライベートでもお世話になっている API Gateway + Lambda + GitHub Actions での私的ブルーグリーンデプロイを考えてみました。 実際に運用しているわけではなく、これならイケそうだな、の一例に過ぎません 🙏
※ 今回 DB はスコープ外にしています。DB 複製の有無に応じて、デプロイ戦略だけではなく、アプリケーション(主にマイグレーションまわり)の設計・実装にまで話が及ぶためです。
サンプルリポジトリを用意しています。 合わせて確認いただければです。
API Gateway と Lambda におけるブルーグリーンの切り替え
以下では、CLI コマンドと CDK を駆使した API Gatway と Lambda におけるブルーグリーンの設定について簡単に説明しています。 コンソールでの設定はこちらの記事が大変参考になるかと思います。
Lambda のバージョン
Lambda には、バージョンという仕組みがあります。
新しく Lambda のソースもしくは Image を更新した際に、バージョンを発行することで、その時点の Lambda をスナップすることができます。
latest
というバージョンは、その時点の Lambda の最新状態を常に指しているバージョンになります。
そして、latest
に対してバージョンを発行することで、その時点の最新状態をナンバリング(1, 2, 3...)という形でスナップできます。
Lambda のエイリアス
Lambda には、エイリアスという仕組みがあります。
Lambda のエイリアスは関数の特定のバージョンに対するポインタのようなものです。
公式が説明するように特定のバージョンに対するアクセスポイントを作成でき、ARN を介することで他サービスから Lambda にアクセスできるようになります。
バージョンとエイリアスからブルーグリーンを切り替え
バージョン発行で Lambda のデプロイスナップを取りつつ、各バージョンに対してエイリアスを設定することで、blue
のエイリアスは最新の Lambda が適用されている、green
のエイリアスは現在 production で稼働している(動作保証済み) Lambda が適用されているという構成ができます。
以下にブルーグリーンの流れを簡単に示します。
① デプロイ前
エイリアス | バージョン | アプリケーションの新旧 |
---|---|---|
blue | latest | 新 |
green | 1 | 新 |
② blue
にのみ最新アプリケーションをデプロイ
latest
バージョンをblue
にエイリアスしておけば、Lambda をデプロイすることで自動的にblue
に最新のアプリケーションがあたります。
1# Lambdaの更新コマンド
2# https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-function-code.html
3aws lambda update-function-code \
4 --function-name ${function-name} \
5 --image-uri $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
エイリアス | バージョン | アプリケーションの新旧 |
---|---|---|
blue | latest | 新 |
green | 1 | 旧(ユーザー向けに稼働中) |
③ blue
で動作保証が取れたのでバージョンを発行、更新バージョンをgreen
にエイリアス
1# AWS CLI のバージョン発行コマンド
2# https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/publish-version.html
3aws lambda publish-version --function-name ${function-name}
4# -> version 2 が発行される
5
6# AWS CLI のエイリアス更新コマンド
7# https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/update-alias.html
8aws lambda update-alias \
9 --function-name ${function-name} \
10 --function-version 2 \
11 --name green
エイリアス | バージョン | アプリケーションの新旧 |
---|---|---|
blue | latest | 新 |
green | 2 | 新 |
API Gateway のステージとステージ変数
API Gateway には、ステージという仕組みがあります。
ステージは、デプロイに対する名前付きのリファレンスで、API のスナップショットです。
blue
、green
のステージを用意して、それぞれのエンドポイントに役割を持たせます。
しかし、両ステージに Lambda を紐づけてもlatest
バージョンが呼び出されてブルーグリーンの切り替えができないため、ステージごとに呼び出す Lambda のエイリアスを変える必要があります。
blue
ステージは Lambda の blue
エイリアスを、green
ステージは green
エイリアスを呼び出すようにします。
そこで、必要なのがステージごとに変数を用意できるステージ変数という機能です。 ステージ変数でエイリアス名を指定することで、対象の Lambda のエイリアスを呼び出すことができるようになります。 詳しくは公式も参照してください。
以下は CDK でエイリアス Lambda を呼び出す API Gateway のスタック例になります。
Lambda の ARN に :${stageVariables.alias}
を追加することで、エイリアス付きで呼び出されるようになります。
apigateway.LambdaIntegration
では、ステージ変数を組み込めなかったため、こちらの Issue を参考に apigateway.AwsIntegration
で統合しています。
1const lambdaAlias = lambda.Function.fromFunctionArn(
2 this,
3 "sample-lambda-alias",
4 `${lambdaFunction.functionArn}:\${stageVariables.alias}` // ここがポイント
5);
6
7const defaultIntegration = new apigateway.AwsIntegration({
8 proxy: true,
9 service: "lambda",
10 path: `2015-03-31/functions/${lambdaAlias.functionArn}/invocations`,
11});
12
13this.restApi = new apigateway.RestApi(this, id, {
14 restApiName,
15 endpointTypes: [apigateway.EndpointType.REGIONAL],
16 defaultIntegration,
17});
API Gateway Stage のスタック例は以下になります。
stageName
をそのまま alias に指定しています。
1new apigateway.Stage(this, id, {
2 deployment,
3 stageName,
4 variables: {
5 alias: stageName,
6 },
7});
また、API Gateway からエイリアス Lambda を呼び出すには、Lambda 側のリソースベースポリシーで API Gateway の指定のステージに対して invoke
する権限を与える必要があります。
1new lambda.CfnPermission(this, "invoke-root-function", {
2 action: "lambda:InvokeFunction",
3 functionName: `${functionName}:${stageName}`,
4 principal: "apigateway.amazonaws.com",
5 sourceArn: `arn:aws:execute-api:${region}:${account}:${restApiId}/${stageName}/*/`,
6});
各ステージ変数で、alias
をキーとして blue
・green
の値を設定して、上述のスタックをプロビジョニングすることで、blue
ステージは blue
エイリアスを、green
ステージは green
エイリアスを呼び出せるようになります。
デプロイフロー
ブルーグリーンデプロイの全体の流れはこちらのワークフローになります。 以下ではポイントを抑えて説明します。
1. ソースを Container Image ビルド、Lambda にデプロイ
1- name: Login to Amazon ECR
2 id: login-ecr
3 uses: aws-actions/amazon-ecr-login@v1
4
5- name: Build and push image to Amazon ECR, Deploy the new image to Lambda
6 run: |
7 ECR_REGISTRY=${{ steps.login-ecr.outputs.registry }}
8 ECR_REPOSITORY=bgd
9 IMAGE_TAG=${{ github.sha }}
10
11 # ビルドしたImageをECRにプッシュ
12 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
13 docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
14
15 # ImageをLambdaにデプロイ
16 # ここでlatestバージョンに最新のアプリケーションがあたり、latestのエイリアスであるGreenも最新になります
17 aws lambda update-function-code \
18 --function-name bgd \
19 --image-uri $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
20
21 # Lambdaの更新が完了するまで待機
22 # Lambdaが更新状態であると、後続StepのLambda向けのコマンドが失敗してしまうため、明示的に完了するまで待機しています
23 aws lambda wait function-updated --function-name bgd
2. API Gateway に "blue" ステージを作成
API Gateway の blue
ステージはエンドポイントがそのまま露出してしまうため、今回はデプロイの度に作成、不要になったら削除をするようにしています。
1- name: Deploy(=Create) the "blue" stage to API Gateway
2 run: |
3 # CDKをもとに "blue" ステージを作成
4 yarn cdk deploy bgd-apigateway-blue-stage \
5 --require-approval never \
6 --exclusively
7
8 # CLIから対象のAPI GatewayのAPI IDを取得
9 REST_API_ID=$(aws apigateway get-rest-apis | jq -r '.items[] | select(.name == "bgd") | .id')
10 echo "REST_API_ID=${REST_API_ID}" >> $GITHUB_ENV
11
12 # "blue"ステージのデプロイメントを更新(デプロイメントを更新しないとAPI Gateway自体の更新が反映されないため)
13 aws apigateway create-deployment \
14 --rest-api-id $REST_API_ID \
15 --stage-name blue \
16 --description "For blue stage deployment by CICD"
3. "blue" ステージに対してテスト
ここは適当に書いています。
効果的なテストをまだ想定できていないですが、blue
エンドポイントに対して網羅的なテストを用意することが考えられます。
1- name: Test
2 run: |
3 echo success
4. Lambda の新しいバージョンを発行、新バージョンを "green" にエイリアス
発行した新バージョンを "green" にエイリアスすることで、実際のユーザーに向けて新アプリケーションが提供されます。
1- name: Publish new version of Lambda, Update "green" alias to new version
2 run: |
3 # Lambdaの新しいバージョンを発行
4 aws lambda publish-version --function-name bgd >> response.json
5
6 # 発行されたバージョンのナンバリングを取得
7 JSON=`cat response.json`
8 LAMBDA_NEW_VERSION=$(echo $JSON | jq -r '.Version')
9
10 # 発行バージョンを"green"にエイリアス
11 aws lambda update-alias \
12 --function-name bgd \
13 --function-version $LAMBDA_NEW_VERSION \
14 --name green
15
16 # "green"ステージのデプロイメントを更新(デプロイメントを更新しないとAPI Gatewayの更新が反映されないため)
17 aws apigateway create-deployment \
18 --rest-api-id ${REST_API_ID} \
19 --stage-name green \
20 --description "For green stage deployment by CICD"
5. (option) "blue" ステージを削除
旧バージョンへのスイッチングは、Lambda のバージョン及び API Gateway のデプロイメントで可能なため、今回はエンドポイントとして露出する "blue" ステージは削除します。
1- name: Destroy the "blue" stage to API Gateway
2 run: |
3 yarn cdk destroy bgd-apigateway-blue-stage --force
おわりに
ここまでダラダラと書きましたが、やっていて感じたこととして Lambda と API Gateway の設定について詳しくなる必要があり、ナレッジ共有のコストが比較的高いなと感じています。 また、DB 周りのブルーグリーン問題は、アプリケーションの全体設計にも影響するため、考えることが爆発しそうだなとも感じています。 それなら、ビジネス側ともユーザ規約でも合意を取った上で、メンテナンスモードを用意して、安全なデプロイを計画した方がトータルコストは低いのかもしれないと考えています 💦