AWS SAMがめちゃめちゃアップデートされてる件 – ClassmethodサーバーレスAdvent Calendar 2017 #serverless #adventcalendar #reinvent

この記事は公開されてから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 セクションで定義されたプロパティが追加されます。例えば以下の場合、 Func1SecurityGroupIds を追加しているため、計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関数にロールバックさせるといったことが可能になります。

[新機能]CodeDeployを利用したLambdaのバージョン間の段階デプロイ #reinvent

ちなみにですが、API Gatewayのカナリアリリースとは異なる機能になります。ちょっと混乱しますねw

【新機能】 Amazon API Gateway が Canary Release Deployments (新しいバージョンへの部分的な振り分け) に対応しました #reinvent

AWS::Serverless::Function リソースに DeploymentPreference というプロパティが追加されています。現時点でサポートされている設定は以下の通りです。

  • Type : どういった方法で新しいLambda関数にトラフィックを流すか
  • Alarms : デプロイ中、閾値を超過した場合に以前のLambda関数へロールバックさせたいCloudWatch Alarm
  • Hooks : デプロイの前後で実行させたいLambda関数

なかなか奥深い機能なので今回は簡単に TypeLinear10PercentEvery1Minute を指定してどういったフローでトラフィックが切り替わっていくのかを見てみたいと思います。 AlarmsHooks を利用したより実践的な内容は後ほど別エントリでご紹介できれば。

まず、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! 📻」と書かれています。楽しみですね。今後もアップデート内容をご紹介していきたいと思います。

本エントリがみなさんの参考になれば幸いに思います。