TerraformでAPI Gateway + コンテナのLambdaを作成してみる

`terraform apply` コマンドだけでコンテナイメージを作成して、API GatewayとLambdaを作成できるようにします。コンテナイメージの更新時も `terraform apply` だけで済むように内部で `aws lambda update-function-code` コマンドを呼び出しています。前回の記事と前々回の記事の内容の合体プラスアルファです。

前回：[TerraformでECRのリポジトリを作成しdocker build & pushする - Qiita](https://qiita.com/suzuki-navi/items/613311d1a31d0306be0d)
前々回：[TerraformでAPI Gateway + Lambdaの最小構成テンプレート - Qiita](https://qiita.com/suzuki-navi/items/6a896a6577deaa858210)

Terraformのファイル本体は以下の通りです。

コンテナイメージのソースとなるファイルが更新されたらdocker build & pushされるようにするのと、 `aws lambda update-function-code` コマンドを呼ぶようにしています。ソースファイルの更新は `docker_source_file_sha1` で検知しています。

```main.tf
variable aws_profile {}
variable aws_region {}
variable resource_name {}

terraform {
  backend "s3" {
  }
}

provider "aws" {
   profile = var.aws_profile
   region = var.aws_region
}

################################################################################
# ECR
################################################################################

resource "aws_ecr_repository" "default" {
  name = var.resource_name
  force_delete = true # terraform destroyしたときにリポジトリも削除されるように
}

################################################################################
# docker push
################################################################################

locals {
  # Dockerイメージのソースとなるファイルのハッシュ値
  docker_source_file_sha1 = sha1(join("", [for f in ["build-docker.sh", "Dockerfile", "app.js", "package.json"]: filesha1(f)]))
}

resource "null_resource" "image" {
  depends_on = [
    aws_ecr_repository.default
  ]

  triggers = {
    file_content_sha1 = local.docker_source_file_sha1
  }

  provisioner "local-exec" {
    # docker build & push
    command = "sh ./build-docker.sh"
  }
}

################################################################################
# Lambda
################################################################################

resource "aws_iam_role" "lambda_role" {
  name               = "${var.resource_name}-lambda-role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

data "aws_iam_policy_document" "lambda_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_lambda_function" "api" {
  depends_on       = [
    aws_iam_role.lambda_role,
    null_resource.image,
  ]
  function_name    = "${var.resource_name}-api"
  role             = aws_iam_role.lambda_role.arn
  package_type     = "Image"
  image_uri        = "${aws_ecr_repository.default.repository_url}:latest"
  timeout          = 30
}

resource "null_resource" "refresh_lambda" {
  depends_on = [
    aws_lambda_function.api,
    null_resource.image,
  ]

  triggers = {
    # イメージを更新したときに新しいイメージでLambdaを更新するためのトリガー
    file_content_sha1 = local.docker_source_file_sha1
  }

  provisioner "local-exec" {
    command = "sh ./refresh-lambda.sh"
    # スクリプトの中身は aws lambda update-function-code を呼び出している
  }
}

################################################################################
# API Gateway
################################################################################

resource "aws_iam_role" "api_gateway_role" {
  name               = "${var.resource_name}-apigateway-role"
  assume_role_policy = data.aws_iam_policy_document.api_gateway_assume_role.json
}

resource "aws_iam_role_policy_attachment" "api_gateway_policy_logs" {
  role       = aws_iam_role.api_gateway_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
}

resource "aws_iam_role_policy_attachment" "api_gateway_policy_lambda" {
  role       = aws_iam_role.api_gateway_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaRole"
}

data "aws_iam_policy_document" "api_gateway_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      type        = "Service"
      identifiers = ["apigateway.amazonaws.com"]
    }
  }
}

resource "aws_api_gateway_rest_api" "api" {
  name = "${var.resource_name}-api"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
  
  body = jsonencode({
    openapi = "3.0.1"
    info = {
      title   = "api"
      version = "1.0"
    }
    paths = {
      "/" = {
        get = {
          x-amazon-apigateway-integration = {
            httpMethod           = "POST"
            payloadFormatVersion = "1.0"
            type                 = "AWS_PROXY"
            uri                  = aws_lambda_function.api.invoke_arn
            credentials          = aws_iam_role.api_gateway_role.arn
          }
        }
      }
      "/{proxy+}" = {
        get = {
          x-amazon-apigateway-integration = {
            httpMethod           = "POST"
            payloadFormatVersion = "1.0"
            type                 = "AWS_PROXY"
            uri                  = aws_lambda_function.api.invoke_arn
            credentials          = aws_iam_role.api_gateway_role.arn
          }
        }
      }
    }
  })
}

resource "aws_api_gateway_deployment" "deployment" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  depends_on  = [aws_api_gateway_rest_api.api]
  stage_name  = "prod"
  triggers = {
    redeployment = sha1(jsonencode(aws_api_gateway_rest_api.api))
  }
}

output "invoke_url" {
  value = aws_api_gateway_deployment.deployment.invoke_url
}

################################################################################
```

Terraformの変数定義の例です。

```terraform.tfvars
aws_profile="default"
aws_region="ap-northeast-1"
resource_name="lambda-container-sample"
```

Docker build & pushしている `build-docker.sh` は前回の記事とまったく同じです。

よく似たスクリプトとしてLambdaにコンテナイメージの更新を強制させるスクリプトとして `refresh-lambda.sh` を作成しています。

```refresh-lambda.sh
#!/bin/bash

. ./terraform.tfvars

aws_account_id=$(aws --profile $aws_profile sts get-caller-identity --query 'Account' --output text)

lambda_function_name=${resource_name}-api

aws --profile $aws_profile --region $aws_region lambda update-function-code --function-name $lambda_function_name --image-uri $aws_account_id.dkr.ecr.$aws_region.amazonaws.com/$resource_name:latest
```

Dockerfile等、コンテナイメージを構成するファイルには、サンプルとして最低限のことだけ書いています。

```Dockerfile
FROM public.ecr.aws/lambda/nodejs:18

COPY app.js package.json  ${LAMBDA_TASK_ROOT}/

RUN npm install

CMD [ "app.lambda_handler" ]
```

```app.js
exports.lambda_handler = async (event, context) => {
  return new Promise(function(resolve, reject) {
    resolve({
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json",
        },
        "body": JSON.stringify({message: "OK", event: event}),
    });
    return;
  });
};
```

```package.json
{
  "name": "lambda-container-sample",
  "version": "0.1.0",
  "dependencies": {}
}
```

以上。

## 追記 2023/06/19

この記事のコードは、以下の記事でも活用しました。

https://www.beex-inc.com/blog/slackbot-aws-lambda