AWS SAMにおける環境管理のベストプラクティス

AWS SAMにおける環境構築のベストプラクティス

こんにちは、エンジニアインターンの佐藤です。
SAMを使えば、LambdaやAPI Gatewayの再現をローカルで行うことができ、そのデプロイもできます。しかし、API Gatewayにはステージがあったり、Lambdaにはエイリアスがあったりと、どれを活用するべきなのかまとまっている記事がありませんでした。
この記事では、その分かりにくい概念から説明し、SAMデプロイのベストプラクティスを紹介していきます。
またSAMについては、2つ記事を出しているので、是非見てみてください。

AWS SAM + DockerでAWSサーバーレス環境をローカル構築する
CloudFrontなしでS3をHTTPSカスタムドメインでホスティングしてみた

※SAMのバージョンは1.24.1を使用しています

  1. API Gatewayのステージ
  2. Lambdaのエイリアスとバージョニング 
  3. template.yamlでの記述方法
  4. 環境変数の渡し方
    1. template.yamlで環境変数を受け取る
    2. swagger.yamlで環境変数を受け取る
  5. ベストプラクティス考察
    1. 1つのAPI Gatewayで管理する場合
    2. 複数のAPI Gatewayで管理する場合
    3. 1つのLambdaで管理する場合
    4. 複数のLambdaで管理する場合
  6. ベストプラクティス
  7. Lambda Authorizerの環境分け
  8. まとめ

API Gatewayのステージ

API Gatewayにはステージと呼ばれるものが存在します。公式リファレンス では以下のように説明されています。

ステージは、デプロイに対する名前付きのリファレンスで、API のスナップショットです。
Stage (ステージ)を使用して、特定のデプロイを管理および最適化します。


ステージを使用すれば1つのAPI Gatewayで複数の状態を管理することが可能です。
API GatewayのエンドポイントURLにステージが含まれるためAPI Gatewayにおいて各ステージ同士で影響を及ぼさないようになっています。
つまり、この機能を使ってSAMのデプロイ先をステージごとに変更できれば1つのAPIだけで開発環境と本番環境を分けることも可能になります。

Lambdaのエイリアスとバージョニング 

Lambdaのエイリアスは公式リファレンス では以下のように説明されています。

Lambda 関数の 1 つ以上のエイリアスを作成できます。
Lambda のエイリアスは関数の特定のバージョンに対するポインタのようなものです。


そしてLambdaのバージョニングについては公式 で以下のように説明されています。

バージョンを使用して、関数のデプロイを管理できます。
たとえば、安定した本番稼働用のバージョンのユーザーに影響を与えることなく、ベータテスト用の関数の新しいバージョンを公開できます。
Lambda によって、関数を公開するたびに関数の新しいバージョンが作成されます。
新しいバージョンは、関数の未公開バージョンのコピーです。


例えばLambdaで最新のデプロイを行うと$LATESTというバージョンが付与されます。この$LATESTをdevというエイリアスで紐付けます。
エイリアスがつけられたLambdaは以下のようにエイリアスが紐づけられます。

lambda-function:dev


あとはAPI Gatewayの設定でdevステージで呼び出すLambda関数にlambda-function:devを指定すれば最新版のLambdaが起動するような仕組みにできるということです。
これらの機能を使えば、1つのLambdaでlambda-function:devとlambda-function:prdに分けて環境分けを行うことができます。

template.yamlでの記述方法

SAMをデプロイする際はLambdaやAPI Gatewayの情報を記述したtemplate.yamlが必須です。templateの特定のプロパティに値を指定することでエイリアスの名前やステージ名を指定できます。templateでの書き方は以下の通りです。

ステージ

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      Name: <ApiName>
      StageName: <StageName>

エイリアス

Resources:
  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: <lambda-function-name>
      AutoPublishAlias: <alias-name>


また、SAMはエイリアス付きのLambdaをデプロイするとそのエイリアスに紐づくバージョンは$LATESTと紐づくようになっています。
そのため、Lambdaのバージョニングで$LATEST以外のバージョンを作成するにはAWSのインタフェースから指定する必要があります。

環境変数の渡し方

次に環境変数の渡し方を説明します。環境変数を渡せれば関数名やAPI名を変数ごとに分けてデプロイが可能になります。
まず環境変数はtemplate.yamlで以下のように定義する必要があります。Defaultはオプションです。

Parameters:
  Stage:
    Type: String
    Default: dev


そしてSAMで注意が必要なのは、ローカルとデプロイで環境変数を渡す方法が少し異なることです。
ローカルで渡す場合は以下の3種類の方法があります。

  • ・-n または --env-vars オプションでjsonファイルのパスを指定する

    ・--parameter-overridesで上書きする

    ・samconfig.toml


1つ目の場合は以下のような環境変数のjsonを用意します。

{
  "Parameters": {
    "Stage": "dev"
  }
}


パラメータにはjsonのパスを指定すれば渡すことができます。

sam local start-api --env-vars <jsonのパス>


2つ目の方法では以下のように、<変数名>=<値>の形で渡します。

sam local start-api --parameter-overrides Stage=dev


3つ目の方法は前回の記事 でも紹介した方法です。samconfig.tomlにパラメータを記述して--config-envオプションを指定する方法です。
samconfig.tomlは以下のように記述します。ハイフンは使わず、全てアンダーバーなので注意してください。

[dev.local_start_api.parameters]
parameter_overrides = "EnvName=dev"


オプションにはsamconfig.tomlで定義された環境名を指定します。今回であればdevです。

sam local start-api --config-env dev


また、samconfig.tomlにはparamter-overridesオプション以外に、コマンドで指定できるオプションは記述して渡すことができます。

ここで注意が必要な点としては、local start-apiコマンドで、template.yamlのパスを指定する-tオプションを使用するとsamcofing.tomlのパラメータ情報を渡すことができないことです。しかしこのオプションを指定しないとSAMはプロジェクトのルートディレクトリにあるtemplate.yamlを自動で探しにいきます。万が一template.yamlが違うディレクトリや異なる名前で使用していた場合は、一度sam buildしてビルドすることで解決することができます。

しかし、この方法を用いるとホットリロードが効かなくなります。変更を行ってもビルドしないと変更は反映されませんでした。


一方、デプロイで渡す際は-nまたは--env-varsオプションは使用することができません。そのため、ローカルの環境変数はtemplate.yamlのDefaultに記述し、デプロイの環境変数はsamconfig.tomlに記述するのがおすすめです。

template.yamlで環境変数を受け取る

コマンドで渡されたパラメータによってAPIの名前やLambdaの名前を変えることができます。SAMのtemplate.yamlはCloudFormationの拡張であるためCloudFormationの機能を利用することができます。環境変数を扱うときに利用するのは主に以下の2つです。

  • ・Ref

    ・Sub


どちらも使い方は簡単です。
Refは指定したパラメータまたはリソースの値を返します。例えばLambdaにどのAPIを使用するのかパラメータを指定したければ以下のように使用します。

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      Name: ApiGateway-Api
      StageName: dev

  Function:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: xxx
      FunctionName: <function-name>
      Role: <lambda-role>
      Events:
        Get:
          Type: Api
          Properties:
            Path: /get/
            Method: get
            RestApiId: !Ref Api # Apiのリソース名を参照


次にSubです。Subは文字列の変数としてパラメータを渡すことができます。これを使うことで文字列の中にパラメータやリソース名を埋め込むことができます。例えば環境変数によってLambdaの関数名を変更したい場合は以下のように使用します。

Parameters:
  StageName:
    Type: String
    Default: dev
Resources:
  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: xxx
      FunctionName: !Sub get-function-${StageName}


この手法を組み合わせることで1つのtemplateファイルで複数の環境名を指定することも可能になります。

swagger.yamlで環境変数を受け取る

swagger.yamlでも同様にRefとSubを使用することができます。実は!Ref・!Subは省略系で、正式にはFn::RefとFn::Subが正式な書き方です。swagger.yamlでは省略系には対応していないので注意してください。

また、swagger.yamlでは環境変数だけではなく、template.yamlで定義したFunctionのArnを取得したい場合もあります。その場合にはFn::GetAttを使用します。この関数を使用することでtemplate.yamlのLambdaやAPIのArnを取得できます。

文字列にその情報を埋め込みたい場合には以下のようにFn::Join関数を使い、結合することで可能です。

  /get:
    get:
      responses: {}
      x-amazon-apigateway-integration:
        type: "aws_proxy"
        httpMethod: "GET"
        uri:
          'Fn::Join':
            - ''
            - - 'arn:aws:apigateway:'
              - 'ap-northeast-1:lambda:path/2015-03-31/functions/'
              - 'Fn::GetAtt':
                  - GetFunction
                  - Arn
              - '/invocations'
        passthroughBehavior: "when_no_match"

ベストプラクティス考察

まとめるとデプロイには以下の方法が考えられます。

API Gateway

  • ・環境ごとにAPIを分けない場合(ステージを活用)

    ・環境ごとに別のAPIを使う


Lambda

  • ・Lambdaを環境ごとに分けない(エイリアスを活用)

    ・Lambdaを環境ごとに分ける


そして、デプロイの際にはtemplate.yamlやswagger.yamlを使用するので、ファイルの管理も重要な点です。管理するならば1つのファイルでできることが理想です。これについては先ほど説明した環境変数の渡し方の部分を使用すれば1つのtemplateで管理することが可能でした。以下の考察では前提としてtemplateとswaggerを1つずつで実現することを目的にして説明していきます。

1つのAPI Gatewayで管理する場合

1つのAPI Gatewayで管理する場合はステージを活用します。
しかし、早速ですがこの方法には問題があります。

SAMでは1つのAPI Gatewayに対して1つのステージしかデプロイに対応していません。例えば同じAPIで、はじめに「dev」ステージをデプロイします。その後ステージ名のみを変更した「prd」ステージのAPIをデプロイするとステージdevが上書きされてprdに変わってしまいます。
そのためSAMで1つのAPIの複数ステージを管理することはできません。

複数のAPI Gatewayで管理する場合

複数のAPIで管理する場合は特に問題ありません。やり方としては以下のようにtemplateに渡す環境変数に合わせてAPIの名前を変更するだけです。ステージはデプロイする環境に合わせて変更していますが、固定しても大丈夫です。

Parameters:
  EnvName:
    Type: String
    Default: dev
Resources:
  OwletApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Sub ${EnvName}-api
      StageName: !Ref EnvName


それでは次にLambdaについて考えてみます。
以下では前提として複数のAPIで環境分けをするものとします。

1つのLambdaで管理する場合

1つのLambdaで管理する場合はエイリアスを使用します。ですがこの方法にも問題があります。

その問題とはすでに同一のLambdaが作成されているために発生するエラーです。

AutoPublishAliasをはじめに「dev」としてデプロイした後に「prd」に変更して改めてデプロイすると以下のエラーが出ます。

<lambda-function-name> already exists in stack arn:aws:cloudfromation:ap-northeast-1:xxxx:stack/<api-name>/yyyy


これはすでにdevデプロイのときに作られたLambdaと同じ名前のLambdaをprdがデプロイしてしまうからです。SAMはCloudFormationでデプロイを行なっており、APIとLambdaが紐づき、1つのスタックとしてデプロイされます。そのためこの問題はSAMはスタック外のLambdaを上書きすることはできないため起きる現象です。

もしCloudFormationの別のスタックで作成されたLambdaや、インタフェース上で作成された既存のLambdaに接続したい場合は、2つtemplateファイルが必要です。
片方にはLambdaを記述して、もう片方にはLambdaを記述しないようにします。そしてswaggerでは1つのLambdaを指定すれば可能です。
しかしこれでは1つのtemplateで管理する大筋から外れてしまうため推奨はできません。

そのためエイリアスを使ったデプロイは1つのtemplateでは行えません。

複数のLambdaで管理する場合

環境変数でLambdaの関数名を指定して分ける方法です。先ほども少し触れましたが、以下のようにtemplateに渡した環境変数でデプロイする関数名を変更します。

FunctionName: !Sub get-function-${StageName}


この方法を使えば、環境ごとで関数名が異なるためSAMが既存のLambdaを上書きするエラーを回避することができます。

ベストプラクティス

以上より、1つのtemplate.yamlでデプロイする場合は、環境ごとに複数のAPIを作成し、環境ごとに複数のLambdaを作成することがベストプラクティスであることがわかりました。

この考察では1つのtemplateで実現することを目標としましたが、2つのtemplateが必須の場合も存在します。それがLambda Authorizerの環境分けをする場合です。

Lambda Authorizerの環境分け

swaggerでLambda Authorizerを指定する場合は以下のような記述となっています。

  /create:
    post:
      responses: {}
      security:
      - LambdaAuthorizer: []


そしてtemplate.yamlではこのように記載されます。

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      Name: <api-name>
      StageName: <stage-name>
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: swagger.yaml
      Auth:
        Authorizers:
          LambdaAuthorizer: 
            FunctionArn: !GetAtt AuthorizerFunction.Arn
            FunctionPayloadType: TOKEN
            
  AuthorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: xxx
      FunctionName: <function-name>
      Role: <lambda-role>
      Events:
        Login:
          Type: Api
          Properties:
            Path: /validate
            Method: post
            RestApiId: !Ref Api

Lambda Authorizerを適用させるためにはswaggerとtemplateのどちらにも記述する必要があります。
swaggerの部分を見ればわかるのですが、参照してる値はリソース名ではなくtemplate.yamlのキーの部分のためswagger.yamlに渡すことができません。
そのため環境ごとにLambda Authorizerの有無を使い分ける場合は2つのtemplate.yamlとswagger.yamlが必要になるので注意が必要です。

まとめ

今回はSAMの環境を意識したデプロイ方法について紹介しました。
残念ながらSAMはAWSが用意したステージやエイリアスの機能を十分に使えないということがわかりましたが、工夫することで管理するファイルの数を減らすことができました。
まだまだ改善の余地があるSAMですが、この記事が参考になれば幸いです!


※ samconfig.tomlを用いたローカルでの環境変数の渡し方で問題点を発見したため本文を修正しました (2021-09-30)

Techブログ 新着記事一覧