AWS SAMを通してCodeDeployを利用したLambda関数のデプロイを理解する – ClassmethodサーバーレスAdvent Calendar 2017 #serverless #adventcalendar #reinvent
はじめに
こんにちは、中山です。
このエントリはServerless Advent Calendar 2017 7日目の記事です。
以前、3日目のエントリでAWS SAMのアップデートをご紹介しました。
本エントリではこのアップデートの内、安全なLambda関数のデプロイについてより詳細にご紹介したいと思います。
目次
- 基本的な機能の紹介
- DeploymentPreferenceプロパティを有効化すると何が作成されるのか
- トラフィックはどのように流れていくのか
- アラームを設定するとどのような動作になるのか
- フックを使ってみる
基本的な機能の紹介
「安全なLambda関数のデプロイ」とは、要するにre:Invent 2017中に発表されたCodeDeployの機能を利用したデプロイのことです。この機能を利用することで以下のような柔軟なデプロイが可能となります。
- エイリアスが付与された新しいLambda関数に対して段階的にトラフィックを流す
- CloudWatch Alarmを設定し、デプロイ中にしきい値を超過した場合以前のLambda関数にロールバックさせる
- デプロイ前後にLambda関数を実行して任意のロジック(例えばE2Eテストなど)を組み込む
このアップデートはAWS SAMからも利用可能です。 AWS::Serverless::Lambda
リソースに DeploymentPreference
プロパティを設定することで使えます。現在サポートされている設定は以下3つです。
Type
: どういった方法で新しいLambda関数にトラフィックを流すかAlarms
: デプロイ中、しきい値を超過した場合に以前のLambda関数へロールバックさせたいCloudWatch AlarmHooks
: デプロイの前後で実行させたいLambda関数
基本機能を解説したところで、より詳細な内容を以下で解説していきます。
DeploymentPreferenceプロパティを有効化すると何が作成されるのか
AWS SAMとは要するにCloudFormationの拡張機能です。AWS SAM特有のリソースタイプが複数存在していますが、これらを使ったとしても最終的に素のCloudFormationがパース可能なリソースタイプに変換されます。例えば、 AWS::Serverless::Function
リソースを利用してスタックを作成すると、実際には AWS::Lambda::Function
が作られることが分かるでしょう。
では AWS::Serverless:Function
リソースに DeploymentGroup
プロパティを付与するとどのようなリソースが作成されるのか、確認してみましょう。ドキュメントにまとまっていますが、より詳細に見ていきたいと思います。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Safe Lambda Deployment Samples - Basic Resources: Func1: Type: AWS::Serverless::Function Properties: Runtime: python3.6 Handler: index.handler AutoPublishAlias: dev CodeUri: src/handlers/func1 DeploymentPreference: Enabled: true Type: AllAtOnce
スタック作成後、 該当のプロパティによってどのようなリソースが作られたのか確認してみます。
$ aws cloudformation describe-stack-resources \ --stack-name basic-dev-v1 \ --query 'StackResources[?contains(LogicalResourceId,`Deploy`)].[ResourceType,LogicalResourceId,PhysicalResourceId]' \ --output text AWS::IAM::Role CodeDeployServiceRole basic-dev-v1-CodeDeployServiceRole-1U9ZZRVD66DOA AWS::CodeDeploy::DeploymentGroup Func1DeploymentGroup basic-dev-v1-Func1DeploymentGroup-1HBVC4XH7PXRG AWS::CodeDeploy::Application ServerlessDeploymentApplication basic-dev-v1-ServerlessDeploymentApplication-1GDVKHZYTTA0N
出力結果から以下の点が確認できます。
- CodeDeployのService Role用IAM Roleが作成される
- CodeDeployのDeployment Groupが作成される
- CodeDeployのApplicationが作成される
- 各物理リソースIDはスタック名をベースにランダムな文字列になる
- Deployment Groupの論理リソースIDは
<AWS::Serverless::Functionリソースの論理リソースID>DeploymentGroup
という形式になる - ドキュメントに記載されているようにDeployment Groupが
AWS::Serverless::Function
リソース毎に作成される - 逆に、
AWS::IAM::Role
とAWS::CodeDeploy::Application
リソースの論理リソースIDは一意になるような命名になっている - つまり、上記2リソースはスタック毎に1つだけ作成されることが分かる
更に詳細を眺めてみます。まずService Role用IAM Roleです。どのようなポリシーがアタッチされているか見てみると、マネージドポリシーである AWSCodeDeployRoleForLambda
ポリシーのみ付与されていることが分かります。
$ aws iam list-attached-role-policies \ --role-name basic-dev-v1-CodeDeployServiceRole-1U9ZZRVD66DOA { "AttachedPolicies": [ { "PolicyName": "AWSCodeDeployRoleForLambda", "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda" } ] }
Applicationの設定内容を確認してみます。compute platformがLambdaになっている/Deployment Groupが存在していることが分かります。
$ aws deploy get-application \ --application-name basic-dev-v1-ServerlessDeploymentApplication-1GDVKHZYTTA0N { "application": { "applicationId": "1b646ab6-8e84-49bd-b4a0-8a7f8ace269a", "applicationName": "basic-dev-v1-ServerlessDeploymentApplication-1GDVKHZYTTA0N", "createTime": 1512243240.848, "linkedToGitHub": false, "computePlatform": "Lambda" } } $ aws deploy list-deployment-groups \ --application-name basic-dev-v1-ServerlessDeploymentApplication-1GDVKHZYTTA0N { "deploymentGroups": [ "basic-dev-v1-Func1DeploymentGroup-1HBVC4XH7PXRG" ] }
Deployment Groupの設定は以下のようになっています。デフォルトでロールバックが有効化されていることが分かります。
$ aws deploy get-deployment-group \ --application-name basic-dev-v1-ServerlessDeploymentApplication-1GDVKHZYTTA0N \ --deployment-group-name basic-dev-v1-Func1DeploymentGroup-1HBVC4XH7PXRG { "deploymentGroupInfo": { "applicationName": "basic-dev-v1-ServerlessDeploymentApplication-1GDVKHZYTTA0N", "deploymentGroupId": "65a8398d-66ea-452b-ba57-0eb11ffa9dee", "deploymentGroupName": "basic-dev-v1-Func1DeploymentGroup-1HBVC4XH7PXRG", "deploymentConfigName": "CodeDeployDefault.LambdaAllAtOnce", "ec2TagFilters": [], "onPremisesInstanceTagFilters": [], "autoScalingGroups": [], "serviceRoleArn": "arn:aws:iam::************:role/basic-dev-v1-CodeDeployServiceRole-1U9ZZRVD66DOA", "triggerConfigurations": [], "autoRollbackConfiguration": { "enabled": true, "events": [ "DEPLOYMENT_STOP_ON_REQUEST", "DEPLOYMENT_STOP_ON_ALARM", "DEPLOYMENT_FAILURE" ] }, "deploymentStyle": { "deploymentType": "BLUE_GREEN", "deploymentOption": "WITH_TRAFFIC_CONTROL" }, "computePlatform": "Lambda" } }
ドキュメントにも記載されていますが、素のCloudFormationに変換されたテンプレートを見ると AWS::Lambda::Alias
リソースに UpdatePolicy
が設定されていることが分かります。この設定がされると、Lambda関数のエイリアスに変化が発生した時にCodeDeployを利用したデプロイが利用可能になります。 AWS::Lambda::Alias
と UpdatePolicy
の使い方はこちらのドキュメントにまとまっています。今回は特にHookの設定を行っていないためApplication/Deployment Groupのみ設定されています。
"Func1Aliasv1": { "Type": "AWS::Lambda::Alias", "UpdatePolicy": { "CodeDeployLambdaAliasUpdate": { "ApplicationName": { "Ref": "ServerlessDeploymentApplication" }, "DeploymentGroupName": { "Ref": "Func1DeploymentGroup" } } },
まとめます。 DeploymentPreference
プロパティを有効化するとCodeDeploy関連のリソースが自動的に作成されることが分かりました。もちろんAWS SAMを利用せず素のCloudFormationでこれらのリソースを定義することも可能ですが、プロパティを設定するだけで自動的に作成してくれるのは便利ですね。
トラフィックはどのように流れていくのか
Type
に「トラフィックの流し方」を指定することでどのように新しいLambda関数へトラフィックを流したいか指定可能です。該当のドキュメントはこちらです。細かい設定が可能になっています。大きく分けて3パターンあることが分かります。
- LinearXPercentYMinutes
- Y分毎にXパーセントずつ新しいLambda関数へトラフィックを流し、100%になるまでこれを継続する
- CanaryXPercentYMinutes
- XパーセントのトラフィックをY分間新しいLambda関数へ流し、その後一気に100パーセントのトラフィックを流す
- AllAtOnce
- 即時で100%のトラフィックを新しいLambda関数に流す
Hooks
などを使える以外は既存のデプロイ方法と同じ
それぞれどういった動きになるのか、各パターンのうち1つをピックアップして検証してみましょう。なお、検証にはAPI GatewayのバックエンドにLambda関数が存在し、GETメソッドでInvoke、レスポンスに現在のバージョンを返すという動作にしています。
補足すると、CodeDeployはデプロイメントコンフィギュレーションをカスタマイズ可能なため、事前に用意されているものでは要件に合わない場合に自分で作成することも可能です。
LinearXPercentYMinutes
事前に用意されている設定は以下の通りです。
Linear10PercentEvery1Minute
Linear10PercentEvery2Minutes
Linear10PercentEvery3Minutes
Linear10PercentEvery10Minutes
今回は Linear10PercentEvery1Minute
を試してみます。サンプルとなるテンプレートは以下の通りです。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Safe Lambda Deployment Samples - Linear10PercentEvery1Minute Parameters: Stage: Type: String Default: dev ArtifactBucket: Type: String Resources: Api: Type: AWS::Serverless::Api Properties: StageName: !Ref Stage DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: !Sub s3://${ArtifactBucket}/swagger.yml Variables: Stage: !Ref Stage Func1: Type: AWS::Serverless::Function Properties: Runtime: python3.6 Handler: index.handler AutoPublishAlias: !Ref Stage CodeUri: src/handlers/func1 DeploymentPreference: Enabled: true Type: Linear10PercentEvery1Minute Events: Func1Api: Type: Api Properties: Path: / Method: GET RestApiId: !Ref Api Outputs: ApiEndpoint: Value: !Sub https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${Stage}
- Lambda関数
import json def handler(event, context): message = json.dumps({'message': 'v1'}) response = {'statusCode': 200, 'headers': {'Content-Type': 'application/json'}, 'body': message} return response
Lambda関数にバージョンの番号を出力させるようにしてあるのでそれを修正後、デプロイしてみます。すると以下のように徐々に新しいLambda関数へトラフィックが流れている点、さらにトラフィックの流量も徐々に増えている点が確認できます。また、100パーセントになるまでおおよそ10分(10%/count x 1min)かかることが分かります。
$ while true; do printf "$(date): $(curl -s 'https://n6k1g307mg.execute-api.ap-northeast-1.amazonaws.com/dev')\n"; sleep 1; done # デプロイ開始(最初は10パーセント程度) Sun Dec 3 11:25:22 JST 2017: {"message": "v1"} Sun Dec 3 11:25:23 JST 2017: {"message": "v1"} Sun Dec 3 11:25:24 JST 2017: {"message": "v1"} Sun Dec 3 11:25:26 JST 2017: {"message": "v2"} Sun Dec 3 11:25:27 JST 2017: {"message": "v1"} Sun Dec 3 11:25:29 JST 2017: {"message": "v1"} Sun Dec 3 11:25:30 JST 2017: {"message": "v1"} Sun Dec 3 11:25:32 JST 2017: {"message": "v1"} Sun Dec 3 11:25:33 JST 2017: {"message": "v1"} Sun Dec 3 11:25:35 JST 2017: {"message": "v1"} <snip> # 50パーセント Sun Dec 3 11:29:17 JST 2017: {"message": "v2"} Sun Dec 3 11:29:18 JST 2017: {"message": "v1"} Sun Dec 3 11:29:20 JST 2017: {"message": "v2"} Sun Dec 3 11:29:21 JST 2017: {"message": "v1"} Sun Dec 3 11:29:23 JST 2017: {"message": "v1"} Sun Dec 3 11:29:24 JST 2017: {"message": "v1"} Sun Dec 3 11:29:25 JST 2017: {"message": "v1"} Sun Dec 3 11:29:27 JST 2017: {"message": "v1"} Sun Dec 3 11:29:28 JST 2017: {"message": "v2"} Sun Dec 3 11:29:30 JST 2017: {"message": "v2"} Sun Dec 3 11:29:31 JST 2017: {"message": "v1"} Sun Dec 3 11:29:33 JST 2017: {"message": "v2"} Sun Dec 3 11:29:34 JST 2017: {"message": "v1"} # 100パーセント Sun Dec 3 11:34:20 JST 2017: {"message": "v2"} Sun Dec 3 11:34:22 JST 2017: {"message": "v2"} Sun Dec 3 11:34:23 JST 2017: {"message": "v2"} Sun Dec 3 11:34:25 JST 2017: {"message": "v2"} Sun Dec 3 11:34:26 JST 2017: {"message": "v2"} Sun Dec 3 11:34:28 JST 2017: {"message": "v2"} Sun Dec 3 11:34:29 JST 2017: {"message": "v2"} Sun Dec 3 11:34:31 JST 2017: {"message": "v2"}
CanaryXPercentYMinutes
事前に用意されている設定は以下の通りです。
Canary10Percent5Minutes
Canary10Percent10Minutes
Canary10Percent15Minutes
Canary10Percent30Minutes
今回は Canary10Percent5Minutes
を試してみます。サンプルとなるテンプレートは以下の通りです。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Safe Lambda Deployment Samples - Canary10Percent5Minutes Parameters: Stage: Type: String Default: dev ArtifactBucket: Type: String Resources: Api: Type: AWS::Serverless::Api Properties: StageName: !Ref Stage DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: !Sub s3://${ArtifactBucket}/swagger.yml Variables: Stage: !Ref Stage Func1: Type: AWS::Serverless::Function Properties: Runtime: python3.6 Handler: index.handler AutoPublishAlias: !Ref Stage CodeUri: src/handlers/func1 DeploymentPreference: Enabled: true Type: Canary10Percent5Minutes Events: Func1Api: Type: Api Properties: Path: / Method: GET RestApiId: !Ref Api Outputs: ApiEndpoint: Value: !Sub https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${Stage}
先程と同じようにLambda関数を修正後デプロイしてみます。すると以下のようにおおよそ5分経過すると新しいLambda関数へトラフィックが完全に切り替わることが分かります。
$ while true; do printf "$(date): $(curl -s 'https://7p3plkeoze.execute-api.ap-northeast-1.amazonaws.com/dev')\n"; sleep 1; done # デプロイ開始(少しだけトラフィックが流れている) Sun Dec 3 11:01:11 JST 2017: {"message": "v1"} Sun Dec 3 11:02:52 JST 2017: {"message": "v2"} Sun Dec 3 11:02:53 JST 2017: {"message": "v1"} Sun Dec 3 11:02:54 JST 2017: {"message": "v1"} Sun Dec 3 11:02:56 JST 2017: {"message": "v1"} <snip> # おおよそ5分経過すると完全に切り替わる Sun Dec 3 11:07:09 JST 2017: {"message": "v1"} Sun Dec 3 11:07:11 JST 2017: {"message": "v2"} Sun Dec 3 11:07:12 JST 2017: {"message": "v2"} Sun Dec 3 11:07:14 JST 2017: {"message": "v2"} Sun Dec 3 11:07:15 JST 2017: {"message": "v2"} Sun Dec 3 11:07:16 JST 2017: {"message": "v2"} Sun Dec 3 11:07:18 JST 2017: {"message": "v2"} Sun Dec 3 11:07:19 JST 2017: {"message": "v2"} Sun Dec 3 11:07:20 JST 2017: {"message": "v2"}
AllAtOnce
このパターンで指定可能なのは1つだけです。
AllAtOnce
サンプルとなるテンプレートは以下の通りです。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Safe Lambda Deployment Samples - AllAtOnce Parameters: Stage: Type: String Default: dev ArtifactBucket: Type: String Resources: Api: Type: AWS::Serverless::Api Properties: StageName: !Ref Stage DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: !Sub s3://${ArtifactBucket}/swagger.yml Variables: Stage: !Ref Stage Func1: Type: AWS::Serverless::Function Properties: Runtime: python3.6 Handler: index.handler AutoPublishAlias: !Ref Stage CodeUri: src/handlers/func1 DeploymentPreference: Enabled: true Type: AllAtOnce Events: Func1Api: Type: Api Properties: Path: / Method: GET RestApiId: !Ref Api Outputs: ApiEndpoint: Value: !Sub https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${Stage}
再度Lambda関数を修正後デプロイしてみます。すると以下のようにデプロイが開始するとすぐに切り替わることが分かります。
$ while true; do printf "$(date): $(curl -s 'https://5ttwzagmqf.execute-api.ap-northeast-1.amazonaws.com/dev')\n"; sleep 1; done # デプロイが始まるとすぐ切り替わる Sun Dec 3 11:33:22 JST 2017: {"message": "v1"} Sun Dec 3 11:33:24 JST 2017: {"message": "v1"} Sun Dec 3 11:33:25 JST 2017: {"message": "v1"} Sun Dec 3 11:33:27 JST 2017: {"message": "v1"} Sun Dec 3 11:33:28 JST 2017: {"message": "v1"} Sun Dec 3 11:33:30 JST 2017: {"message": "v1"} Sun Dec 3 11:33:32 JST 2017: {"message": "v1"} Sun Dec 3 11:33:34 JST 2017: {"message": "v2"} Sun Dec 3 11:33:36 JST 2017: {"message": "v2"} Sun Dec 3 11:33:37 JST 2017: {"message": "v2"} Sun Dec 3 11:33:38 JST 2017: {"message": "v2"} Sun Dec 3 11:33:40 JST 2017: {"message": "v2"} Sun Dec 3 11:33:41 JST 2017: {"message": "v2"} Sun Dec 3 11:33:43 JST 2017: {"message": "v2"} Sun Dec 3 11:33:44 JST 2017: {"message": "v2"} Sun Dec 3 11:33:46 JST 2017: {"message": "v2"} Sun Dec 3 11:33:48 JST 2017: {"message": "v2"} Sun Dec 3 11:33:49 JST 2017: {"message": "v2"}
各トラフィックの振り分け方法を確認すると、以下のようなユースケースで選択するとよいかと思います。
- LinearXPercentYMinutes
- デプロイの影響範囲を最小限にしたい場合(例えば大きなメジャーアップデートなど)
- また、両バージョンの混在時間/デプロイ時間が一番長いのでそれが許容できるかもポイントになります
- CanaryXPercentYMinutes
- 軽微な修正の場合
- AllAtOnce
- CodeDeployの便利な機能(
Hooks
やAlarms
)を利用したいが、デプロイ時間は短縮したい場合
ちなみに Type
は途中から変更可能です。デプロイしたい内容によって適宜変更するとよいでしょう。
アラームを設定するとどのような動作になるのか
Alarms
にCloudWatch Alarm名を指定することで、デプロイ中意図しない動作をした場合に以前のLambda関数へロールバックさせることが可能です。例えば、Lambda関数のError rate用メトリクスからアラームを設定しておき、しきい値を超過した場合に元に戻すといった用途が考えられます。
サンプルとなるテンプレートは以下の通りです。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Safe Lambda Deployment Samples - Alarm Resources: Func1Alarm: Type: AWS::CloudWatch::Alarm Properties: Namespace: AWS/Lambda Dimensions: - Name: FunctionName Value: !Ref Func1 MetricName: Errors ComparisonOperator: GreaterThanOrEqualToThreshold Statistic: Sum Period: 60 EvaluationPeriods: 1 Threshold: 1 Func1: Type: AWS::Serverless::Function Properties: Runtime: python3.6 CodeUri: src/handlers/func1 Handler: index.handler AutoPublishAlias: dev DeploymentPreference: Enabled: true Type: Linear10PercentEvery10Minutes Alarms: - !Ref Func1Alarm
当初Lambda関数を以下のようにしてデプロイしておきます。
- バージョン1
def handler(event, context): print(event)
続いて、ロールバックの動作を確認したいのでこのLambda関数をわざと失敗させるため以下のように例外を出力させます。
- バージョン2
def handler(event, context): raise Exception('Exception occured')
「バージョン2」のLambda関数をデプロイさせしばらく待つと、デプロイに失敗しロールバックすることが分かります。
$ aws deploy get-deployment \ --deployment-id d-QBP1NHUCP \ --query 'deploymentInfo.[deploymentOverview,errorInformation,rollbackInfo]' [ { "Pending": 0, "InProgress": 0, "Succeeded": 0, "Failed": 1, "Skipped": 0, "Ready": 0 }, { "code": "ALARM_ACTIVE", "message": "the deployment is cancelled due to an abort request" }, { "rollbackDeploymentId": "d-A9LOSCYCP", "rollbackMessage": "Deployment d-QBP1NHUCP terminated. Automatic rollback is triggered with a DeploymentId d-A9LOSCYCP." } ]
今回「わざとらしく」アラーム機能の動作を確認してみましたが、注視しておきたいメトリクス(例えばDead Letter Queue)に対しこの設定をしておくと心の平穏が訪れそうです。
フックを使ってみる
Hooks
を使うことでデプロイの前後にLambda関数を実行し、ロジックを組み込むことが可能です。現在以下2つのポイントにフックさせることができます。
BeforeAllowTraffic
: 新しいLambda関数にトラフィックを流す前- AWS SAMでは
PreTraffic
AfterAllowTraffic
: 新しいLambda関数にトラフィックが流れた後- AWS SAMでは
PostTraffic
ポイントは BeforeAllowTraffic
の時点で新しいLambda関数がデプロイされているが、トラフィックはまだ流れてないという点です。つまりシステムを利用しているユーザに影響をあたえること無くロジックを挟むことが可能です。例えば、 BeforeAllowTraffic
でインテグレーションテスト、 AfterAllowTraffic
E2Eテストといったことができるでしょう。夢がひろがりングですね。
早速試してみます。今回は BeforeAllowTraffic
でテストさせたいLambdaをInvokeしレスポンスをチェック、 AfterAllowTraffic
でHTTPエンドポイントにアクセスしレスポンスをチェック、ということをやってみます。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Safe Lambda Deployment Samples - Hooks Parameters: Stage: Type: String Default: dev ArtifactBucket: Type: String Resources: PreTrafficFunc: Type: AWS::Serverless::Function Properties: FunctionName: CodeDeployHook_PreTraffic Runtime: python3.6 Handler: index.handler CodeUri: src/handlers/pre-traffic-func Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - codedeploy:PutLifecycleEventHookExecutionStatus Resource: "*" - Effect: Allow Action: - lambda:InvokeFunction Resource: !GetAtt Func1.Arn DeploymentPreference: Enabled: false Environment: Variables: FUNC1_NAME: !Ref Func1 FUNC1_ARN_VERSION: !Ref Func1.Version PostTrafficFunc: Type: AWS::Serverless::Function Properties: FunctionName: CodeDeployHook_PostTraffic Runtime: python3.6 Handler: index.handler CodeUri: src/handlers/post-traffic-func Policies: - CloudFormationDescribeStacksPolicy: {} - Version: 2012-10-17 Statement: - Effect: Allow Action: - codedeploy:PutLifecycleEventHookExecutionStatus Resource: "*" DeploymentPreference: Enabled: false Environment: Variables: STACK_NAME: !Ref AWS::StackName FUNC1_ARN_VERSION: !Ref Func1.Version Api: Type: AWS::Serverless::Api Properties: StageName: !Ref Stage DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: !Sub s3://${ArtifactBucket}/swagger.yml Variables: Stage: !Ref Stage Func1: Type: AWS::Serverless::Function Properties: Runtime: python3.6 Handler: index.handler AutoPublishAlias: !Ref Stage CodeUri: src/handlers/func1 DeploymentPreference: Enabled: true Type: AllAtOnce Hooks: PreTraffic: !Ref PreTrafficFunc PostTraffic: !Ref PostTrafficFunc Events: Func1Api: Type: Api Properties: Path: / Method: GET RestApiId: !Ref Api Outputs: ApiEndpoint: Value: !Sub https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${Stage}
それぞれのLambda関数は以下のようにしました。フックで呼び出されるLambda関数は呼び出し元となるCodeDeployに結果を返す必要があるので、PutLifecycleEventHookExecutionStatus APIを利用しています。
BeforeAllowTraffic
のLambda関数
import os import json import boto3 CODE_DEPLOY = boto3.client('codedeploy') AWS_LAMBDA = boto3.client('lambda') FUNC1_VERSION = os.getenv('FUNC1_ARN_VERSION').split(':')[-1] def invoke_aws_lambda(): func1_name = os.getenv('FUNC1_NAME') return AWS_LAMBDA.invoke(FunctionName=func1_name, InvocationType='RequestResponse', LogType='Tail', Qualifier=FUNC1_VERSION) def notify_execution_status(event, status): deployment_id = event.get('DeploymentId') execution_id = event.get('LifecycleEventHookExecutionId') return CODE_DEPLOY.put_lifecycle_event_hook_execution_status(deploymentId=deployment_id, lifecycleEventHookExecutionId=execution_id, status=status) def handler(event, context): print(event) status = 'Succeeded' try: aws_lambda_log = invoke_aws_lambda() payload = json.loads(aws_lambda_log.get('Payload').read()) assert json.loads(payload.get('body')).get('message') == FUNC1_VERSION notify_response = notify_execution_status(event, status) except Exception as e: print(e) status = 'Failed' notify_execution_status(event, status) else: print(notify_response)
AfterAllowTraffic
のLambda関数
import os import boto3 import requests CODE_DEPLOY = boto3.client('codedeploy') CLOUDFORMATION = boto3.client('cloudformation') def get_api_endpoint(): stack_name = os.getenv('STACK_NAME') stack_outputs = CLOUDFORMATION.describe_stacks(StackName=stack_name).get('Stacks')[0].get('Outputs') for stack_output in stack_outputs: for output_value in stack_output.values(): if output_value == 'ApiEndpoint': return stack_output.get('OutputValue') def get_api_response(): api_endpoint = get_api_endpoint() return requests.get(api_endpoint) def notify_execution_status(event, status): deployment_id = event.get('DeploymentId') execution_id = event.get('LifecycleEventHookExecutionId') return CODE_DEPLOY.put_lifecycle_event_hook_execution_status(deploymentId=deployment_id, lifecycleEventHookExecutionId=execution_id, status=status) def handler(event, context): print(event) status = 'Succeeded' func1_version = os.getenv('FUNC1_ARN_VERSION').split(':')[-1] try: api_response = get_api_response() assert api_response.status_code == 200 assert api_response.json().get('message') == func1_version notify_response = notify_execution_status(event, status) except Exception as e: print(e) status = 'Failed' notify_execution_status(event, status) else: print(notify_response)
- テスト対象となるLambda関数
import json def handler(event, context): message = json.dumps({'message': context.function_version}) response = {'statusCode': 200, 'headers': {'Content-Type': 'application/json'}, 'body': message} return response
テスト対象となるLambda関数を適当に修正した後スタックをアップデートするとCodeDeployのデプロイが実行され、フックに登録したLambda関数がInvokeされることがわかると思います。
今回Lambda自体でインテグレーションテスト、E2Eテストをするといった方法にしてみました。ただ本格的に使うには例えばCIとの連携を考えた方が良いかもしれません。例えば BeforeAllowTraffic
からCircleCIを呼び出してテストするとかできたらおもしろそうですね。今後ももっと使い倒してご紹介できればと思います。
まとめ
いかがだったでしょうか。
CodeDeployの機能を利用したより安全なLambda関数のデプロイ方法についてご紹介しました。デプロイ戦略が増えたことは1ユーザとして嬉しい限りです。今回ご紹介した以外のユースケースもありそうな程奥深い機能なので、今後便利なユースケースが見つかったらまたご紹介できればと思います。
本エントリがみなさんの参考になれば幸いに思います。