CircleCI + LamveryによるLambda functionのdeployベストプラクティス

# 前置き
[AWS Lambda](https://aws.amazon.com/jp/lambda/)はEC2インスタンス無しでイベント駆動で必要な時に必要なだけコードを実行でき、課金も立ち上げているだけで料金がかかるEC2とは違い、完全なる使った分だけ課金という非常に強力で魅力的なサービスです。

しかし、このサービスはその軽量でシンプルなコンセプトとは裏腹に従来無かった独自の概念が多いためか、面倒なことや悩ましいことが多いと感じています。

だからこそ、もっと簡単かつ実用的に使いたいという想いから[Lamvery](http://qiita.com/marcy-terui/items/1617ab4e20e3339d1930)というツールを作っている訳なのですが、その中でも実用面で大きな課題となると感じていたデプロイの部分において、現時点で自分の中でのベストプラクティスと言って差し支えないフローを組むことができたので紹介したいと思います。

## Lambda functionのdeployにおける３つの課題
### ①デプロイパッケージの作成
- Node.js
  `npm install`して`node_modules`ごとzipにまとめれば良いので割と簡単
  https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-create-deployment-pkg.html

- Java
  `Maven`とか`Gradle`とか割りとベーシックな感じ
  https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-java-how-to-create-deployment-package.html

- Python
  割りと残念な手順（Virtualenv で作成した Python 環境を使用してデプロイパッケージを作成するの章）
  https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html

### ②バージョン管理  
- Lambdaの特殊なバージョニング仕様  
http://dev.classmethod.jp/cloud/aws/lambda-versioning/
- Git等のbranch運用とLambda環境の関連付け(stagingとproduction等)


### ③デプロイフローの整備
ベストプラクティスはまだ無いというか、ほとんどこの辺の話を見かけたことがない。[^1]


## 今回の方法によって解決するもの

### ①Pythonデプロイパッケージの作成（Node.jsもいけるはず）
まず、[Lamvery](http://qiita.com/marcy-terui/items/1617ab4e20e3339d1930)自体がvirtaulenv内で簡単にデプロイパッケージを作り、それをデプロイするための機能を持っています。なので、CircleCIのPython環境でクリーンなvirutalenv環境を作り、その中で必要なライブラリをインストールしてデプロイするだけです。
単純にカレントディレクトリ以下をzipにまとめてデプロイすることもできるので、Node.jsも`package.json`をリポジトリに含めて`npm install`を叩かせれば同じようにいけるはずです。[^2]

### ②Lambdaの特殊なバージョニング仕様を活かしたGit branchとの紐付け
Lambdaのバージョンに付けられるエイリアスを活用し、エイリアスを個々に指定してfunctionの実行が可能である特性を活かして、branchと紐付けた個別の実行環境を提供します。

### ③Pull Requestベースのデプロイフロー
いわゆるこういうやつ → http://d.hatena.ne.jp/naoya/20140502/1399027655
②ができれば、そりゃできるよねって話ではあるんですが。

# 方法と手順
ちなみに今回の内容を実際に検証したものがこちらになります。
https://github.com/marcy-terui/lamvery-circleci-deploy

CircleCIで該当のリポジトリを有効にする所は通常と同じなので割愛します。

## 1. IAM設定
Lambda functionに割り当てるIAM Roleはもちろんですが、CircleCIに設定するIAM Userが必要になります。

### Lambda functionに割り当てるIAM Roleのポリシー
これは、そのfunctionに何をさせたいかによりけりなのですが、最低限の権限とLamveryの固有機能であるKMSを利用した機密情報の受け渡しを行う場合は以下の様な権限となります。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:<region>:<account-number>:key/<key-id>"
            ]
        }
    ]
}
```
`<region>`,`<account-number>`,`<key-id>`は適宜置き換えてください。
KMSのキーの作り方（key-idの発行）についてはこちら↓
https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/create-keys.html

**このRoleは以降の章で使用するので、ARN[^3]を控えておきましょう。**

### CircleCIに設定するIAM Userのポリシー
ここはあまり変動はないと思います。
一々設定するのが面倒なら`lambda:*`は全リソース許可(`"Resource": "*"`)でもまあ場合によってはアリかなとは思いますが、`iam:PassRole`は全リソース許可すると大変危険なので止めたほうが良いです。[^4]

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "lambda:*",
                "iam:PassRole"
            ],
            "Resource": [
                "arn:aws:lambda:<region>:<account-number>:function:<function-name>",
                "arn:aws:lambda:<region>:<account-number>:function:<function-name>:*",
                "<function-role-arn>"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "lambda:CreateFunction",
                "lambda:ListFunctions",
                "lambda:ListVersionsByFunction"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```
`<region>`,`<account-number>`,`<function-name>`は適宜置き換えてください。
`<function-role-arn>`には先ほど作成したRoleのARN[^3]が入ります。

**このUserのCredentialを次の章で使用しますので、控えておきましょう。**

### IAM UserのCredentialを取得し、CircleCIへ設定

1. Project Settingsを開く  
![image](https://qiita-image-store.s3.amazonaws.com/0/57327/e6481647-8b11-0dbc-760e-016c167f9537.png)
2. AWS Permissionを選択  
![image](https://qiita-image-store.s3.amazonaws.com/0/57327/2898c135-d303-6c89-f42d-95beea30d9cd.png)
3. Credentialを入力する  
![image](https://qiita-image-store.s3.amazonaws.com/0/57327/d18134b7-dc64-d191-435b-5981db3d19dc.png)

## 2. Lamveryのインストールと設定
### Lamveryをインストールして設定ファイルを生成

```shell-session
$ pip install lamvery
$ lamvery init
lamvery: Output initial file: .lamvery.yml
lamvery: Output initial file: .lamvery.exclude.yml
lamvery: Output initial file: .lamvery.event.yml
lamvery: Output initial file: .lamvery.secret.yml
```

### 設定ファイルを編集する
`region`及び`configuration`以下の各設定は適宜置き換えてください。
`{{ env['AWS_LAMBDA_ROLE'] }}`の部分はPrivateリポジトリならRoleのARNくらいはリポジトリに入ってても問題ないかなという気はします。
同じように環境変数から渡したい場合はCircleCI上にて「Project Settings」→「Environment Variables」で設定できます。

```yaml:.lamvery.yml
profile: null
region: us-east-1
versioning: false
default_alias: master
configuration:
  name: lamvery-deploy-sample
  runtime: python2.7
  role: {{ env['AWS_LAMBDA_ROLE'] }}
  handler: lambda_function.lambda_handler
  description: This is a sample lambda function.
  timeout: 10
  memory_size: 128
```

設定のポイント

- `versioning: false`  
  デフォルトではLambdaのバージョニングを無効にして、必要に応じてコマンドオプションで有効化します。
- `default_alias: master`  
  コマンドオプションで指定しない場合にデプロイ時に付けるエイリアスです。`master`じゃなくても良いのですが、オペミス防止の為production用のエイリアス（後述）は避けましょう。

## Functionを実装し、必要なライブラリを列挙する
functionの実装はただのPythonコーディングなので割愛します。上記の例では`handler`が`lambda_function.lambda_handler`のとなっているので、以下の様なファイルになります。

```py:lambda_function.py
import lamvery


def lambda_handler(event, context):
    print(lamvery.secret.get('foo'))
```

Pythonプロジェクトでは`requirements.txt`に書くことが多いので、今回はそのようにしています。virtualenv内で以下のように必要なライブラリをインストールした上でファイルに書き出すという方法がおすすめです。

```shell-session
pip install flake8
pip freeze > requirements.txt
```

こうすることでCircleCI上で以下を実行することで同じライブラリがインストールできます。

```shell-session
pip install -r requirements.txt
```

## CircleCIの設定を記述
以下のように記述します。

```yml:circle.yml
---
machine:
  python:
    version: 2.7

dependencies:
  pre:
    - |
      virtualenv .venv
      source .venv/bin/activate
      pip install -r requirements.txt

test:
  override:
    - |
      source .venv/bin/activate
      flake8 lambda_function.py

deployment:
  master-head:
    branch: master
    commands:
      - |
        source .venv/bin/activate
        lamvery deploy
  staging:
    branch: staging
    commands:
      - |
        source .venv/bin/activate
        lamvery deploy -a staging -p
  production:
    branch: production
    commands:
      - |
        source .venv/bin/activate
        lamvery deploy -a production -p
```

### 設定のポイント

- `dependencies.pre`でクリーンなvirtaulenv環境を作り、その中で必要なライブラリをインストール  
Lamveryもこの中に入っています（機密情報の受け渡しを行わない場合はFunction自体にLamveryは必要ないので、別途インストールしても良いと思います）
- `master`ブランチではデプロイ時のコマンドオプション無し  
オプション無しの場合、`versioning: false`, `default_alias: master`となっているため、常に`$LATEST`というバージョニングが無効な場合に使われる特殊なバージョン[^5]に`master`というエイリアスが付くことになります。  
これにより、**[Lambdaには容量制限がある](http://ijin.github.io/blog/2016/01/14/monitor-lambda-capacity/)ためmasterブランチの更新で常に新しいバージョンが発行されるのは避けたいが、HEADは常に実行可能**という状態にできます。
- `staging`及び`production`では明示的にエイリアス名とバージョニングを有効にするオプションを付ける  
  `-a`でエイリアス名指定、`-p`でバージョニングを有効にできます。

# Let's deploy!
プルリクエストを作ってマージしてみます。

![image](https://qiita-image-store.s3.amazonaws.com/0/57327/362d629f-dbea-3f2c-3779-83a945caf10c.png)


デプロイ結果を見てみましょう。

![image](https://qiita-image-store.s3.amazonaws.com/0/57327/14f0f599-f1dc-312a-a68b-ba7823cdfc5d.png)

色付きのログを見ると、`4`という新しいバージョンが発行されてそこに`production`のエイリアスが付け替えられているのがなんとなくお分かりいただけるかと思います。

ちなみに併せて `production-pre`というエイリアスが設定されているかと思いますが、これはロールバック用で、以下のように緊急時のロールバックができます。手元で叩いても良いですし、ChatBotなんかに叩かせても良いと思います。

```shell-session
$ lamvery rollback -a production
lamvery: [Function] Previous version: 2
lamvery: [Alias] production: 4 -> 2
```

# デプロイされたfunctionの実行について
各種Event等から実行する場合、エイリアス毎に個別のARN[^3]がついているので注意しましょう。
以下の様な指定になります。APIの場合、`Qualifier`というパラメータでエイリアスを指定する場合もあります。
`arn:aws:lambda:<region>:<account-number>:function:<function-name>:<alias>`

# まとめ
[Lamvery](http://qiita.com/marcy-terui/items/1617ab4e20e3339d1930)というツールを作る目的の一つであった、自分の中で「これだ！」と思えるLambda functionのデプロイフローができたのでご紹介させてもらいました。
今までEC2等で行われていた手法と大きな違いがなく、違和感なく使えるフローを意識していますが、もっとこうした方が良い等ご意見ありましたら、是非以下からフィードバックいただけると嬉しいです。

https://github.com/marcy-terui/lamvery

TODO: CircleCIの手前までを自動化するCloudFormationテンプレートの作成


[^1]: 唯一見つけたのが[これ](http://qiita.com/kinzal/items/046a848a9f0829f87670)。凄く良く出来てるけど、今回の内容はもっとデプロイに特化していて方向性が違う。
[^2]: Node.jsは未検証なのでやってみて結果を報告してもらえると喜びます（Javaは・・・？）
[^3]: [Amazon Resource Name](https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-arns-and-namespaces.html)の略
[^4]: 任意のRoleを割り当てられるようになる→AdminやPowerUser権限のRoleがあれば指定できちゃう→Lambda経由でやりたい放題
[^5]: わかりにくい表現だけど他に思いつかない。。。