SAM CLI で AWS SAM と Swagger を用いた API 開発環境を構築する

追記 (2018-10-02)

!FindInMapAWS::Include とともに用いると aws cloudformation deploy で失敗することが分かったため、 SwaggerPath をパラメータとして受け取る形に修正しました。


AWS SAM と Swagger を用いてサーバーレスで API を実装する際の開発環境を SAM CLI で構築しました。

github.com

AWS SAM と SAM CLI について

AWS SAM(AWS サーバーレスアプリケーションモデル)は API Gateway、Lambda、DynamoDB を管理することが出来る CloudFormation の拡張です。
また SAM CLI (aws-sam-cli) は AWS SAM のテンプレートをもとに、ローカルでの開発やデプロイする環境を提供してくれるツールです。

docs.aws.amazon.com

docs.aws.amazon.com

AWS SAM と Swagger

AWS SAM では API の定義に Swagger 形式を用いることが可能です。

dev.classmethod.jp

上記の記事にある通り Swagger を用いる方法として、以下の3種類の方法があります。

  • 外部ファイルに記述
  • AWS SAM のテンプレートファイルにインラインで記述
  • AWS SAM のテンプレートファイルから AWS:Include で読み込み

今回は、AWS SAM のテンプレートファイルから AWS:Include で swagger.yml を読み込む方法のアプリケーションの開発環境を SAM CLI で構築します。

ローカル開発環境の構築

まずは SAM CLI をインストールします。 SAM CLIPython 製のため pip を使います。

$ pip install aws-sam-cli

github.com

続いて、サンプルリポジトリgit clone します。

$ git clone https://github.com/yami-beta/aws-sam-swagger-sample.git

template.ymlAWS SAM のテンプレートで swagger.yml が Swagger Spec になります。 API を起動する場合は以下のコマンドを実行します。

$ sam local start-api

開発環境構築のポイント

開発環境を構築する上でのポイントが以下の部分です。
DefinitionBody での swagger.yml の指定に SwaggerPath というパラメータを用いています。 開発環境( Stage パラメータが dev )の場合はローカルのパスを、ステージング環境( Stage パラメータが stg )の場合は S3 バケットとなるように SwaggerPath を指定します。

Parameters:
  SwaggerPath:
    Type: String
    Default: swagger.yml

Resources:
  HelloApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Stage
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: !Ref SwaggerPath

以下の Mappings の例はデプロイできない古い情報です。


DefinitionBody で swagger.yml を指定しているのですが、開発環境( Stage パラメータが dev )の場合はローカルのパスを、ステージング環境( Stage パラメータが stg )の場合は S3 バケットとなるように、Mappings を用いています。

Mappings:
  StageConf:
    dev:
      ArtifactBucket: swagger.yml
    stg:
      ArtifactBucket: !Sub s3://${ArtifactBucket}/swagger.yml

Resources:
  HelloApi:
    Type: AWS::serverless::Api
    Properties:
      StageName: !Ref Stage
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: !FindInMap [StageConf, !Ref Stage, "ArtifactBucket"]

AWS:Include はローカルのファイルを指定することが現時点では出来ません。

Allow `aws cloudformation package` to work with local paths in `AWS::Include` transforms · Issue #2513 · aws/aws-cli · GitHub

しかし SAM CLI ではローカルのパスを指定すると読み込んでくれます。
これを利用して、開発環境ではローカルの swagger.yml を用いて sam local start-api を実行し、デプロイ時には S3 バケット名とすることで、開発環境と実際に AWS にデプロイする場合で共通のテンプレートを利用できます。

def _download_swagger(self, location):
    """
    Download the file from given local or remote location and return it
    Parameters
    ----------
    location : str or dict
        Local path or S3 path to Swagger file to download. Consult the ``__init__.py`` documentation for specifics
        on structure of this property.
    Returns
    -------
    dict or None
        Downloaded and parsed Swagger document. None, if unable to download
    """

    if not location:
        return

    bucket, key, version = self._parse_s3_location(location)
    if bucket and key:
        LOG.debug("Downloading Swagger document from Bucket=%s, Key=%s, Version=%s", bucket, key, version)
        swagger_str = self._download_from_s3(bucket, key, version)
        return yaml_parse(swagger_str)

    if not isinstance(location, string_types):
        # This is not a string and not a S3 Location dictionary. Probably something invalid
        LOG.debug("Unable to download Swagger file. Invalid location: %s", location)
        return

    # ``location`` is a string and not a S3 path. It is probably a local path. Let's resolve relative path if any
    filepath = location
    if self.working_dir:
        # Resolve relative paths, if any, with respect to working directory
        filepath = os.path.join(self.working_dir, location)

    if not os.path.exists(filepath):
        LOG.debug("Unable to download Swagger file. File not found at location %s", filepath)
        return

    LOG.debug("Reading Swagger document from local file at %s", filepath)
    with open(filepath, "r") as fp:
        return yaml_parse(fp.read())

aws-sam-cli/reader.py at 6164d6d2e7351a849ad3d79973ac18b8d3d1d371 · awslabs/aws-sam-cli · GitHub

まとめ

SAM CLI を用いて AWS SAM と Swagger で構成された API の開発環境を構築しました。
現在は AWS:Include がローカルパスを指定できないため、Mappings パラメータ でローカルパスと S3 バケットを切り替えることで、テンプレートを共通化することができます。
AWS:Include がローカルパスを指定できるようになると、更にシンプルなテンプレートに出来るため、今後に期待したいところです。