CloudFromationのAWS::Includeを利用してAWS SAMからインラインSwaggerを分離して管理する

はじめに

こんにちは、中山です。

米国時間3/28にCloudFormationのアップデートが発表されました。複数のアップデートがあったのですが、本エントリではその中からAWS::Includeについて、AWS Serverless Application Model(以下 AWS SAM)と交えながらご紹介したいと思います。

AWS::Includeとは何か

一言で説明すると、 CloudFromationにおけるプログラミング言語のモジュールに相当する機能 です。S3にアップロードしたCloudFormationのテンプレートを、別のテンプレートからIncludeすることができます。今までも、ネストスタックやクロススタック参照を利用することで、テンプレートを機能毎に分離して管理することは可能でした。ただし、それぞれ AWS:Include とは似て非なるものなので、以下のような機能はありませんでした。

  • 複数のテンプレートから1つのスタックを作成する
    • AWS::Include をトップレベルで利用することにより可能となった
    • ネストスタックの場合は複数のスタックを作成してしまうので、スタック最大作成数の上限に達してしまう問題があった
  • あるリソースのプロパティの一部を別テンプレートから呼び出す
    • AWS::Include をリソースのプロパティに指定することにより可能となった

AWS::Include を使うことでこういったことが可能になりました!CloudFormationがより「コード化」したと言えますね。

ありがちなユースケースを考えてみます。例えば、社内で決められた規定に沿ったVPC/サブネット用のテンプレートを作成したとします。サブネットをパブリック/プライベート/データストアの3階層に分けるようなものを想定してください。こういったテンプレートを作成しておき、S3にアップロードしておくことで、別のテンプレートからそれを再利用できるという訳です。または、プロパティが長くなりがちなリソースをテンプレートで利用する場合、 AWS::Include で別テンプレートからIncludeすることにより、面倒な作業から開放してくれます。DRY原則の導入ですね!

執筆時点(3/30)では、S3へのアップロードは事前に実施しておく必要があります。ただし、こちらのイシューでAWSの中の人達がコメントしていますが、AWS CLIの aws cloudformation package コマンドに対応しそうです。これが対応してくれるとより便利に使えると思います。

AWS SAMとの連携

以前AWS SAMとSwaggerを連携させる方法を以下のエントリにまとめました。

上記エントリでは、Swaggerファイルをテンプレート内にインラインで定義する場合、テンプレートを分離して扱うのが現状難しいため、行数が長くなってしまうと記述しました。が、 AWS::Include の登場により、インラインSwaggerかつテンプレートの分離が簡単に作成可能になりました。

例えば、インラインSwaggerのファイルを以下のように定義したとします。インラインなのでCloudFormationの組み込み関数が利用可能です。ただし、 AWS::Include でIncludeする場合、現状組み込み関数の短縮表記はサポートされてない点は注意してください。

swagger: 2.0
info:
  title:
    Fn::Sub: swagger-transform-1-${Stage}
  description:
    Fn::Sub: swagger-transform-1-${Stage}
  version: 1.0.0
schemes:
  - https
basePath:
  Fn::Sub: /${Stage}
paths:
  /:
    get:
      summary: Root
      description: |
        Root Method.
      consumes:
        - application/json
      produces:
        - application/json
      parameters:
        - name: number
          in: query
          description: Some number
          required: true
          type: number
          format: integer
      responses:
        "200":
          description: 200 response
          schema:
            $ref: "#/definitions/Empty"
      x-amazon-apigateway-integration:
        responses:
          default:
            statusCode: 200
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Func1.Arn}/invocations
        passthroughBehavior: when_no_templates
        httpMethod: POST
        type: aws
        requestTemplates:
          application/json: |
            {
              #foreach($key in $input.params().querystring.keySet())
              "$key": "$util.escapeJavaScript($input.params().querystring.get($key))" #if($foreach.hasNext),#end
              #end
            }
definitions:
  Empty:
    type: object
    title: Empty Schema

このテンプレートをIncludeするAWS SAMを以下のように記述します。

---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: swagger-transform-1

Parameters:
  Stage:
    Type: String
    Default: dev
  ArtifactBucket:
    Type: String

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: General Configuration
        Parameters:
          - Stage
          - ArtifactBucket
    ParameterLabels:
      Stage:
        default: Stage
      ArtifactBucket:
        default: Artifact Bucket

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Stage
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: !Sub s3://${ArtifactBucket}/swagger.${Stage}.yml

  Func1:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/handlers/func1
      Handler: index.handler
      Runtime: python2.7
      Events:
        PostApi:
          Type: Api
          Properties:
            Path: /
            Method: GET
            RestApiId: !Ref Api

Outputs:
  ApiUrl:
    Value: !Sub https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${Stage}
  • 33 - 36行:
    • AWS::Include を利用して、 DefinitionBody プロパティを別テンプレートから呼び出しています
    • 今回は、Swaggerを検証/開発用途で分ける方式( swagger.dev.yml / swagger.prod.yml )にしてみました

最終的なディレクトリ構成は以下の通りです。

$ tree .
.
├── params
│   ├── param.dev.json
│   └── param.prod.json
├── sam.yml
└── src
    ├── api
    │   ├── swagger.dev.yml
    │   └── swagger.prod.yml
    └── handlers
        └── func1
            └── index.py

5 directories, 6 files

この状態でスタックを作成してみましょう。 stage 及び s3_bucket 変数はご自身の環境に合うよう、適宜読み替えてください。

$ aws cloudformation package \
  --template-file sam.yml \
  --s3-bucket $s3_bucket \
  --output-template-file .sam/packaged.yml
Uploading to 0906c54356fd357815c158c45a3ff3e5  306 / 306.0  (100.00%)
Successfully packaged artifacts and wrote output template to file .sam/packaged.yml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /path/to/.sam/packaged.yml --stack-name <YOUR STACK NAME>
$ aws cloudformation deploy \
  --template-file .sam/packaged.yml \
  --stack-name test-$stage \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides $(cat params/param.$stage.json | jq -r '.Parameters | to_entries | map("\(.key)=\(.value|tostring)") | .[]' | tr '\n' ' ' | awk '{print}')
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - test-dev

スタックの作成後、アウトプットに出力されるAPI GatewayのURLにアクセスするとLambda関数が実行されます(今回はJSONを返すだけ)。やりましたね。

$ curl https://stkgjfohn8.execute-api.ap-northeast-1.amazonaws.com/dev -w '\n'
{"body": "{\"message\": \"Hello World!\"}", "headers": {"x-custom-header": "My Header Value"}, "statusCode": 200}

まとめ

いかがだったでしょうか。

AWS::Include の機能と、AWS SAMとの連携方法をご紹介しました。CloudFormationは当初「コード」ではなく「テンプレート」という印象が強かったですが、さまざまなアップデートによりとても使いやすいサービスへと進化しています。今後のアップデートにも期待したいですね。

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