Lambda with Apex: 環境変数で環境別にLambda環境を整える

## 要約

- Lambda管理ツールのApexが便利
- Apexでは環境変数の設定ができる
- 環境別(dev,prod等)にセットを分けるには少し工夫しないとダメ

## 概要

AWS Lambdaを本格的に運用する場合、同じアプリ(Lambda Function)を環境別に分けたいケースがあります。例えば本番用は現在運用中のものをのせ、検証用は最新の開発に使うようなケースです。

ただ単に分けるだけであれば名前を変えてLambda Functionを登録すればよいのですが、ソースコードはそのままで、認証キーの値やアクセスするDB・テーブル等の設定値のみ変更したいケースが多々あると思います。残念ながら現状のAWS Lambdaでは環境変数のようなものを設定する機能等は用意されていないため、少し工夫が必要になります。

そこで、最近話題のLambda管理ツール[Apex](https://github.com/apex/apex)が環境変数に対応しているようなので、実際に試してみました。

※(追記:2016/06/16) 環境別にAWSアカウントを用意しApexでTerraformを使ってインフラ構築する方法についても書きました。
[Lambda with Apex: Terraformを使った管理 - Qiita](http://qiita.com/gaishimo/items/dd27e49ba55b4d99761c)

## やってみること

DynamoDBにアクセスするサンプルのLambda Functionを用意し、Apexの環境変数により対象テーブルを切り替えてみます。LambdaのエンジンはNode.js 4.3を使います(ES6が使える)。


## 事前準備

AWSのAPIを使用するため、`~/.aws/credentials`に値を設定しておきます。

~/.aws/credentials
```
[default]
region=ap-northeast-1
aws_access_key_id=xxxxxxxxxxxxxxx
aws_secret_access_key=xxxxxxxxxxxxxxxxxxx
```

また、Lambda Functionに設定するIAM Role(DynamoDBにアクセス可能な)を用意しておく必要があります。

## DynamoDBにテーブルを作成

事前にDynamoDBテーブルを環境別に作成しておきます。

DynamoDBは同一Regionでデータベースを分けたりということはできないので、`dev_xxxx`, `prod_xxxx` といったように環境別にPrefixをつけて区別する形とします。　サンプルとしてユーザテーブル(xxx_users)を作成します。データは` { id: 1, name: "Masahiko Usui"}`のようなシンプルな形式です。

今回はRubyでaws.rbを使って作成します。
以下でaws.rbを起動し、

```bash
aws.rb --region ap-northeast-1
```

コンソールで以下を実行します。

```ruby
client = Aws::DynamoDB::Client.new

# テーブル作成
%w(dev prod).each do |env_name|
  table_name = "#{env_name}_users"
  client.create_table({
    table_name: table_name,
    attribute_definitions: [
      { attribute_name: 'id', attribute_type: 'N' }
    ],
    key_schema: [
      { attribute_name: 'id', key_type: 'HASH' }
    ],
    provisioned_throughput: {
      read_capacity_units: 1,
      write_capacity_units: 1
    }
  })
  p client.describe_table(table_name: table_name)
end

# データ作成

# dev用データ
(1..3).each do |i|
  client.put_item({
    table_name: 'dev_users',
    item: {
      id: i,
      name: "test user#{i}"
    }
  })
end

# prod用データ
users = [
  { id: 1, name: "Masahiko Usui"},
  { id: 2, name: "Maki Azuma"},
  { id: 3, name: "Sadahisa Sugimoto" }
]
users.each do |user|
  client.put_item({
    table_name: 'prod_users',
    item: user
  })
end
```

これで開発用のユーザテーブル(dev_users)、本番用のユーザテーブル(prod_users)とそのテストデータが作成されました。

## Apexの導入

Apexのインストール・設定は以下の記事を参考にしました。
[ApexでAWS Lambdaファンクションを管理する ｜ Developers.IO](http://dev.classmethod.jp/cloud/aws/how-to-manage-aws-lambda-functions-with-apex/)
### インストール

```
curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh
```

### 設定
ApexはAWS APIを使う際に`~/.aws/credentialｓ`の値を読みにいってくれますが、regionについて読み込まれないようなので(~/.aws/configからでないとダメ)、環境変数で設定しておきます。

```bash
export AWS_REGION='ap-northeast-1'
```

### プロジェクトの作成

以下でApexのプロジェクトを作成します。

```
mkdir apex-env
cd apex-env
apex init
```

対話コマンドでプロジェクト名、 Project name、Project description、IAM roleを聞かれるため入力します(今回はTerraformは使いません)。

```
  Enter the name of your project. It should be machine-friendly, as this
  is used to prefix your functions in Lambda.

    Project name: apex-env

  Enter an optional description of your project.

    Project description:

  Would you like to manage infrastructure with Terraform? (yes/no) no

  Enter IAM role used by Lambda functions.

    IAM role: <Your IAM Role>

  [+] creating ./project.json
  [+] creating ./functions

  Setup complete!

  Next step:
    - apex deploy - deploy example function
```

完了すると以下のような構成でファイル・ディレクトリが作成されます。

```
├── functions
│   └── hello
│       └── index.js
└── project.json
```


## Lambda Functionの処理作成

今回はfunctions/helloの下に作成していきます。

まず、依存ライブラリの管理のため、`package.json`を追加し、


```functions/hello/package.json
{
  "name": "apex-env-hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "aws-sdk": "^2.3.12",
    "dynamodb-marshaler": "^2.0.0"
  }
}
```

`npm install`でインストールします。

```bash
cd functions/hello
npm install
```


次に実際のFunction内の処理を作成していきます。


```functions/hello/index.js
'use strict';
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB({region: 'ap-northeast-1'});
const unmarshalItem = require('dynamodb-marshaler').unmarshalItem;

exports.handle = function(e, ctx, cb) {
  let params = {
    TableName: 'dev_users',
    Key: { id: { "N": e.id || '1' } }
  };
  dynamodb.getItem(params, (err, res) => {
    let user = unmarshalItem(res.Item);
    cb(err, user)
  })
}
```

DynamoDBから指定したキーでレコードを取得しcallbackに渡して処理を終了しています。現時点ではテーブル名は固定(`dev_users`)です。


この時点で一度デプロイし実行してみます。

```bash
$ apex deploy hello
   • config unchanged          function=hello
   • updating function         function=hello
   • updated alias current     function=hello version=14
   • function updated          function=hello name=apex-env_hello version=14
```

```bash
$ apex invoke hello
{"name":"test user1","id":1}
```

実行されてFunctionの結果が返ってきました!  デプロイして実行するのがすぐに出来てしまうので、サーバで実行されている感じがせず、あたかもローカルで実行しているような感覚に陥ります。


更にeventパラメータを指定して実行してみます。

```bash
$ echo -n '{ "id": "2" }' | apex invoke hello
{"name":"test user2","id":2}
```

指定された`id="2"`のユーザが返ってきました! eventパラメータが正しく渡されて実行されていることがわかります。


## 環境変数の利用

次に環境変数を使ってみたいと思います。Apexではデプロイ時の`--env`オプション、もしくは`project.json` (または`function.json`)の `"environment"`属性で環境変数を指定することができます。

まずコードのテーブル名を指定している部分を以下のように書き換えて動的にします。

```functions/hello/index.js
  let params = {
    TableName: `${process.env.DYNAMO_TBL_PREFIX}users`,
    // ...
  };
```

`project.json`の`environment`でENVの値を指定します。


```project.json
{
  ...
  "environment": {
    "DYNAMO_TBL_PREFIX": "dev_"
  }
}
```

この状態でデプロイして実行してみます。

```bash
$ apex deploy hello && apex invoke hello
{"name":"test user1","id":1}
```

結果が無事返ってきました!

次にENVの値を変更して実行してみます。

```project.json
{
  ...
  "environment": {
    "DYNAMO_TBL_PREFIX": "prod_"
  }
}
```

```bash
$ apex deploy hello && apex invoke hello
{"name":"Masahiko Usui","id":1}
```

値が切り替わってproduction用のテーブルからデータを取得しているのが確認できました! ENVの指定が正しく利用されていることがわかります。



## 環境ごとにセットを切り替える

環境変数が利用できるのは確認できましたが、このままだと環境別に切り替えるにはデプロイする都度環境変数を変更しないといけないことになります。また、function名は同じであるため、同一のFunctionを使いわますことになってしまいます。

残念ながらApexの現状のバージョン(v0.9.0)では環境セットを切り替える手段は用意されていません。そこでいくつか対応案を考えてみました。

### 対応案1) AWSのアカウントを環境別に分ける

`apex deploy`時には`--profile` オプションが指定できます。AWSアカウントを環境別に用意して、都度Profileを指定すればいけそうです。環境変数の値については、`env-dev`、`env-production`といった形で各環境用のenvファイルを用意しておき、 

```bash
apex deploy hello --profile=xxxx --set  $(cat env-dev)
```

のように`--set`オプションに渡してあげれば良さそうです。

アカウントが別になるということはLambda Functionに渡すIAM Roleも別物になるということになります。ただ、現状apexコマンドからはIAM Roleを指定するオプションが存在しないため、`project.json`(もしくは`function.json`)を都度編集しないといけなくなりそうです..

以下のIssueでその辺り議論されています。
[Multi-env support in config · Issue #344 · apex/apex](https://github.com/apex/apex/issues/344#issuecomment-203788257)

Terraformを使う場合、`-e` switchを使えば簡単にできるそうですが、今回Terraform使っていないので試していません。


### 対応案2) AWSのRegionを環境別に分ける。

apexコマンド実行時に`--region`オプションでRegionを切り替えることができます。
```
apex deploy hello --region  us-east-1  --set  $(cat env-dev)
```
都度region指定でデプロイ・実行することで環境を分けます。この方法であれば確かに環境は分けることができますが、都度指定するため間違えたりしそうです:sweat: また、 Regionを別にしたくないケース等あるかもしれません。

### 対応案3) project.jsonファイルを都度切り替える

事前に環境別の`project.json`ファイルを用意しておき、apexコマンド実行前にファイルを都度切り替える方法です。

まずdev用とproduct用の`project.json`ファイルを以下のように用意します。プロジェクト名と環境変数の部分のみ異なります。

```project.dev.json
{
  "name": "apex-env-dev",
  ...
  "environment": {
    "DYNAMO_TBL_PREFIX": "dev_"
  }
}
```

```project.prod.json
{
  "name": "apex-env-prod",
  ...
  "environment": {
    "DYNAMO_TBL_PREFIX": "prod_"
  }
}
```

そしてシンボリックリンクを作成し、project.jsonはdevかprodのどちらかを向くようにします。デフォルトではdevのjsonファイルを向くようにしておきます。


```
ln -sf project.dev.json  project.json
```

dev環境に対しては通常どおりapexコマンドをそのまま使用します。prodに対しては以下のようなwrapperスクリプトを用意しておきます。


```apex-prod
#!/bin/bash
set -e

trap 'ln -sf project.dev.json  project.json' ERR

ln -sf project.prod.json project.json
apex "$@"
ln -sf project.dev.json  project.json

exit 0
```

やっていることは`project.json`のシンボリックリンクの向き先を一時的にprodにし、apexコマンドを実行しているだけです。


prodに対しては常にwrapperを通して実行する形とします。

```bash
./apex-prod deploy hello
```

　これで環境別にFunctionを分けることができました! 同一アカウントでFunctionの名前を変えて分ける場合はこの方法が一番シンプルだと思います。apexでいずれ環境を切り替える方法が提供される気がするので、それまでの暫定対応であればとりあえずこれでよいかと思います。


## 雑感

Apexはとても便利でした。まだ今後も機能が増えていくと思うので期待したいです。

Lambdaの魅力はサーバ管理の手間が要らずスピーディに作りたいものを動かせる点だと思いますが、apexを使うことでLambdaの管理の手間が更に少なくなり、開発を加速させられるのではないかと思います。また、Goが使えるのも魅力的です。

Lambdaを使って本番運用等を行う場合、どのようなフローで開発・デプロイ・保守等を行っていくのかが重要になってくると感じています。大きい規模のアプリであればそういったフローは自然と整えざるを得ないと思うんですが、Lambdaは単位が小さいため何も考えずに作っていくと管理がよくわからない状態のFunctionが大量にできてしまったり、保守しづらいものが増えてしまったりしそうな気がします。そのためApex等のツールを使ったりCIを導入したりすることで管理の枠組みを整えてしまえば、より安心してスピーディにLambda開発ができるようになるのではないかと思います。


また、今後Terraformを使った管理も試してみたいと思います。