TerraformでLambda(Cron起動)を構築する

# はじめに
Lambdaのバッチ実行をCloudWatch Evnetと組み合わせて使うことがよくあると思いますが、
今回はその設定をTerraformを使って構築してみたいと思います。

# 事前準備
Terraform, aws-cliのインストールをお願いします。

```bash
brew install terraform
brew install awscli
```

ちなみに私の環境は下記のようになっています。
macOS Mojave 10.14.6
Terraform 0.12.6
aws-cli 1.16.21

# フォルダ構成説明
```
.
├── main.tf　　　　　　　　→Mainファイル
├── variable.tf　　　　　 →変数定義ファイル
├── terraform.tfvars　　 →外部変数設定ファイル
├── lambda_src         　→Lambdaソース置き場
│   ├── handler.py
│   └── requirements.txt
└── modules　　　　　　　　→AWS各サービスモジュール
    ├── cloudwatch
    │   ├── main.tf
    │   └── variable.tf
    ├── iam
    │   ├── main.tf
    │   └── variable.tf
    └── lambda
        ├── main.tf
        └── variable.tf
```

# Terraform初期設定
AWSアクセスキーとtfstateファイルを管理するS3バケットは事前に作成してください。
アクセスキーは **名前付きプロファイル** を使用すると便利です。

```terraform:main.tf
# Variables are not allowed in this block(このブロックで変数は使えません)
provider "aws" {
  region  = "ap-northeast-1"
  version = "2.24.0"
  profile = "sample-iam"　←ここ
}

# Variables are not allowed in this block(このブロックで変数は使えません)
terraform {
  required_version = ">= 0.12"

  backend "s3" {
    bucket  = "sample-tfstate-bucket"　←ここ
    region  = "ap-northeast-1"
    key     = "terraform.tfstate"
    encrypt = true
    profile = "sample-iam"　←ここ
  }
}
```

```bash
# 初期化
terraform init
```

# Lambda用IAMロール作成
MainファイルでIAMロール作成用のModuleを呼び出し、変数を渡します。
`modules/iam/main.tf`にはLambdaのIAMロールに必要なポリシーが定義されています。

```terraform:main.tf
module "iam" {
  source = "./modules/iam"
  name   = var.app_name
}
```

アプリ名を適宜置き換えてください。

```terraform:terraform.tfvars
app_name = "sample-app"
```

```terraform:modules/iam/main.tf
# Lambda Role
resource "aws_iam_role" "lambda_role" {
  name = "${var.name}-lambda-role"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow"
    }
  ]
}
POLICY
}

# Lambda Policy Data
data "aws_iam_policy_document" "lambda_policy_document" {
  statement {
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    effect    = "Allow"
    resources = ["*"]
  }
}

# Lambda Attach Role to Policy
resource "aws_iam_role_policy" "lambda_role_policy" {
  role   = aws_iam_role.lambda_role.id
  name   = "${var.name}-lambda-policy"
  policy = data.aws_iam_policy_document.lambda_policy_document.json
}

output "lambda_role_arn" {
  value = aws_iam_role.lambda_role.arn
}
```

```terraform:modules/iam/variable.tf
variable "name" {}
```

# Lambda作成
MainファイルでLambda作成用のModuleを呼び出し、変数を渡します。
Lambdaのプログラムは`lambda_src`内に格納してください。フォルダ毎zip化してアップロードされます。
タグや環境変数が必要な場合は変数を追加で記載してください。

```terraform:main.tf
module "lambda" {
  source                = "./modules/lambda"
  name                  = var.app_name
  role_arn              = module.iam.lambda_role_arn
  lambda_source_dir     = "./lambda_src"
  lambda_handler        = "handler.main"
  function_runtime      = "python3.7"
  memory_size           = 128
  timeout               = 30
  log_retention_in_days = 14
  environment_key_1     = "STAGE"
  environment_value_1   = "dev"
}
```

```terraform:modules/lambda/main.tf
# Local Values
locals {
  tag_terraform_value    = "Terraform"
  archive_file_type      = "zip"
  deploy_upload_filename = "lambda_src.zip"
}

# Lambda Function
resource "aws_lambda_function" "lambda" {
  filename         = data.archive_file.lambda_file.output_path
  function_name    = var.name
  role             = var.role_arn
  handler          = var.lambda_handler
  runtime          = var.function_runtime
  source_code_hash = data.archive_file.lambda_file.output_base64sha256
  memory_size      = var.memory_size
  timeout          = var.timeout

  environment {
    variables = {
      "${var.environment_key_1}" = var.environment_value_1
    }
  }

  tags = {
    CreateOwner = local.tag_terraform_value
  }
}

# CloudWatch Logs
resource "aws_cloudwatch_log_group" "log_group" {
  name              = "/aws/lambda/${var.name}"
  retention_in_days = var.log_retention_in_days

  tags = {
    Name        = var.name
    CreateOwner = local.tag_terraform_value
  }
}

# Lambda File Zip
data "archive_file" "lambda_file" {
  type        = local.archive_file_type
  source_dir  = var.lambda_source_dir
  output_path = local.deploy_upload_filename
}

output "arn" {
  value = aws_lambda_function.lambda.arn
}

output "function_name" {
  value = aws_lambda_function.lambda.function_name
}
```

```terraform:modules/lambda/variable.tf
variable "name" {}
variable "lambda_source_dir" {}
variable "role_arn" {}
variable "lambda_handler" {}
variable "function_runtime" {}
variable "memory_size" {}
variable "timeout" {}
variable "log_retention_in_days" {}
variable "environment_key_1" {}
variable "environment_value_1" {}
```

# CloudWatch Event作成
MainファイルでCloudWatch Event作成用のModuleを呼び出し、変数を渡します。
時間の書き方は[AWSドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html)を参照してください。

```terraform:main.tf
module "cloudwatch_evnet" {
  source     = "./modules/cloudwatch"
  name       = var.app_name
  schedule   = "cron(0 0 1 * ? *)"
  lambda_arn = module.lambda.arn
}
```

```terraform:modules/cloudwatch/main.tf
resource "aws_cloudwatch_event_rule" "event_rule" {
  name                = var.name
  schedule_expression = var.schedule
}

resource "aws_cloudwatch_event_target" "event_target" {
  rule = aws_cloudwatch_event_rule.event_rule.name
  arn  = var.lambda_arn
}

output "rule_arn" {
  value = aws_cloudwatch_event_rule.event_rule.arn
}
```

```terraform:modules/cloudwatch/variable.tf
variable "name" {}
variable "schedule" {}
variable "lambda_arn" {}
```

# CloudWatch EventからLambda関数の呼び出しを許可

```terraform:main.tf
resource "aws_lambda_permission" "allow_cloudwatch" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = module.lambda.function_name
  principal     = "events.amazonaws.com"
  source_arn    = module.cloudwatch_evnet.rule_arn
}
```

# Finish
```bash
# 初期化
terraform init
# Dry Run
terraform plan
# 実行
terraform apply
```