既存AWS構成をTerraformでIaC化（CloudWatch, Lambda, IAM）

## はじめに
今回は前回の続きです。Lambda部分をTerraformでIaC化していきます。
前回ではVPC回りとAPI Gateway構成部分をTerraformでIaC化しました。以下がその記録です。
↓

https://qiita.com/kennyQiita/items/efa452ca81528acb8270

ちなみにIaC化する前提構成は前回と同じままです（以下図）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/bc17bb9b-74f2-e029-3214-bace0963c74b.png)

今回の実施内容としては以下となります。
* CloudWatchLogs管理をTerraform管理に変更
* Lambdaで利用するIAMロールをTerraform管理に変更
* LambdaをTerraform管理に変更

## IaC化
### CloudWatchLogs
Lambdaに関わるCloudWatchLogsのロググループだけでなく、本構成で必要なロググループをIaC化しました。
```terraform:aws_pubsub_cw.tf
# #----------------------------------------
# # Authorizer Lambda Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "AuthForPubSub" {
  name              = "/aws/lambda/AuthForPubSub"
  retention_in_days = 90
  skip_destroy      = false
}

# #----------------------------------------
# # Fargate Task Run Lambda Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "deliverTopicToSNS_Test01" {
  name              = "/aws/lambda/deliverTopicToSNS_Test01"
  retention_in_days = 90
  skip_destroy      = false
}

# #----------------------------------------
# # Subscriber Lambda Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "subscribeTopicFromSNS_Test01" {
  name              = "/aws/lambda/subscribeTopicFromSNS_Test01"
  retention_in_days = 90
  skip_destroy      = false
}

# #----------------------------------------
# # DynamoDB Stream Triggered Lambda Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "getDataFromDynamodbStream" {
  name              = "/aws/lambda/getDataFromDynamodbStream"
  retention_in_days = 90
  skip_destroy      = false
}

# #----------------------------------------
# # Fargate Publisher Log Group
# #----------------------------------------
resource "aws_cloudwatch_log_group" "task-pubsub-pub" {
  name              = "/ecs/task-pubsub-pub"
  retention_in_days = 90
  skip_destroy      = false
}
```

### IAM
続いてIAMのPolicyとRoleの設定部分をIaC管理できるようにしていきます。
もともとはインラインポリシーとして利用していた`LambdaRunEcsTaskPolicy`,`PutItemToSnsSqsLambdaTable`をカスタマーマネージドポリシーとして作成し、それらのポリシーも含めて`Custom_Lambda_Basic_Execution_Role`にアタッチしています。
```terraform:aws_pubsub_iam.tf
# #----------------------------------------
# # IAM Policy for Lambda Execution Role
# #----------------------------------------
resource "aws_iam_policy" "LambdaRunEcsTaskPolicy" {
  name        = "LambdaRunEcsTaskPolicy"
  path        = "/"
  description = "Allow Lambda to run ECS Task"

  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Sid" : "VisualEditor0",
        "Effect" : "Allow",
        "Action" : [
          "iam:PassRole",
          "ecs:RunTask"
        ],
        "Resource" : "*"
      }
    ]
  })
}

resource "aws_iam_policy" "PutItemToSnsSqsLambdaTable" {
  name        = "PutItemToSnsSqsLambdaTable"
  path        = "/"
  description = "Allow Lambda to putItem to DynamoDB sns_sqs_lambda_table"

  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Sid" : "VisualEditor0",
        "Effect" : "Allow",
        "Action" : "dynamodb:PutItem",
        "Resource" : "arn:aws:dynamodb:ap-northeast-1:<アカウントID>:table/sns_sqs_lambda_table*"
      }
    ]
  })
}

# #----------------------------------------
# # IAM Role: Lambda Execution Role
# #----------------------------------------
resource "aws_iam_role" "Custom_Lambda_Basic_Execution_Role" {
  name = "Custom_Lambda_Basic_Execution_Role"
  assume_role_policy = jsonencode(
    {
      Statement = [
        {
          Action = "sts:AssumeRole"
          Effect = "Allow"
          Principal = {
            Service = "lambda.amazonaws.com"
          }
        },
      ]
      Version = "2012-10-17"
    }
  )
  description = "Lambda Role for PubSub Application"
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/AWSLambdaExecute",
    "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess",
    "arn:aws:iam::aws:policy/AmazonSNSFullAccess",
    "arn:aws:iam::aws:policy/CloudWatchFullAccess",
    "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole",
    aws_iam_policy.LambdaRunEcsTaskPolicy.arn,
    aws_iam_policy.PutItemToSnsSqsLambdaTable.arn
  ]
}
```
### Lambda
続いてLambdaです。これまではコンソールにコードを直接書いてデプロイしてましたが、今回のIaC化に合わせて、zipファイルを作成してzipファイルをデプロイする方式に変更しました。
LambdaをTerraformでIaC化する際の大まかな手順イメージは以下のようになるかと思います。
1. `data "archive_file"`でzipファイルを作成
1. Lambdaコードをデプロイ元の環境に転記（今回の場合はコンソール⇒Cloud9へ転記）
1. `resource "aws_lambda_function"`でzipファイルをデプロイするリソースを作成
1. 必要に応じて不要な部分を削除（既存のdataリソース部分など）

一つのファイルにまとめて記載しています。
```terraform:aws_pubsub_lambda.tf
# #----------------------------------------
# # Authorizer Lambda Function
# #----------------------------------------
# data "aws_lambda_function" "AuthForPubSub" {
#   function_name = "AuthForPubSub"
# }

# #----------------------------------------
# # Publisher Lambda Function
# #----------------------------------------
# data "aws_lambda_function" "deliverTopicToSNS_Test01" {
#   function_name = "deliverTopicToSNS_Test01"
# }

# #----------------------------------------
# # Create zip file
# #----------------------------------------
data "archive_file" "AuthForPubSub" {
  type        = "zip"
  source_file = "lambda_functions/src/auth_for_pubsub.py"
  output_path = "lambda_functions/output/auth_for_pubsub.zip"
}

data "archive_file" "deliverTopicToSNS_Test01" {
  type        = "zip"
  source_file = "lambda_functions/src/deliver_topic_sns.py"
  output_path = "lambda_functions/output/deliver_topic_sns.zip"
}

data "archive_file" "subscribeTopicFromSNS_Test01" {
  type        = "zip"
  source_file = "lambda_functions/src/subscribe_topic_sns.py"
  output_path = "lambda_functions/output/subscribe_topic_sns.zip"
}

data "archive_file" "getDataFromDynamodbStream" {
  type        = "zip"
  source_file = "lambda_functions/src/get_data_from_dynamodb_stream.py"
  output_path = "lambda_functions/output/get_data_from_dynamodb_stream.zip"
}

# #----------------------------------------
# # Deply Lambda Configuration
# #----------------------------------------
resource "aws_lambda_function" "AuthForPubSub" {
  filename         = data.archive_file.AuthForPubSub.output_path
  function_name    = "AuthForPubSub"
  handler          = "auth_for_pubsub.lambda_handler"
  description      = "Authorizer Lambda to check header token."
  role             = aws_iam_role.Custom_Lambda_Basic_Execution_Role.arn
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 5
  source_code_hash = data.archive_file.AuthForPubSub.output_base64sha256
  tags = {
    "lambda-console:blueprint" = "api-gateway-authorizer-python"
  }
}

resource "aws_lambda_function" "deliverTopicToSNS_Test01" {
  filename         = data.archive_file.deliverTopicToSNS_Test01.output_path
  function_name    = "deliverTopicToSNS_Test01"
  handler          = "deliver_topic_sns.lambda_handler"
  description      = "run ECS fargate task to deliver sns topic."
  role             = aws_iam_role.Custom_Lambda_Basic_Execution_Role.arn
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 5
  source_code_hash = data.archive_file.deliverTopicToSNS_Test01.output_base64sha256
  environment {
    variables = {
      "ECS_CLUSTER"     = "pubsub-cluster-pub"
      "SUBNET_ID_1"     = "subnet-0a8e33a542ddb7443"
      "SUBNET_ID_2"     = "subnet-02ba1bf978801ab20"
      "TASK_DEFINITION" = "task-pubsub-pub:2"
    }
  }
}

resource "aws_lambda_function" "subscribeTopicFromSNS_Test01" {
  filename         = data.archive_file.subscribeTopicFromSNS_Test01.output_path
  function_name    = "subscribeTopicFromSNS_Test01"
  handler          = "subscribe_topic_sns.lambda_handler"
  description      = "subscribe message from sqs and put new item into DynamoDB."
  role             = aws_iam_role.Custom_Lambda_Basic_Execution_Role.arn
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 5
  source_code_hash = data.archive_file.subscribeTopicFromSNS_Test01.output_base64sha256
}

resource "aws_lambda_function" "getDataFromDynamodbStream" {
  filename         = data.archive_file.getDataFromDynamodbStream.output_path
  function_name    = "getDataFromDynamodbStream"
  handler          = "get_data_from_dynamodb_stream.lambda_handler"
  description      = "get new item info from dynamodb stream and publish a new sns topic for email."
  role             = aws_iam_role.Custom_Lambda_Basic_Execution_Role.arn
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 5
  source_code_hash = data.archive_file.getDataFromDynamodbStream.output_base64sha256
}
```
まずは`# Create zip file`のセクションでデプロイするzipファイルを作成する内容を記載しています。zip化される実際の関数コードについては別ファイルに記載（転記）してます（ソースコードは以下に載せてます）。
次にデプロイするLambda関数の設定などを記載する`# Deploy Lambda Configuration`セクションでLambdaの実行時の設定などを記載しています。
また、前回までのIaC化ではLambda関数は参照部分のみTerraformで記載してたので、`data "aws_lambda_function"`を使っていましたが、今回でLambda自身もTerraformで管理するように変更したので、該当部分（コード一番上部分）はコメントアウトしています。

以下がLambda関数のコードを記載しているファイルになります。`lambda_functions/src`配下としました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/05bdbb10-c6e4-68b4-430f-0f7bbd6d75de.png)

例えば`auth_for_pubsub.py`の中身は以下のようになっています。
```python:auth_for_pubsub.py
import base64
import json
from logging import getLogger, INFO

logger = getLogger(__name__)
logger.setLevel(INFO)

def lambda_handler(event, context):
    print("============ output event =================")
    logger.info(json.dumps(event))
    
    token = event['headers']['Authorization']
    effect = 'Deny'
    if token == 'abc':
        effect = 'Allow'
        
    context = {
        "overwrite_message": "this is override message from authorizer."
    }
    
    json_context = json.dumps(context)
    base64_context = base64.b64encode(json_context.encode('utf8'))
        
    return_policy = {
        'principalId': '*',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': '*',
                    'Effect': effect,
                    'Resource':  event['methodArn']
                }    
            ]
        },
        'context': {
            'overwrite_message': base64_context.decode('utf-8')
        }
        
    }
    
    logger.info(json.dumps(return_policy))
    return return_policy
```
他のpythonファイルについてもgithubにアップ予定ですので、興味あるかたはそちらを参照してください。アップが完了したら記事の最下部にリンクを足しておきます。

そして、前回まではAPI GatewayのTerraform記載部分で`data.aws_lambda_function`としてLambda関数を紐づけていましたが、今回Lambda関数もTerraform管理としたので、該当部分を`aws_lambda_function`としTerraformリソースを参照するように修正しています。
```terraform:aws_pubsub_apigw.tf
# (一部抜粋)
resource "aws_api_gateway_authorizer" "pubsubApi" {
  name        = "PubSubLambdaAuthorizer"
  rest_api_id = aws_api_gateway_rest_api.pubsubApi.id
  # authorizer_uri                   = data.aws_lambda_function.AuthForPubSub.invoke_arn
  authorizer_uri                   = aws_lambda_function.AuthForPubSub.invoke_arn
  authorizer_result_ttl_in_seconds = 0
  type                             = "REQUEST"
}

...

resource "aws_api_gateway_integration" "pubsubApi" {
  rest_api_id = aws_api_gateway_rest_api.pubsubApi.id
  resource_id = aws_api_gateway_resource.pubsubApi.id
  http_method = "POST"
  type        = "AWS_PROXY"
  # uri                     = data.aws_lambda_function.deliverTopicToSNS_Test01.invoke_arn
  uri                     = aws_lambda_function.deliverTopicToSNS_Test01.invoke_arn
  content_handling        = "CONVERT_TO_TEXT"
  integration_http_method = "POST"
}
```
以上で今回やろうとしていたIaC化部分は完了しました。

## おわりに
Lambda関数をTerraformで管理する場合は環境内にzipを作成する手法（今回はこちらを採用）とS3へzipをアップロードしてそこからデプロイする手法が主に考えられると思います。実際に複数人でより多くのLambda関数を開発・管理していく場合はS3を経由する方法が現実的かもしれません。
また、Lambda関数Terraform化する際に、Lambdaで間接的に利用するAWSリソース（CWログとかIAMとか）を先にTerraformで記載しておいた方が、Lambda自身をTerraformで記載する際にスムーズになるとも思いました。
次回以降、まだIaC化できていない他の部分を実施していきたいと思います。

## 参考サイト

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function

https://tech.crassone.jp/posts/terraform_aws_lambda