[Terraform] DynamoDBをトリガーとしたLambdaの設定方法



# やりたいこと
記事タイトルそのままです。
DynamoDBのデータ更新をトリガーとして起動されるLambdaを、Terraformで設定したい。

# tfファイルの記述例

* 使用tfリソース一覧
  * [aws_iam_role](https://www.terraform.io/docs/providers/aws/r/iam_role.html)
  * [aws_iam_role_policy](https://www.terraform.io/docs/providers/aws/r/iam_role_policy.html)
  * [archive_file](https://www.terraform.io/docs/providers/archive/d/archive_file.html)
  * [aws_lambda_function](https://www.terraform.io/docs/providers/aws/r/lambda_function.html)
  * [aws_dynamodb_table](https://www.terraform.io/docs/providers/aws/r/dynamodb_table.html)
  * [aws_lambda_event_source_mapping](https://www.terraform.io/docs/providers/aws/r/lambda_event_source_mapping.html)

* tfファイルは `IAM`, `Lambda`, `DynamoDB` で分割しています。そのへんはお好みで。
* オプション等は、作成したいものに合わせて適宜変更。


## IAM
```:aws.iam.tf
// Lambda用ロール
resource "aws_iam_role" "sample-lambda-role" {
    name = "sample-lambda-dynamodb-trigger"
    assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "sts:AssumeRole",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Effect": "Allow",
            "Sid": ""
        }
    ]
}
EOF
}

// Lambdaログ出力権限
resource "aws_iam_role_policy" "sample-lambda-log-output" {
    role = "${aws_iam_role.sample-lambda-role.id}"
    name = "lambda-log-output"
    policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*",
            "Effect": "Allow"
        }
    ]
}
EOF
}

// DynamoDB Stream の読み込み権限
resource "aws_iam_role_policy" "sample-dynamodb-stream" {
    role = "${aws_iam_role.sample-lambda-role.id}"
    name = "dynamodb-stream"
    policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "dynamodb:DescribeStream",
                "dynamodb:GetRecords",
                "dynamodb:GetShardIterator",
                "dynamodb:ListStreams"
            ],
            "Resource": "${aws_dynamodb_table.sample-table.stream_arn}",
            "Effect": "Allow"
        }
    ]
}
EOF
}
```
* `DynamoDB Stream` からデータを読み取るためのポリシーが必要です。この権限が付いていないと、トリガー設定でコケます。


## Lambda
```:aws.lambda.tf
// Lambda Function を ZIP化
data "archive_file" "lambda-zip_sample-function" {
    type        = "zip"
    output_path = "./_zip/sample-function.zip"
    source {
        filename = "lambda_function.py"
        content  = <<EOF
import json
def lambda_handler(event, context):
    result = json.dumps(event)
    print(result)
    return result
EOF
    }
}

// Lambda Function 作成
resource "aws_lambda_function" "sample-function" {
    function_name      = "sample-function-name"
    handler            = "lambda_function.lambda_handler"
    filename           = "${data.archive_file.lambda-zip_sample-function.output_path}"
    source_code_hash   = "${data.archive_file.lambda-zip_sample-function.output_base64sha256}"
    memory_size        = 128
    timeout            = 300
    runtime            = "python3.6"
    role               = "${aws_iam_role.sample-lambda-role.arn}"
    description        = "Dynamo Trigger Test"
}
```
* この記述例ではイベント情報を出力するだけの Lambda Function をtfファイルに直書きしています。
zipファイルの置き場として、ディレクトリ `./_zip/` が必要。
* functionファイルを作成しておいて `archive_file` でZIP化しても、あらかじめzipファイルを用意しておいても構いません。


## DynamoDB
```:aws.dynamodb.tf

resource "aws_dynamodb_table" "sample-table" {
  name           = "sample-table-name"
  read_capacity  = 1
  write_capacity = 1
  hash_key       = "key1"
  range_key      = "key2"
  attribute {
    name = "key1"
    type = "S"
  }
  attribute {
    name = "key2"
    type = "S"
  }
  stream_enabled   = true
  stream_view_type = "NEW_AND_OLD_IMAGES"
}


//
resource "aws_lambda_event_source_mapping" "sample-table-trigger" {
    depends_on         = ["aws_dynamodb_table.sample-table", "aws_iam_role_policy.sample-dynamodb-stream"]
    batch_size         = 100
    event_source_arn   = "${aws_dynamodb_table.sample-table.stream_arn}"
    enabled            = true
    function_name      = "${aws_lambda_function.sample-function.arn}"
    starting_position  = "TRIM_HORIZON"
}
```

* `aws_dynamodb_table` に下記のパラメータを記述して、DynamoDB Stream を有効化。
  * stream_enabled = true
  * stream_view_type = (任意)
* `aws_lambda_event_source_mapping` で Lambdaのトリガーとして `DynamoDB Stream` を追加。
* `aws_lambda_event_source_mapping`　は Lambda が主語のような気はするものの、`DynamoDB Stream` の存在に依存する設定なので、DynamoDB用のtfファイルにまとめてます。個人的な好みです。





# 注意事項

## 一発目はこれでOK。
Terraform実行一発目から上記内容で設定する場合は、問題なく設定できるはずです。

## DynamoDB Stream を後付けする場合はNG。
既にDynamoDBのTableを作成済みで、後から `stream_enabled = true` を追加設定する場合は、単純にtfファイルに上記の通り記述して実行しても上手く行きません。（Terraform v0.11.3 時点）
下記のようなエラーが発生するはずです。

```
aws_iam_role_policy.sample-dynamodb-stream: Resource 'aws_dynamodb_table.sample-table' does not have attribute 'stream_arn' for variable 'aws_dynamodb_table.sample-table.stream_arn'
```
「`aws_dynamodb_table` に `stream_arn` が無いよ！」と怒られてます。

## なぜ？
Terraform は、Terraform を利用して設定したリソースの情報をtfstateファイルに保持しています。
そして次回実行するときは、tfstateファイルに保持されている現在のリソース情報と、新たなtfファイルの内容を突き合わせて最小限のアップデートをします。
その時、リソース情報の整合性チェック等（Plan）を行ってから、設定反映（Apply）を実行する仕組みになっています。
`DynamoDB Stream` の後付けと同時にmappingをしようとすると、現状としては `stream_arn` が存在しないのにそれを利用してリソースを設定しようとしているため、Plan の段階でNGになります。
「Planで `stream_arn` を補完してくれればいいのに」とは思いますが、Stream名には作成日時が自動的に入るので、実際に作ってからでないと確定できないんですよね。

... というのが原因と考えられます。
Terraform のソースを見れば原因がハッキリわかるんでしょうが、私にはそこまでのモチベーションは無いです。

## では、どうすれば？
段階を踏んで実行しましょう。
1. `stream_enabled = true` で実行し`DynamoDB Stream` を有効化する。これで次回から `stream_arn` が参照できるようになる。
2. `DynamoDB Stream` の ARN に依存するリソース設定を追加する。

もしかしたら Terraform の実行オプションで回避出来る等、他に方法があるかもしれませんが、私には見つけられませんでした。