この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
2018年2月14日追記 こちらのリリースでいくつかアップデートがあったようです。最新の情報はドキュメントを参照してください。
はじめに
こんにちは、中山です。
このエントリはServerless Advent Calendar 2017 3日目の記事です。
AWS re:Invent 2017でさまざまな新サービスが発表されました。すぐにでも実案件に使ってみたいサービスがたくさんあったのですが、ひっそりと?AWS SAMもアップデートされていました。今回はその内容をご紹介したいと思います。
目次
ポリシーテンプレートの導入
Lambda関数に割り当てるIAMポリシーをどうすべきか悩んだ方は多いと思います。当初はガチガチに管理しようとしていたが、途中から心が折れてFullAccess系のマネージドポリシーを付与するといったパターンはあるある話ですね。こういった問題に対し、新しく導入されたポリシーテンプレートを利用することで、セキュアなIAMポリシーをより簡単に設定ができるようになりました。
このポリシーテンプレートは私の理解では「サーバーレスアプリケーション向けにチューニングされたマネージドポリシー」といった印象です。既存のマネージドポリシーと大きく違う点が2つあります。
- 最終的にはインラインポリシーに変換される
- パラメータを渡すことでリソースをカスタマイズ可能
実際に見てみましょう。
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Policy Template Sample 1
Resources:
Bucket:
Type: AWS::S3::Bucket
Func1:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: python3.6
CodeUri: src/handlers/sample-func1
Policies:
- CloudFormationDescribeStacksPolicy: {}
- S3CrudPolicy:
BucketName: !Ref Bucket
Policies
プロパティに2つのポリシーテンプレートを使用しています。1つは CloudFormationDescribeStacksPolicy
。こちらは cloudformation:DescribeStacks
APIの対象となるリソースを極力絞った形でIAM Roleに付与するためのテンプレートです。ドキュメントを見てみると以下のようなポリシーになります。
"CloudFormationDescribeStacksPolicy": {
"Description": "Gives permission to describe CloudFormation stacks",
"Parameters": {
},
"Definition": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudformation:DescribeStacks"
],
"Resource": {
"Fn::Sub": "arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/*"
}
}
]
}
},
2つ目が S3CrudPolicy
です。先程のポリシーテンプレートとは異なりパラメータにバケット名を渡せるのでポリシーを動的に生成可能です。少し長いですが引用します。S3関連の操作を特定のバケットのみに制限していることが分かります。
"S3CrudPolicy": {
"Description": "Gives read permissions to objects in the S3 Bucket",
"Parameters": {
"BucketName": {
"Description": "Name of the Bucket"
}
},
"Definition": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket",
"s3:GetBucketLocation",
"s3:GetObjectVersion",
"s3:PutObject",
"s3:GetLifecycleConfiguration",
"s3:PutLifecycleConfiguration"
],
"Resource": [
{
"Fn::Sub": [
"arn:${AWS::Partition}:s3:::${bucketName}",
{
"bucketName": {
"Ref": "BucketName"
}
}
]
},
{
"Fn::Sub": [
"arn:${AWS::Partition}:s3:::${bucketName}/*",
{
"bucketName": {
"Ref": "BucketName"
}
}
]
}
]
}
]
}
},
なお、より詳細なサンプルテンプレートがリポジトリにあるので利用する際には参考にしてみてください。
プロパティ共有機能
新しくGlobalsというセクションが追加されたことで、共通のプロパティ(例えばLambda関数のランタイムなど)を簡単に記述することが可能となりました。Serverless Frameworkでいうところの provider
に似てますね。
例えば以下のようなテンプレートがあったとします。
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Globals Sample 1
Globals:
Function:
Runtime: python3.6
Timeout: 60
Handler: index.handler
Environment:
Variables:
TABLE_NAME: test-table
Resources:
Func1:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/sample-func1
Func2:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/sample-func2
トップレベルに Globals
セクションを指定しているため、今回であれば以下のプロパティが2つのLambda関数( Func1
/ Func2
)でも共有可能、つまり個別に指定したのと同じです。
Runtime
Timeout
Handler
Environment
この機能は執筆時点で AWS::Serverless::Lambda
リソースのみサポートされています。また、以下のプロパティは共有することはできません。セキュリティ上のリスク/テンプレートの複雑化などを懸念して明示的に禁止しているようです。
Role
Policies
FunctionName
Events
また、 Globals
セクションで定義した内容のオーバーライドもサポートされています。例えば、基本的に共有のプロパティにしたいがこれだけ変えたい、といった場合に便利です。このオーバーライドですが、共有したいプロパティの型によって動作が少し異なります。順番に見ていきましょう。
- プリミティブ型(文字列/数値/ブーリアン)
Globals
セクションで定義されたプロパティが個別の値に置き換わります。例えば以下の場合、 Func1
でランタイムを上書きしているため、このLambda関数のみランタイムがNode.js 6.10になります。
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Globals Sample 2
Globals:
Function:
Runtime: python3.6
Timeout: 60
Handler: index.handler
Resources:
Func1:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/sample-func1
Runtime: nodejs6.10
- マップ型
Globals
セクションで定義されたプロパティが個別の値にマージされます。例えば以下の場合、 Func1
で環境変数( TABLE_NAME
)を上書きしているため、この関数のみ下記環境変数になります。
TABLE_NAME
:local-table
SOME_KEY
:some-var
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Globals Sample 3
Globals:
Function:
Runtime: python3.6
Timeout: 60
Handler: index.handler
Environment:
Variables:
TABLE_NAME: global-table
Resources:
Func1:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/sample-func1
Environment:
Variables:
TABLE_NAME: local-table
SOME_KEY: some-var
- リスト型
Globals
セクションで定義されたプロパティが追加されます。例えば以下の場合、 Func1
で SecurityGroupIds
を追加しているため、計3つのセキュリティグループ( sg-231e7856
/ sg-351b7d40
/ sg-901177e5
)がLambda関数に設定されます。
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Globals Sample 4
Globals:
Function:
Runtime: python3.6
Timeout: 60
Handler: index.handler
VpcConfig:
SecurityGroupIds:
- sg-231e7856
- sg-351b7d40
SubnetIds:
- subnet-530baf7c
- subnet-060eaa29
Resources:
Func1:
Type: AWS::Serverless::Function
Properties:
Policies:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
CodeUri: src/handlers/sample-func1
VpcConfig:
SecurityGroupIds:
- sg-901177e5
Lambdaのバージョニング/エイリアス対応
以前AWS SAMではなぜバージョニング/エイリアスが難しいのか解説するエントリを書きましたが、ついに対応しました。
CodePipelineのInvokeアクションを利用してAWS Serverless Application Modelでバージョニングを有効化する
さらに、後述する安全なLambda関数のデプロイ機能と組み合わせるとより便利に使えます。ちなみにServerless Frameworkはバージョニングを以前からサポートしていますが、コア機能としてエイリアスは現在サポートされてません。各種プラグイン(例えばserverless-aws-alias)を使う必要があります。
使い方は簡単です。 AWS::Serverless::Function
リソースに AutoPublishAlias
プロパティを付けるだけです。例えば以下のような感じです。
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Version and Alias
Resources:
Func1:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.6
CodeUri: src/handlers/sample-func1
Handler: index.handler
AutoPublishAlias: fuga
何度かLambda関数をデプロイしてみると以下の点が確認できるかと思います。
- デプロイ毎にバージョンが付けられる
- エイリアスが最新バージョンを参照している
$ aws lambda list-aliases --function-name sample2-Func1-1VV8TAAUKY63B
{
"Aliases": [
{
"AliasArn": "arn:aws:lambda:ap-northeast-1:************:function:sample2-Func1-1VV8TAAUKY63B:hoge",
"Name": "fuga",
"FunctionVersion": "2",
"Description": ""
}
]
}
途中でエイリアス名を変更してもエイリアス名しか変わらないようですね(バージョン番号が失われること無く)。
$ aws lambda list-aliases --function-name sample2-Func1-1VV8TAAUKY63B
{
"Aliases": [
{
"AliasArn": "arn:aws:lambda:ap-northeast-1:************:function:sample2-Func1-1VV8TAAUKY63B:hoge",
"Name": "hoge",
"FunctionVersion": "2",
"Description": ""
}
]
}
どうやってバージョニングを実施しているのか確認してみると、Lambda関数がデプロイされる毎に AWS::Function::Version
リソースが一意な論理リソースIDで生成されているようです。Serverless Frameworkと同じような動作となってます。
$ aws cloudformation describe-stack-resources \
--stack-name sam5 \
--query 'StackResources[?ResourceType==`AWS::Lambda::Version`].LogicalResourceId' \
--output text
Func1Version0e1b35aa1a
$ aws cloudformation describe-stack-resources \
--stack-name sam5 \
--query 'StackResources[?ResourceType==`AWS::Lambda::Version`].LogicalResourceId' \
--output text
Func1Version6afe0e7241
ちなみに、ドキュメントに記載されているよう、それぞれ <論理リソースID>.Version
/ <論理リソースID>.Alias
という形式で組み込み関数から参照可能です。
安全なLambda関数のデプロイ
以下のエントリでご紹介したようにCodeDeployを利用したLambda関数のデプロイがサポートされました。エイリアスが付与された新しいLambda関数に対して少しずつトラフィックを流す/デプロイ前後にテストロジックを組み込む/以前のLambda関数にロールバックさせるといったことが可能になります。
ちなみにですが、API Gatewayのカナリアリリースとは異なる機能になります。ちょっと混乱しますねw
【新機能】 Amazon API Gateway が Canary Release Deployments (新しいバージョンへの部分的な振り分け) に対応しました #reinvent
AWS::Serverless::Function
リソースに DeploymentPreference
というプロパティが追加されています。現時点でサポートされている設定は以下の通りです。
Type
: どういった方法で新しいLambda関数にトラフィックを流すか- サポートされている設定はドキュメント参照してください
Alarms
: デプロイ中、閾値を超過した場合に以前のLambda関数へロールバックさせたいCloudWatch AlarmHooks
: デプロイの前後で実行させたいLambda関数
なかなか奥深い機能なので今回は簡単に Type
に Linear10PercentEvery1Minute
を指定してどういったフローでトラフィックが切り替わっていくのかを見てみたいと思います。 Alarms
や Hooks
を利用したより実践的な内容は後ほど別エントリでご紹介できれば。
まず、API GatewayのバックエンドにLambda関数が1つ存在するだけのシンプルなAPIを作成します。ルートリソースに対してGETメソッドを定義しただけの簡易的なものです。
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Safe Lambda Deploy Sample 1
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/sample-func1
Events:
Func1Api:
Type: Api
Properties:
Path: /
Method: GET
RestApiId: !Ref Api
Outputs:
ApiEndpoint:
Value: !Sub https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${Stage}
InvokeされるLambda関数は以下のようにしました。単純にレスポンスを返してるだけです。
import json
def handler(event, context):
message = json.dumps({'message': 'func1-test1'})
response = {'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': message}
return (response)
この状態でAPI Gatewayのエンドポイントにアクセスすると以下のレスポンスが返ってきます。
$ curl -s 'https://rk4ytz0zp9.execute-api.ap-northeast-1.amazonaws.com/dev' -w '\n'
{"message": "func1-test1"}
続いて、AWS SAMテンプレートとLambda関数のコードをそれぞれ以下のように変更します。
- AWS SAMテンプレート
--- sam-after.yml 2017-12-02 16:35:53.000000000 +0900
+++ sam-before.yml 2017-12-02 16:35:46.000000000 +0900
@@ -30,6 +30,9 @@
Handler: index.handler
AutoPublishAlias: !Ref Stage
CodeUri: src/handlers/sample-func1
+ DeploymentPreference:
+ Enabled: true
+ Type: Linear10PercentEvery1Minute
Events:
Func1Api:
Type: Api
- Lambda関数のコード
--- index-after.py 2017-12-02 16:37:00.000000000 +0900
+++ index-before.py 2017-12-02 16:36:30.000000000 +0900
@@ -2,7 +2,7 @@
def handler(event, context):
- message = json.dumps({'message': 'func1-test2'})
+ message = json.dumps({'message': 'func1-test1'})
response = {'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': message}
この状態でエンドポイントにアクセスしてみましょう。少しずつ新しいLambda関数へトラフィックが流れていく点、また時間が経過する毎に新しいLambda関数へ多くトラフィックが流れている様子が確認できます。
$ while true; do printf "$(date): $(curl -s 'https://rk4ytz0zp9.execute-api.ap-northeast-1.amazonaws.com/dev')\n"; sleep 1; done
# CodeDeployによるデプロイの開始(当初は新しいLambda関数に少量のトラフィックのみ流れている)
Sat Dec 2 16:40:04 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:05 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:40:06 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:08 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:40:09 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:10 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:11 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:12 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:40:14 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:15 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:16 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:17 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:40:19 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:22 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:40:24 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:25 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:28 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:29 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:31 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:40:32 JST 2017: {"message": "func1-test2"}
<snip>
# このあたりからほとんどのトラフィックが新しいLambda関数に流れている
Sat Dec 2 16:45:22 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:45:23 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:45:24 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:45:26 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:45:31 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:45:38 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:45:42 JST 2017: {"message": "func1-test1"}
Sat Dec 2 16:45:46 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:45:50 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:45:55 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:46:05 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:46:07 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:46:08 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:46:10 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:46:11 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:46:12 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:46:17 JST 2017: {"message": "func1-test2"}
<snip>
# 完全に切り替わった状態
Sat Dec 2 16:49:31 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:33 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:34 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:36 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:38 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:39 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:40 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:42 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:44 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:45 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:47 JST 2017: {"message": "func1-test2"}
Sat Dec 2 16:49:49 JST 2017: {"message": "func1-test2"}
まとめ
いかがだったでしょうか。
AWS SAMのアップデートをご紹介しました。今回の内容だけでも盛り沢山のアップデートとなりましたが、リリースノートを見ると「Lots more to come.. Stay tuned! ?」と書かれています。楽しみですね。今後もアップデート内容をご紹介していきたいと思います。
本エントリがみなさんの参考になれば幸いに思います。