SwaggerをAWS Serverless Application Modelから扱う方法を理解する

はじめに

こんにちは、中山です。

最近SwaggerをAWS Serverless Application Model(以下AWS SAM)から利用する機会がありました。AWS SAMではAWS::Serverless::ApiまたはAWS::ApiGateway::RestApiリソースを利用することにより、Swagger形式で記述されたファイルを扱うことが可能です。基本的にAWS SAMのみで利用可能な AWS::Serverless::Api リソースを利用することが多いはずです。API Gatewayは複数のリソースに分離されているため、AWS SAMのベースとなっているCloudFromation用のリソースを使うと、テンプレートの行数が長くなってしまうからです。

執筆時点(2017/03/08)では AWS::Serverless::Api リソースはSwaggerファイルを以下2つの方法で定義できます。

  • Swaggerを外部ファイルに記述する
  • Swaggerをインラインで記述する

それぞれメリット/デメリットがあります。本エントリでそれぞれの違いをご紹介したいと思います。

なお、本エントリを執筆する上で検証に利用した主要な各種ツールのバージョンは以下の通りです。バージョンによって結果が変更される可能性があるので、その点ご了承ください。

  • AWS SAM: 2016-10-31
  • AWS CLI: aws-cli/1.11.58 Python/2.7.12 Darwin/16.4.0 botocore/1.5.21

Swaggerを外部ファイルに記述する場合

AWS SAMのテンプレートと分けた形でSwaggerを管理したい場合、 AWS::Serverless::Api リソースの DefinitionUri プロパティを利用します。このプロパティの値にS3へアップロードされたSwaggerファイルへのパスを記述することで、その内容をもとにAPI Gatewayの設定を行うことが可能です。指定方法は以下の2つがあります。

  • DefinitionUri にS3またはローカルファイルへのパスを指定する
  • S3 Location Object形式でSwaggerファイルが設置されたバケット名とキーを指定する

1点目について。 DefinitionUri にSwaggerファイルが設置されているS3へのパスを指定する方式です。例えば、以下のように記述できます。

  Api:
    Type: AWS::Serverless::Api
    Properties:
      DefinitionUri: s3://<_YOUR_S3_BUCKET_>/swagger.yml

ただし、S3へのパスを直接記述してしまうとSwaggerの更新毎にファイルをアップロードしなければならず、少し面倒です。この問題に対して、 aws cloudformation package コマンドを利用することにより、ローカルファイルへのパスを指定可能です。指定されたファイルはこのコマンドによってS3へ自動的にアップロードされます。例えば以下のように記述できます。

  Api:
    Type: AWS::Serverless::Api
    Properties:
      DefinitionUri: src/api/swagger.yml

ただし、この形式でSwaggerを指定する場合、CloudFromationの組み込み関数は現状利用できないという点は注意してください。例えば、Swaggerを開発/本番環境で分けたいと考えた時( swagger.dev.yml / swagger.prod.yml など)、以下にように記述することはできません。 Env はパラメータで渡された変数です。

  Api:
    Type: AWS::Serverless::Api
    Properties:
      DefinitionUri: !Sub src/api/swagger.${Env}.yml

以下のようなエラーが出力されてしまいます。

Failed to create the changeset: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state Status: FAILED. Reason: Transform AWS::Serverless-2016-10-31 failed with: Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Api] is invalid. 'DefinitionUri' requires Bucket and Key properties to be specified

続いて2点目について。 DefinitionUri はS3 Location Object形式での記述方法にも対応しています。例えば以下のように指定可能です。

  Api:
    Type: AWS::Serverless::Api
    Properties:
      DefinitionUri:
        Bucket: !Ref S3Bucket
        Key: swagger.yml

上記のようにこの形式の場合、組み込み関数が最近サポートされました。このアップデートにより、より柔軟な形でテンプレートを記述できるようになったのは嬉しいポイントです。ただし、1つ目の方法のように aws cloudformation package コマンドでローカルファイルをS3にアップロードする機能はありません。スタックの作成/更新前に自分でアップロードしておく必要があります。

Swaggerを外部ファイルに記述する場合の変数について

この形式でSwaggerを扱う場合、注意する点があります。それはSwaggerファイル内で扱える変数に一部制限があるという点です。Swagger内で変数を扱うには、 AWS::Serverless::Api リソースの Variables プロパティにステージ変数を設定します。例えば、AWS SAMで定義したLambda関数名をSwagger内で変数として扱いたい場合、以下のように記述できます。

  Api:
    Type: AWS::Serverless::Api
    Properties:
      Variables:
        FuncName: !Ref Func1
<snip>
  Func1:
      Type: AWS::Serverless::Function

Ref組み込み関数AWS::Serverless::Functionリソースを参照することにより、関数名を FuncName 変数に代入しています。Swaggerファイルからは ${stageVariables.FuncName} という書式で変数を参照可能です。

一見するとステージ変数を使えば何でも変数に代入できるように思えますが、現時点ではSwagger内で頻繁に利用されるであろうAWSリージョン及びAWSアカウントIDは参照できません。こちらのイシューでも言及されています。例えば、API GatewayのバックエンドにLambda関数を指定する場合、 x-amazon-apigateway-integration オブジェクトなどの uri プロパティで、以下のような形式でLambda関数を指定する必要があります。

      x-amazon-apigateway-integration:
        responses:
          default:
            statusCode: 200
        uri: arn:aws:apigateway:<_YOUR_AWS_REGION_>:lambda:path/2015-03-31/functions/arn:aws:lambda:<_YOUR_AWS_REGION_>:<_YOUR_AWS_ACCOUNT_ID_>:function:<_YOUR_LAMBDA_FUNCTION_NAME_>/invocations
        passthroughBehavior: when_no_templates
        httpMethod: POST
        type: aws

ハイライトしている箇所に注目してください。バックエンドのLambda関数を指定する際に、AWSリージョン及びAWSアカウントIDを指定しなければならないことが分かります。実際どういったエラーになるのか確認するため、例えば以下のように無理やりステージ変数にこれらの値を代入させたとします。

  Api:
    Type: AWS::Serverless::Api
    Properties:
      DefinitionUri: src/api/swagger.yml
      StageName: !Ref Env
      Variables:
        FuncName: !Ref Func1
        AWSRegion: !Ref AWS::Region
        AWSAccountId: !Ref AWS::AccountId

API Gatewayの内容を確認するとステージ変数自体には期待した値が代入されています。

$ aws apigateway get-stage \
  --stage-name dev \
  --rest-api-id "$(aws apigateway get-rest-apis \
    --query 'items[?contains(name, `swagger-external`)].id' \
    --output text)"
{
    "stageName": "dev",
    "variables": {
        "AWSAccountId": "************",
        "AWSRegion": "ap-northeast-1",
        "FuncName": "swagger-external-dev-Func1-OENQS4JL6URJ"
    },
    "cacheClusterEnabled": false,
    "cacheClusterStatus": "NOT_AVAILABLE",
    "deploymentId": "ejx35f",
    "lastUpdatedDate": 1488952223,
    "createdDate": 1488952222,
    "methodSettings": {}
}

この状態で uri プロパティを以下のように変更しても期待した動作はしません。

      x-amazon-apigateway-integration:
        responses:
          default:
            statusCode: 200
        uri: arn:aws:apigateway:${stageVariables.AWSRegion}:lambda:path/2015-03-31/functions/arn:aws:lambda:${stageVariables.AWSRegion}:${stageVariables.AWSAccountId}:function:${stageVariables.FuncName}/invocations
        passthroughBehavior: when_no_templates
        httpMethod: POST
        type: aws

以下のようなエラーが出力されます。

Errors found during import: Unable to put integration on 'GET' for resource at path '/': Invalid lambda function

ちなみに、AWSリージョンとAWSアカウントIDが含まれるLambda関数のARNを渡したとしても同様のエラーが出ます。

Swaggerをインラインで記述する場合

2017年3月31追記
AWS::Includeを利用することで、インラインで定義したSwaggerファイルとAWS SAMで定義したテンプレートを分離可能です。こちらのエントリにまとめました。

続いてAWS SAMのテンプレート内にインラインでSwaggerを記述する方法についてご紹介します。最近のアップデートで対応した機能です。 AWS::Serverless::Api リソースの DefinitionBody プロパティを利用することにより、例えば以下のような記述が可能です。

  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Env
      DefinitionBody:
        swagger: 2.0
        info:
          title: !Sub swagger-external-${Env}
          description: !Sub swagger-external-${Env}
          version: 1.0.0
        schemes:
          - https
        basePath: !Sub /${Env}
        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: !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

  Func1:
    Type: AWS::Serverless::Function

この方式の利点は何と言ってもAWS SAMの機能をそのまま利用できるという点です。つまり、組み込み関数が使えるのでより柔軟な形でテンプレートを記述できます。ただし、外部ファイルに記述する方式であればAWS SAM以外でも利用しているSwaggerファイルを基本的にそのまま再利用することはできますが、こちらの方式の場合AWS SAM以外で管理できなくなるという点は注意してください。AWS SAMの機能に依存している分ポータビリティは低くなります。

AWS SAMのネストスタックについて

インライン方式の場合、Swaggerの内容をAWS SAMテンプレートに記述する必要があるためどうしてもテンプレートの内容が長くなってしまいます。CloudFromationにはAWS::CloudFormation::Stackリソースでスタックをネストさせることができるので、大本となるAWS SAMとSwaggerを記述した2つのテンプレートに分ければこの問題を解決できそうです。が、「社会は甘くない」のでそう簡単にはいきません。

例えば、以下のようなディレクトリ構成にしたとします。

$ tree
.
├── sam.yml
└── src
    ├── handlers
    │   └── func1
    │       └── index.py
    └── templates
        └── swagger.yml

4 directories, 3 files

各種テンプレートは以下の内容です。

  • sam.yml
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: swagger-inline-2

Parameters:
  Env:
    Type: String
    Default: dev

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: General Configuration
        Parameters:
          - Env
    ParameterLabels:
      Env:
        default: Env

Resources:
  Api:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: src/templates/swagger.yml
      Parameters:
        Env: !Ref Env
        FuncArn: !GetAtt Func1.Arn

  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.Name
  • src/templates/swagger.yml
---
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: swagger

Parameters:
  Env:
    Type: String
    Default: dev
  FuncArn:
    Type: String

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: General Configuration
        Parameters:
          - Env
          - FuncArn
    ParameterLabels:
      Env:
        default: Env
      FuncArn:
        default: Function Arn

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Env
      DefinitionBody:
        swagger: 2.0
        info:
          title: !Sub swagger-internal-${Env}
          description: !Sub swagger-internal-${Env}
          version: 1.0.0
        schemes:
          - https
        basePath: !Sub /${Env}
        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: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${FuncArn}/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

Outputs:
  Name:
    Value: !Ref Api

両テンプレートともAWS SAMの機能を利用しているため、 AWSTemplateFormatVersion: 2010-09-09 という記述をし、テンプレートがCloudFromationではなくAWS SAMであることを明示的に指定する必要があります。一見よさそうですが、このテンプレート動きません。。。以下2つの問題があります。

  • ネストされたAWS SAMテンプレートは CreateStack APIに対応していない
  • Events プロパティの RestApiId で指定するAPI Gatewayのリソース名を別のスタックから直接参照できない

1点目について。こちらのイシューでも指摘されていますが、現時点ではネストされたテンプレートがAWS SAMの場合、 CreateStack APIは実行できません。以下のようなエラーが出力されてしまいます。

CreateStack cannot be used with templates containing Transforms.,

そのため、一度 CreateChangeSet してから ExecuteChangeSet する必要があります。幸い、AWS SAMのスタックを作成する際に利用することになる aws cloudformation deploy コマンドには --no-execute-changeset オプションというChangeSetのみ作成してくれる機能があります。これを使えば問題を解消できるかと思ったのですが、私が検証した限りだと ExecuteChangeSet を実施しても、結局内部的には CreateStack しているため、同じエラーが出力されてしまいました。。。

続いて2点目について。先程のテンプレートでは src/templates/swagger.yml のアウトプットで AWS::Serverless::Api リソースのリソース名を sam.yml から参照できるようにしていました。しかし、このテンプレートからスタックを作成しようとすると以下のようなエラーが表示されます。

Failed to create the changeset: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state Status: FAILED. Reason: Transform AWS::Serverless-2016-10-31 failed with: Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Func1] is invalid. Event with id [PostApi] is invalid. RestApiId property of Api event must reference a valid resource in the same template.

src/templates/swagger.yml を別テンプレートとして切り出してFn::ImportValue関数を利用したスタック間参照なら対応できるかと思ったのですが、それもダメそうです。ドキュメントに現時点では対応していないと書かれています。以下に引用します。

ImportValue allows one stack to refer to value of properties from another stack. ImportValue is supported on most properties, except the very few that SAM needs to parse. Following properties are not supported:

RestApiId of AWS::Serverless::Function

Policies of AWS::Serverless::Function

StageName of AWS::Serverless::Api

パラメータで渡す方式なら対応できそうですが、そうするとスタックを別々に作成しなければならず、スタックの管理が煩雑になると思います。そのため、インライン方式を採用する場合はテンプレートの行数が長くなることは覚悟しておいた方が良さそうです。

まとめ

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

AWS SAMからSwaggerを扱う方法をご紹介しました。いろいろ注意点などはありますが、Swaggerをそのまま利用できるというのは非常に便利だと思います。どういった形でSwaggerとAWS SAMを管理したいのか方針を決めてから、ご自身の環境にあう方法を見つけるとよいかと思います。

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