Serverless Framework による AWS Lambda 関数の管理2

Serverless Framework による AWS Lambda 関数の管理2

今回のトピック

前回の投稿で、Serverless Framework (以下、Serverless)を使って AWS Lambda 関数、およびそれのトリガーとなるイベントの定義まで出来たと思います。

今回は、前回紹介できなかった以下の細かいトピックについて記載していきます。

  • serverless.yml での変数の扱い
  • 外部ファイル
  • stage について
  • serverless-external-s3-event プラグイン
  • その他、tips 等

serverless.yml での変数の扱い

serverless.yml ファイルでは、変数をうまく使うことで、簡潔な記述、柔軟な運用などが可能になります。

詳しくはドキュメントを参照していただくとして、ここではドキュメントでは分かりづらい点、記載がない点について説明していきます。

変数の種類

serverless.yml の中で使える変数にはいくつかの種類があります。それぞれ見ていきます。

serverless.yml 自身で定義されているもの

serverless.yml 内で何度も使うような値は、変数として定義することにより、その後の修正が容易になります。具体的にはファイル内の custom キーの配下に値を定義します。

custom:
  bucket:
    module1: mycompany-module1
    module2: mycompany-module2

定義した値を使うには、self キーワードを使い、以下のようにします。

${self:custom.bucket.module1}

環境変数

sls (or serverless) コマンドを実行しているシェルの環境変数を、以下の通り参照することが出来ます。

${env:SOME_ENV}

コマンドライン引数

sls コマンドに対するコマンドライン引数を参照することも可能です。主な用途としては、後述するステージの切り替え(本番、開発、ステージング等)でしょうか。

渡すことの出来る引数は、以下のページに記載があります(AWS の場合)。

Serverless Framework Commands – AWS Lambda – Deploy

参照方法は、例えば stage 引数であれば以下の通りです。

${opt:stage}

変数が未定義の場合のデフォルト値

変数が定義されていない場合、デフォルト値を使用したい場合も多いと思います。その場合は、以下のようにします(ドキュメントより引用・改変)。

custom:
  myRegion: ${opt:region, 'us-west-1'}

外部ファイル

外部ファイルの読み込み方

serverless.yml からは、外部の YAML ファイルなどを読み込むことが出来ます。serverless.yml 内部の複数箇所で同じような内容を定義したい場合は、その定義内容を外部の YAML ファイルにしておき、それを serverless.yml の複数箇所で参照することが出来ます。

以下のような外部 YAML ファイルを考えます。

foo:
  - foo1
  - foo2
bar: something

参照方法としては、いくつかのパターンがありますので、以下に記載します。

# 外部ファイルを全て参照する場合
${file(./external_file.yml)}
# 外部ファイルの一部を参照する場合
${file(./external_file.yml):foo}

外部ファイルと変数

外部ファイルでも、通常の serverless.yml と同じように変数を参照することが出来ます。ただし、読み込み元のファイルで定義された変数を参照することは出来ないようです。

以下のようなファイルがあったとします。

# serverless.yml
custom:
  foo_in_parent: something
functions:
  handler: handler.foo
  name: foo_func
  events: ${file(./events.yml)}

そこで読み込まれる events.yml では、serverless.yml で定義した値は読み込めないようです。

# events.yml
- cloudwatchEvent:
    event:
      source: ${self:custom.foo_in_parent} # ダメ

ステージ

Serverless では、本番、ステージングなど、異なる環境を使い分ける事が出来ます。が、そのためには事前にある程度設計を行っておく必要があります。

ドキュメント推奨の方法

公式ドキュメント では、以下のように記載してあります。

  • At the very least, use a dev and production stage.
  • Use different AWS accounts for stages.
  • In larger teams, each member should use a separate AWS account and their own stage for development.

1番に関しては異論がないのですが、2番は場合によっては問題かもしれません。

推奨の方法では、dev と production で違う AWS アカウントを用意して、それぞれのアカウント内部で同じ名前の Lambda 関数などを定義するのですが、小さいプロジェクトとかだと、dev と production で異なるアカウントを使い分けるという事をしていない場合も多いと思います。その場合の方法は後述します。

さて、アカウントを使い分ける場合は、それほど難しい事は無いと思います。ステージ名と同じプロファイルを用意しておけば、以下のようにすれば良いですし、

provider:
  stage: ${opt:stage, 'dev'}
  profile: ${self:provider.stage}

ステージ名とプロファイル名が異なる場合は、以下のようにします。

custom:
  profiles:
    dev: awsProfileForDev
    production: awsProfileForProduction
provider:
  stage: ${opt:stage, 'dev'}
  profile: ${self:custom.profiles.${self:provider.stage}}

1つのアカウントを使う場合

非推奨のようですが、1つの AWS アカウントを本番と開発の両方で使う場合は、少し工夫が必要です。

# serverless.yml
# 開発と本番で毎に使い分ける必要があるリソースは、custom に定義しておく
custom:
  bucket:
    dev: example-bucket-dev
    pro: example-bucket

provider:
  # Serverless で作成される CloudFormation スタックなどには stage 名が含まれるので、
  # 開発でも本番でもないと分かるような適当な名前(default や common など)を設定しておく。
  stage: 'default'

functions:
  # 関数名に dev_ プリフィックスをつける。
  # 他に方法が無いか、については後述する。
  dev_foo_func:
    handler: handler.dev_foo_func
    name: dev_foo_func
    events:
      # existingS3 に関しては後述
      - existingS3:
          bucket: ${self:custom.bucket.dev}
          # 開発、本番で共通の部分は外部ファイルで共通化しておく。
          # s3_event_foo.yml の内容は省略します。
          events: ${file(./yaml/s3_event_foo.yml):events}
          rules: ${file(./yaml/s3_event_foo.yml):rules}
  pro_foo_func:
    handler: handler.pro_foo_func
    name: dev_foo_func
    events:
      - existingS3:
          bucket: ${self:custom.bucket.pro}
          events: ${file(./yaml/s3_event_foo.yml):events}
          rules: ${file(./yaml/s3_event_foo.yml):rules}

handler.py では、以下のようにします。他の言語の場合は、適宜読み替えてください。

def dev_foo_func(event, context):
  foo_func('dev', event, context)

def pro_foo_func(event, context):
  foo_func('pro', event, context)

def foo_func(stage, event, context):
  # いろいろな処理

関数名に dev_, pro_ のようなプリフィックスをつけて複数定義するのはイマイチなので、以下のように出来ないか、という意見もあると思います。

# うまくいかない
functions:
  # 関数は1つだけ定義
  foo_func:
    # 実体は stage オプションをつけて実行する事で、複数定義
    handler: handler.${self:provider.stage}_foo_func
    name: ${self:provider.stage}_foo_func
    events:
      # 省略

結論から言うと、この方法ではうまくいきませんでした。関数定義の name (この場合は dev_foo_func あるいは pro_foo_func )は、関数定義のエントリーのキー(ここでは foo_func)と同一でなある事が前提のプラグインがいくつか存在するからです(この後説明する serverless-external-s3-event もその1つ)

serverless-external-s3-event プラグイン

既存の S3 バケットを使用できるようにする

現在の Serverless の仕様だと、events に S3 を指定すると、指定された名前のバケットが作成され、既に同名のバケットが存在する場合はエラーになります。(詳細は以下のドキュメントを参照。)

Serverless Framework – AWS Lambda Events – S3

インフラが全て自分(や自部署)の手の届く範囲にあるのでしたら問題ないかもしれませんが、実際問題として、既存の S3 バケットやそれを使っているシステムがあり、それに対して新規で Lambda 関数のイベントを設定するというのもよくあるユースケースかと思います。そうしたときに、現在の Serverless では対応出来ません。実際に、以下のような issue が立っています。

1つめの issue のコメントで紹介されていますが、この問題に対応するためのプラグインが serverless-external-s3-event です。

インストール、設定

インストールは npm を使います。以下の(ドキュメントに書かれている)通り -g はつけずに、package.json をコミットした方が良いと思います。

npm install serverless-external-s3-event

serverless.yml の設定は以下のように行います。

    events:
      - existingS3:
          bucket: a-bucket-that-already-exists
          events:
            - s3:ObjectCreated:*
          rules:
             - prefix: path/to/some/dir/
             - suffix: .csv

詳細はドキュメントを参照してください。

まとめ

今回は、前回の投稿で紹介できなかった機能などについて説明しました。

長くなったのでこの辺で終わりますが、他にも何か tips などを思い出したら、別エントリーとしてまとめようと思います。