AWS Lambdaのデプロイフロー管理

2016年版書きました → http://qiita.com/kinzal/items/7a09659399d9be40d674

---

この投稿は [今年もやるよ！AWS Lambda縛り Advent Calendar 2015 - Qiita](http://qiita.com/advent-calendar/2015/lambda#day-3) の 9日目の記事です。

皆さんAWS Lambdaを使っていますか？
SNS対応や、VPC対応、スケジューラの対応でLambdaの適用できる範囲が増えたり、API Gatewayと組み合わせることでサーバーレスアーキテクチャを実現したりと、今年はLambdaがどんどん出来る子になっていく1年でした。
私自身もLambdaを趣味だけではなく、仕事で使う機会も増えてきて実用性が増してきたなという印象があります。

今日はそんなLambdaのデプロイフローの話をします。

# Lambdaのデプロイフロー構築の難しさ

かれこれ1年近くLambdaを行ってわかったことの一つが、デプロイフローの構築の難しさです。
Lambdaのデプロイの何が難しいというと

* コードのビルド
    - 開発・プロダクション環境の設定の切り替え
    - 依存ライブラリの解決とコードへの混入
    - ネイティブライブラリの生成とコードへの混入
* IAM User（or IAM Role）の準備
    - S3にコードをアップロードするためのIAM User
    - 依存するAWSリソースを作成するためのIAM User
    - Lambda Functionを作成するためのIAM User
    - Lambda Functionを実行するためのIAM Role
* 依存するAWSリソースの作成
* Lambda Functionの作成
* イベントソースの設定

と、意外にもデプロイ時にやることが多く、さらに自動化するために何のツールを使うのか、CI・CD環境の権限管理（特にIAM Userを作るような強い権限の付与）をどうするのかなど考えるべきポイントの多さがデプロイフローの構築の難しさに繋がっています。
（設定ファイルの切り替えとかLambdaの標準機能にないことをやろうとしているせいという説もあります）

とはいえ、ベストプラクティスとまでは言わなくとも、ある程度の解決策自体は見えてきました。

# Lambda Functionのデプロイフロー

というわけで最近自分の中でブームのデプロイフローです。

![構成](https://qiita-image-store.s3.amazonaws.com/0/5522/8e14dcf8-f0cf-8ea1-2551-b63ecd1c9264.png)


大きく分けて3種類のリポジトリを使ってデプロイフローを作成しています。

1. [lambda-deploy-custom-resource-function](https://github.com/k-kinzal/lambda-deploy-custom-resource-function)のリポジトリ
2. Lambda Functionのリポジトリ
3. CloudFormation（もしくはterraformとかでも可）のリポジトリ

## 1. lambda-deploy-custom-resource-functionのリポジトリ

![lambda-deploy-custom-resource-functionのリポジトリ](https://qiita-image-store.s3.amazonaws.com/0/5522/75dc2d32-6199-bbcc-4cfa-cb728ff71dc4.png)

詳しくは少し前に書いた記事を参考にしてください。

* [汎用のLambda FunctionをデプロイするLambda Functionを作った](http://qiita.com/kinzal/items/421167dbeb6fbe884121)

生成したファイルは`s3://file-repositories/lambda/lambda-deploy-custom-resource-function/0.0.3.zip`に配置してあるので、これをCloudFormationで読み込んでLambda Functionのデプロイを行います。

## 2. Lambda Functionのリポジトリ

![Lambda Functionのリポジトリ](https://qiita-image-store.s3.amazonaws.com/0/5522/f2938c7f-b02e-7ecb-1e2c-9f0236b64d2b.png)

実際に扱うLambda Functionをこのリポジトリで管理します。
デプロイフローを作る上でいくつかポイントになる箇所があります。

### node-config

設定の切り替えに[node-config](https://github.com/lorenwest/node-config)を使用します。
node-configは`NODE_ENV`という環境変数で設定ファイルの切り替えを行いますが、Lambda Functionの実行環境では特に設定がないので、使えるのは

* default.json
* local.json

の2つだけになります。
基本的に`default.json`に開発用の設定を記載し、`local.json`はデプロイ時に動的に生成しプロダクション環境用の設定と位置付けて使用しています。
（プロダクション用の設定は開発者に見せたくないことも多いので動的に生成するのを推奨します）

### archiveタスク

gulpを使ってコードと設定をアーカイブします。

```js:gulpfile.js
var gulp = require('gulp');
var path = require('path');
var zip  = require('gulp-zip');
var packageJson = require('./package.json');

gulp.task('archive', function () {
  var nodeModulePaths = Object.keys(packageJson['dependencies']).map(function(name) {
    return 'node_modules/' + name + '/**';
  });

  return gulp.src(['src/**', 'config/**.json'].concat(nodeModulePaths), {base: "."})
             .pipe(zip('archive.zip'))
             .pipe(gulp.dest('dist'));
});
```

このときの`package.json`の`dependencies`から必要な`node_modules`を抜き出すことで依存系を含めてアーカイブすることができます。（npm3になってディレクトリが平坦化されてしまったのでこの方法は無理になってしまったのでどうしようという感じではあります・・・）
ネイティブライブラリ自体もTravisCI上で`npm install`して、同様に依存系を含めてアーカイブすれば基本的に問題ありません。
今のところこの方法で困ったことはないのですが、もしかしたら事故ることはあるかもしれないので、その場合はAmazon Linuxを立ち上げて頑張ってください。

### TravisCIの設定

TravisCIでGitHub releasesにリリースを行います。
テストなりビルドなりはご自由に行ってください。肝としてはアーカイブしたファイルをhttp経由でアクセスできるところにアップロードするところにあります。

```yaml:.travis.yml
language: node_js
node_js:
- '0.10'
sudo: false
cache:
  directories:
  - node_modules
script:
- npm test
before_deploy:
- npm run archive
deploy:
  provider: releases
  api_key:
    secure: xxxxxxxxxxxxxx
  file: dist/archive.zip
  skip_cleanup: true
  on:
    repo: xxx/xxxx
    branch: master
    tags: true
```

S3にアップロードしても良かったのですが、IAM User作るのが面倒臭いのでGitHub releasesにアップロードしています。

## 3. CloudFormationのリポジトリ

![CloudFormationのリポジトリ](https://qiita-image-store.s3.amazonaws.com/0/5522/904bc792-4cd0-a4c7-1cba-dadae5335a65.png)

CloudFormationのテンプレートを管理するリポジトリです。
ここで作成したテンプレートを使って、Lambda Functionのデプロイを行います。

### lambda-deploy-custom-resource-functionの設定

1.で紹介したlambda-deploy-custom-resource-functionをテンプレートに追加してLambda Functionのデプロイ準備をします。

```json
    "LambdaDeployCustomResourceExecuteRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [{
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
              "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
          }]
        },
        "Path": "/",
        "Policies": [{
          "PolicyName": "lambda_exec_role",
          "PolicyDocument": {
            "Version":"2012-10-17",
            "Statement":[
              {
                "Effect": "Allow",
                "Action": [
                  "logs:CreateLogGroup",
                  "logs:CreateLogStream",
                  "logs:PutLogEvents"
                ],
                "Resource": [
                  "arn:aws:logs:*:*:*"
                ]
              },
              {
               "Effect": "Allow",
                "Action": [
                    "lambda:createFunction",
                    "lambda:updateFunctionCode",
                    "lambda:updateFunctionConfiguration",
                    "lambda:deleteFunction",
                    "iam:PassRole"
                ],
                "Resource": [
                  "*"
                ]
              }
            ]
          }
        }]
      }
    },
    "LambdaDeployCustomResourceFunction": {
      "Type" : "AWS::Lambda::Function",
      "Properties" : {
        "Code" : {
          "S3Bucket" : "file-repositories",
          "S3Key" : "lambda/lambda-deploy-custom-resource-function/0.0.3.zip"
        },
        "Description" : "It will deploy the AWS Lambda of general-purpose Lambda Function.",
        "Handler" : "src/index.handler",
        "MemorySize" : 128,
        "Role" : {"Fn::GetAtt" : [ "LambdaDeployCustomResourceExecuteRole", "Arn" ]},
        "Runtime" : "nodejs",
        "Timeout" : 60
      },
      "DependsOn" : "LambdaDeployCustomResourceExecuteRole"
    },
```

### Lamubda Functionの設定

2.で作成したLambda Functionをテンプレートに設定してデプロイできるようにします。

```json
    "LambdaExampleExecuteRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [{
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
              "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
          }]
        },
        "Path": "/",
        "Policies": [{
          "PolicyName": "lambda_exec_role",
          "PolicyDocument": {
            "Version":"2012-10-17",
            "Statement":[
              {
                "Effect": "Allow",
                "Action": [
                  "logs:CreateLogGroup",
                  "logs:CreateLogStream",
                  "logs:PutLogEvents"
                ],
                "Resource": [
                  "arn:aws:logs:*:*:*"
                ]
              }
            ]
          }
        }]
      }
    },
    "LambdaExampleFunction": {
       "Type": "AWS::CloudFormation::CustomResource",
       "Version" : "1.0",
       "Properties" : {
          "ServiceToken": {"Fn::GetAtt" : [ "LambdaDeployCustomResourceFunction", "Arn" ]},
          "URL": "[Your Lambda function URL]",
          "FunctionName": "example-function",
          "Handler": "src/index.handler",
          "Role": { "Fn::GetAtt": ["LambdaExampleExecuteRole", "Arn"] },
          "Runtime": "nodejs",
          "Description": "Example function.",
          "MemorySize": 128,
          "Publish": true,
          "Timeout": 3,
          "Config": {
            "key1": 1,
            "key2": "str",
            "key3": [1, 2, 3]
          }
       },
      "DependsOn" : ["LambdaExampleExecuteRole", "LambdaDeployCustomResourceFunction"]
    }
```

基本的には`AWS::Lambda::Function`リソースと同様ですが、独自の設定の`Config`を指定することで`node-config`で使用する`config/local.json`を生成して、Lambda Functionをデプロイすることができます。
この設定があることで、設定の違う同じLambda Functionを複数デプロイすることができます。

![CloudFormationの実行](https://qiita-image-store.s3.amazonaws.com/0/5522/0390c334-bc4b-4633-1c32-c12e9d37e221.png)


あとは、ここで作成したテンプレートをJenkinsなどのCI環境で実行すればデプロイ完了です。
実行自体ははTravisCIなどでも実行可能ですが、IAM Roleを作成する強い権限が必要になるので、できればプライベートな環境で実行した方が良さそうです。
（encryptでキーを暗号化すれば大丈夫なはずですが、うっかり漏らしたときのダメージがデカすぎてパブリックな環境で実行する勇気がないです）

# この構成のメリット

少し大げさな構成になりますが、この構成を取るメリットは2つあります。

1つ目はCloudFormationを使うことで、Lambda Functionが依存するIAM RoleやAWSリソースを一元管理できることです。
Lambda Functionが少ないうちはそこまで問題にならないのですが、数が増えてくるとどうしても煩雑になってしまうためコードで管理できるなら管理するにこしたことはないです。

2つ目はLambda Functionを公開して利用できる形にするという点です。
Lambda関連の記事でおもしろそうと思える実装例が多いのですが、いかんせんコードをコピーしてIAMを設定して・・・と面倒な手順が多く使うまでに結構気合が必要です。
なので、こういった形で公開されるとCloudFormationに組み込んで設定値を入れるだけで動かせるようになるので、他の人の作成したLambda Functionを利用しやすくなるのではないかと思います。

もちろん、この構成にもデメリットはあります。
1 Lambda Functionにつき1リポジトリという構成になるため、どうしてもリポジトリ数が増えてしまいます。
リポジトリ数が増えるとメンテナンスが難しくなって構成が腐りやすくなってしまったりするので、このあたりどう解決していくはまだ悩んでいる最中です。

複数のLambda Functionで1リポジトリな構成とかも考えていますが、なかなかこれはという構成は思いつかないですね。

# おわりに

ちなみにですが、ちゃんと管理するものはこの手法で管理していますが、どうでも良いようなものだと[TravisCIのLambdaデプロイ機能](https://github.com/travis-ci/dpl#lambda)を使ってデプロイして、IAM Roleは共通のものを使い回していたりします。
このあたりは規模感とか、どう管理したいかで使い分けているような形です。

他にもいろいろなデプロイのやり方があると思うので、この [今年もやるよ！AWS Lambda縛り Advent Calendar 2015](http://qiita.com/advent-calendar/2015/lambda) で自分の思いつかなかったやり方を聞けたら嬉しいなと思います。