CodePipelineのInvokeアクションを利用してAWS Serverless Application Modelでバージョニングを有効化する
2017年12月3日追記 最近のアップデートでAWS SAMがバージョニング/エイリアスに対応しました。
はじめに
こんにちは、中山です。
最近AWS Serverless Application Model(以下AWS SAM)をよく利用しています。基本的にCloudFormationを拡張したモデルという位置づけなので、シンプルで使いやすいところが気に入っています。Swaggerをネイティブな形でサポートしているのも嬉しいポイントです。こちらについては以下のエントリにまとめたので、よろしければ参照ください。
とても便利なモデルなのですが、執筆時点(2017/03/19)でLambda関数のバージョニングと(今回は触れない)エイリアスとの相性が少し悪い印象があります。それぞれ概要を説明すると、バージョニングとはアップロードしたLambda関数のコード毎にバージョン番号を付与する機能、エイリアスは特定のバージョンに対して別名を付ける機能です。より詳細な内容は以下のエントリにまとまっています。
CloudFormationではAWS::Lambda::Versionリソースでバージョニングを利用可能です。ただし、単純にこのリソースを使うだけでは期待した動作はしません。以下に解説します。
何故AWS SAMではバージョニングが難しいのか
この件については、こちらのイシューで議論が進んでいます。AWSの中の人であるsanathkrさんが何故AWS SAMでバージョニングを使うのが難しいのか、分かりやすく説明してくれているため引用します。
A Lambda version is an immutable resource which is created by AWS::Lambda::Version CFN resource type. To create a new Lambda version, you need to add a new AWS::Lambda::Version resource to your CFN template with a different resource name.This behavior makes it hard for SAM to automatically create versions for you. SAM template should somehow automatically store all earlier version resources
具体的なテンプレートで解説します。例えば以下のようなテンプレートがあったとします。
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Test Resources: Func: Type: AWS::Serverless::Function Properties: CodeUri: src/handlers/func Handler: index.handler Runtime: python2.7 FuncVersion1: DeletionPolicy: Retain Type: AWS::Lambda::Version Properties: FunctionName: !GetAtt Func.Arn
AWS::Lambda::Version
リソースの FunctionName
プロパティに、 AWS::Serverless::Function
リソースで定義したLambda関数のARN(関数名でも可)を渡しています。このテンプレートからスタックを作成すると、最初の一度目は期待したとおりバージョン番号が付与されることが分かります。
$ aws lambda list-versions-by-function \ --function-name test-Func-1318F6AZLTXJV \ --query 'Versions[].Version' [ "$LATEST", "1" ]
ただし、Lambda関数を修正した後にスタックを更新しても期待した動作はしません。何故なら、先程引用したコメントに記載されているように、 AWS::Lambda::Version
リソースの論理リソース名が同じなので、常に同じLambda関数を参照しているからです。つまり、 LambdaVersion1
を変更する必要があるというわけです。Lambda関数が1つであれば手動で変更すればよいですが、複数定義している場合は少し面倒です。
対抗フレームワークであるServerless Frameworkの場合、Lambda関数をデプロイする毎に自動的にバージョンを付与してくれます。仕組みを確認してみたのですが、どうやら .serverless
ディレクトリ以下に出力されるCloudFormationテンプレートで、都度 AWS::Lambda::Version
リソースを生成しているようでした。以下のようにLambda関数の変更前後で、論理リソース名を自動生成して新規のリソースを作成していることが分かります。
- Lambda関数変更前
"HelloLambdaVersionQIimobeZuA1DmP1rL7KrHtohCNCcmoUdMKKcEj83tQ": { "Type": "AWS::Lambda::Version", "DeletionPolicy": "Retain", "Properties": { "FunctionName": { "Ref": "HelloLambdaFunction" }, "CodeSha256": "QIimobeZuA1Dm/P1rL7KrHtohCNCcmoUdMKKcEj83tQ=" } }
- Lambda関数変更後
"HelloLambdaVersionWnVKUBrbvz10byODR19IjnzipK5qqXj47FLfdriE": { "Type": "AWS::Lambda::Version", "DeletionPolicy": "Retain", "Properties": { "FunctionName": { "Ref": "HelloLambdaFunction" }, "CodeSha256": "W+nVKUBrbvz10byODR19IjnzipK+5qqXj+47FLfdriE=" } }
一応この問題に対してAWS SAMでも対応を検討しているようですが、すぐに使いたい場合はワークアラウンドが必要です。方法はいろいろと考えられますが、今回はこちらのコメントで紹介されていた、CodePipelineのInvokeアクションで起動させたLambda関数によりバージョニングを有効化する方法をご紹介します。
ワークアラウンド
今回は以下のような構成を作ってみました。
コードはGitHubにアップロードしておきました。ご自由にお使いください。
処理フローは単純です。GitHubからフェッチしてきたソースコードをもとに、CodePipelineによりテスト/承認/デプロイの後、InvokeアクションでLambda関数を起動させます。Lambda関数のコードは以下のリポジトリを参考に作っています。Node.js製だったので、深遠な理由によりPythonに変換しています。
CodePipeline/CodeBuildなどは以下のエントリで紹介したものを流用しています。
CodePipelineのInvokeアクション
CodePipelineはInvokeアクションでLambda関数を起動し、前後のステージと連携させることが可能です。詳細なドキュメントはこちらです。今回はCloudFormationで作っています。テンプレートは以下の通りです。
- Name: Invoke Actions: - Name: publish-version ActionTypeId: Category: Invoke Owner: AWS Version: 1 Provider: Lambda Configuration: FunctionName: !Ref FunctionName InputArtifacts: - Name: DeployOutput
内容は単純です。 FunctionName
に起動したいLambda関数名を指定し、前段のステージ(デプロイ)から渡されたアウトプットを InputArtifacts
に指定しています。
CodePipelineにアタッチするIAM Roleのポリシーに注意してください。Lambda関数の起動、起動させるLambda関数の表示を内部的に行っているので、以下のようなパーミッションが必要です。
- Sid: LambdaAccess Effect: Allow Action: - lambda:InvokeFunction - lambda:ListFunctions Resource: "*"
バージョン番号を付与するLambda関数
from __future__ import print_function from zipfile import ZipFile from botocore.client import ClientError, Config import boto3 import json OUTPUT_ZIP_PATH = '/tmp/deploy-output.zip' OUTPUT_JSON_PATH = 'deploy-output.json' codepipeline = boto3.client('codepipeline') def extract_file(): with ZipFile(OUTPUT_ZIP_PATH) as zip: with zip.open(OUTPUT_JSON_PATH) as f: return json.loads(f.read()) def send_fail(job_id, message): codepipeline.put_job_failure_result( jobId=job_id, failureDetails={'type': 'JobFailed', 'message': message} ) def handler(event, context): print(event) job_id = event['CodePipeline.job']['id'] bucket_name = event['CodePipeline.job']['data']['inputArtifacts'][0]['location']['s3Location']['bucketName'] object_key = event['CodePipeline.job']['data']['inputArtifacts'][0]['location']['s3Location']['objectKey'] try: boto3.resource('s3', config=Config(signature_version='s3v4')).meta.client.download_file(bucket_name, object_key, OUTPUT_ZIP_PATH) except ClientError as e: send_fail(job_id, e.response['Error']) return deploy_output = extract_file() for func_arn in deploy_output.keys(): try: boto3.client('lambda').publish_version(FunctionName=deploy_output[func_arn]) except ClientError as e: send_fail(job_id, e.response['Error']) return codepipeline.put_job_success_result(jobId=job_id) return event
- 32行: インプットで渡されるデータを
/tmp
以下にダウンロードしています - ファイルはアーティファクトのアップロード先バケットに保存され、
event
にパスが記述されています - ファイル自体はZipで固められていて、JSONファイルが入っています
- 保存するファイル名はCodePipeline側で指定可能です
- 今回は以下のようにLambda関数のARN(CloudFormationのアウトプットの内容)が入ってます
{ "Func2Arn": "arn:aws:lambda:us-east-1:************:function:aws-sam-simple-Func2-BTFIE1WXMZG5", "Func1Arn": "arn:aws:lambda:us-east-1:************:function:aws-sam-simple-Func1-15KJYJ3NJERWZ" }
- 40行: Boto3のpublish_versionメソッドを利用してバージョンを付与しています
CodePipelineからInvokeされた場合には、Lambda関数の処理が正常に終了したのか、そうでないのかをCodePipeline側に伝える必要があります。その処理には以下のメソッドを利用します。
- put_job_success_result
- 処理が成功した場合
- put_job_failure_result
- 処理が失敗した場合
動作確認
動作確認用にLambda関数を2つ定義しているリポジトリを作成しておきました。
まずこのコードにpushすると、CI/CDパイプラインが走って最終的にバージョンが付与されることが確認できます。
$ aws lambda list-versions-by-function \ --function-name aws-sam-simple-Func1-15KJYJ3NJERWZ \ --query 'Versions[].Version' [ "$LATEST", "1" ] $ aws lambda list-versions-by-function \ --function-name aws-sam-simple-Func2-BTFIE1WXMZG5 \ --query 'Versions[].Version' [ "$LATEST", "1" ]
Lambda関数の修正後、コードをpushすると都度バージョンが付いていることが確認できます。
$ aws lambda list-versions-by-function \ --function-name aws-sam-simple-Func1-15KJYJ3NJERWZ \ --query 'Versions[].Version' [ "$LATEST", "1", "2" ] $ aws lambda list-versions-by-function \ --function-name aws-sam-simple-Func2-BTFIE1WXMZG5 \ --query 'Versions[].Version' [ "$LATEST", "1", "2" ]
まとめ
いかがだったでしょうか。
CodePipelineのInvokeアクションを利用して、AWS SAMで定義しているLambda関数に自動でバージョンを付与する仕組みをご紹介しました。とりあえず「動くもの」を作ってみたレベルなので、この仕組がサービスに投入できるレベルなのか、利用する上で注意すべき内容などを詳細に調査していない点は注意してください。「こういった仕組みもありますよ」と理解いただければ幸いです。
本エントリがみなさんの参考になれば幸いに思います。