Lambda with Apex: Terraformを使った管理


## 概要

前回以下の記事でApexを使ってLambdaを環境別に管理する方法をまとめましたが、
[Lambda with Apex: 環境変数で環境別にLambda環境を整える - Qiita](http://qiita.com/gaishimo/items/af2a05bcaf4cb96e534b)

今回はApexでTerraformを使ってLambdaを管理する方法を試してみたいと思います。



## バージョン等

- OS: Mac OS X 10.11.1
- Terraform: v0.6.16
- Apex: 0.9.0
- 言語: Go

## 事前設定

### Terraform インストール

Terraformがまだない場合はインストールします。
[Download Terraform - Terraform by HashiCorp](https://www.terraform.io/downloads.html)

### Apexのインストール

Apexがまだない場合はインストールします。

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

### AWS設定


#### AWSアカウントの用意

今回は環境ごとにAWSアカウントを分けてみたいので、prod用とdev用にAWSアカウントを用意しました。
アカウントを用意したら `~/.aws/credentials` に記述しておきます。

```
# dev用
[default]
region=ap-northeast-1
aws_access_key_id=XXXXXXXXXXX
aws_secret_access_key=XXXXXXXXXXX

# prod用
[prod]
region=ap-northeast-1
aws_access_key_id=XXXXXXXXXXX
aws_secret_access_key=XXXXXXXXXXX
```

dev用のprofileををdefaultにしておきました。


#### S3バケットの用意

Terraformの状態ファイルを保存するためのS3バケットを用意しておきます。今回は`apex-terraform-example`という名前にします。


## プロジェクト作成

まずディレクトリを作成します。

```
mkdir apex_terraform
```

apexコマンドでプロジェクトを作成します。

```
cd apex_terraform
apex init
```

対話コマンドで以下のように入力します。

```
Project name: apex_terraform
Project description:
Would you like to manage infrastructure with Terraform? (yes/no): yes
Environments: dev, prod
Would you like to store Terraform state on S3? (yes/no): yes
S3 bucket name: apex-terraform-example
```

Terraformを使うかどうかの質問で`yes`を選択します。
また、Terraformの状態ファイルをS3に配置するか聞かれるので、yesにしておきます。yesにするとS3のBucket名を聞かれるので指定します。

完了すると以下のようなファイル構成でプロジェクトが作成されます。


```
├── functions
│   └── hello
│       └── index.js
├── infrastructure
│   ├── dev
│   │   └── main.tf
│   ├── modules
│   │   └── iam
│   │       ├── iam.tf
│   │       └── outputs.tf
│   └── prod
│       └── main.tf
└── project.json
```

infrastructureの下にdev/prodと環境別にフォルダが作成され、terraformの設定ファイルが配置されています。また、infrastructure/modulesの下にIAMロール作成用のモジュールが配置されています。各環境の`main.tf`ファイルでは以下のように記述されており、このモジュールがIAMロール作成に使用されていることが確認できます。

```
module "iam" {
  source = "../modules/iam"
}

output "lambda_function_role_id" {
  value = "${module.iam.lambda_function_role_id}"
}
```


## インフラ構築 (dev)


環境ごとにTerraformを実行してみます。まずplanで実行計画を確認してみます。

```
$ apex infra plan
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ module.iam.aws_iam_role.lambda_function
    arn:                "" => "<computed>"
    assume_role_policy: "" => "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Service\": \"lambda.amazonaws.com\"\n      },\n      \"Action\": \"sts:AssumeRole\"\n    }\n  ]\n}\n"
    name:               "" => "$ apex infra plan
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ module.iam.aws_iam_role.lambda_function
    arn:                "" => "<computed>"
    assume_role_policy: "" => "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Service\": \"lambda.amazonaws.com\"\n      },\n      \"Action\": \"sts:AssumeRole\"\n    }\n  ]\n}\n"
    name:               "" => "apex_lambda_function"
    path:               "" => "/"
    unique_id:          "" => "<computed>"

+ module.iam.aws_iam_role_policy.cloudwatchlogs_full_access
    name:   "" => "cloudwatchlogs_full_access"
    policy: "" => "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Action\": [\n        \"logs:*\"\n      ],\n      \"Effect\": \"Allow\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n"
    role:   "" => "${aws_iam_role.lambda_function.id}"


Plan: 2 to add, 0 to change, 0 to destroy."
    path:               "" => "/"
    unique_id:          "" => "<computed>"

+ module.iam.aws_iam_role_policy.cloudwatchlogs_full_access
    name:   "" => "cloudwatchlogs_full_access"
    policy: "" => "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Action\": [\n        \"logs:*\"\n      ],\n      \"Effect\": \"Allow\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n"
    role:   "" => "${aws_iam_role.lambda_function.id}"


Plan: 2 to add, 0 to change, 0 to destroy.
```

CloudWatch Logsへのフルアクセス権限を持つPolicy作成と、apex_lambda_functionという名前のIAM Roleが作成されることがわかります。

確認できたのでTerraformを本実行してみます。

```
$ apex infra apply
module.iam.aws_iam_role.lambda_function: Creating...
  arn:                "" => "<computed>"
  assume_role_policy: "" => "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Service\": \"lambda.amazonaws.com\"\n      },\n      \"Action\": \"sts:AssumeRole\"\n    }\n  ]\n}\n"
  name:               "" => "apex_lambda_function"
  path:               "" => "/"
  unique_id:          "" => "<computed>"
module.iam.aws_iam_role.lambda_function: Creation complete
module.iam.aws_iam_role_policy.cloudwatchlogs_full_access: Creating...
  name:   "" => "cloudwatchlogs_full_access"
  policy: "" => "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Action\": [\n        \"logs:*\"\n      ],\n      \"Effect\": \"Allow\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n"
  role:   "" => "apex_lambda_function"
module.iam.aws_iam_role_policy.cloudwatchlogs_full_access: Creation complete

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

Outputs:

  lambda_function_role_id = arn:aws:iam::588762728270:role/apex_lambda_function
```

これでIAMロールが作成されました。ただ、現時点ではdev環境のIAM Roleしか作成されていません。


### インフラ構築 (prod)

現時点ではdev環境のIAM Roleしか構築されていないため、prod用にも構築を行いたいと思います。

初期状態ではTerraformの設定ファイルはまったく同じものが配置されているため、prod用の設定ファイルを編集して以下を記述します。

```infrastructure/prod/main.tf
variable "aws_region" {
  default = "ap-northeast-1"
}

provider "aws" {
  profile = "prod"
}
...
```

そのままだとdevと同じデフォルトのProfile(AWSアカウント)を使用してしまうので、`prod`Profileを使うように設定しています。

~~Terraformの実行ですが、apex infraコマンドでは対象の環境を指定するようなオプションが見当たりませんでした(自分が把握してないだけかも)。。ただ、基本的には通常のTerraformなので、prodディレクトリにいって`terraform apply` することにしました。~~
※2016/06/01変更 - `-e`オプションを指定すれば行けました!


```
$ apex infra -e prod apply
module.iam.aws_iam_role.lambda_function: Creating...
  arn:                "" => "<computed>"
  assume_role_policy: "" => "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Principal\": {\n        \"Service\": \"lambda.amazonaws.com\"\n      },\n      \"Action\": \"sts:AssumeRole\"\n    }\n  ]\n}\n"
  name:               "" => "apex_lambda_function"
  path:               "" => "/"
  unique_id:          "" => "<computed>"
module.iam.aws_iam_role.lambda_function: Creation complete
module.iam.aws_iam_role_policy.cloudwatchlogs_full_access: Creating...
  name:   "" => "cloudwatchlogs_full_access"
  policy: "" => "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Action\": [\n        \"logs:*\"\n      ],\n      \"Effect\": \"Allow\",\n      \"Resource\": \"*\"\n    }\n  ]\n}\n"
  role:   "" => "apex_lambda_function"
module.iam.aws_iam_role_policy.cloudwatchlogs_full_access: Creation complete

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

Outputs:

  lambda_function_role_id = arn:aws:iam::109572732475:role/apex_lambda_function
```

これでprod用のIAM Roleも作成されました!


## function作成(環境変数を指定)

続いて環境変数を使ったLambdaのコードを作成してみます。環境別に環境変数を変えて実行結果が変わるようにしてみます。

```functions/hello/.apexignore
*.go
```

```functions/hello/main.go
package main

import (
	"encoding/json"
	"os"

	"github.com/apex/go-apex"
)

func main() {
	apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) {
		return os.Getenv("HOGE"), nil
	})
}
```

main()の中では環境変数のHOGEに指定されている値を返しています。
※Goを使ったコードのサンプルは[apex/_examples/go at master · apex/apex](https://github.com/apex/apex/tree/master/_examples/go)で確認できます。


環境変数はdevとprodで切り替えたいため、envファイルをそれぞれ用意します。

.env-dev

```.env-dev
HOGE=FUGA
```

.env-prod

```.env-prod
HOGE=PIYO
```


dev環境にデプロイして実行してみます。環境変数は`--set`オプションで指定します。

```
$ apex deploy hello --set $(cat .env-dev)

   • config unchanged          function=hello
   • code unchanged            function=hello
$ apex invoke hello
"FUGA"
```

環境変数の指定が反映されていることがわかります。

次にprod環境にデプロイしてみます。

```
$ apex deploy hello -e prod --profile prod --set $(cat .env-prod)
   • config unchanged          function=hello
   • updating function         function=hello
   • updated alias current     function=hello version=2
   • function updated          function=hello name=apex_terraform_hello version=2
$ apex invoke hello -e prod --profile prod
"PIYO"
```

こちらもprod用に環境変数が指定されていることが確認できました!