AWS LambdaにGo言語のスクリプトをDockerイメージでデプロイする [Terraformを使用]

## 要約
最終的なソースコード全体は以下のリンクからご確認いただけます。

https://github.com/mochi-yu/qiita-material/tree/main/golang-lambda-by-docker-with-terraform

## 関連記事
Go言語で記述したスクリプトを、**AWSのLambdaにTerraformを用いてデプロイする**機会があり、その過程の試行錯誤を整理して一連の記事にまとめました。

この記事はそのうちの2本目として、Dockerイメージを用いてLambdaにスクリプトをデプロイしてみたいと思います。

- [zipファイルを用いてGo言語の処理をAWS Lambdaにデプロイする](https://qiita.com/mochi_2225/items/1b645f87148899f1bc09)
- **Dockerイメージを用いてGo言語の処理をAWS Lambdaにデプロイする**（この記事）
- [EventBridgeからAWS Lambdaを定期実行する処理をデプロイする](https://qiita.com/mochi_2225/items/e41d1f2b87e2bd886790)

## 背景
以前の記事でAWS Lambdaの処理を**zipファイルを用いてデプロイする**方法を実施しました。その際に、Lambdaのデプロイ方法は他に**Dockerイメージを用いた方法**もあるという話をしました。

私自身、普段の開発やECSへデプロイする場合などはDockerを使うことが多いので、LambdaのデプロイもDockerで済ませられたら便利だなと思います。そこで、Terraformを用いてDockerのイメージでLambdaをデプロイできるようにしたいと思います。

なお、注意点として、zipファイルをアップロードしてバイナリが直接実行場合と比べて、DockerでLambdaをデプロイした場合、**多少実行時間が長くなる**ようです。これは、皆さんの求める要件に合わせてご検討ください。

## 実装
フォルダ構成は以下の様になっています。

zipファイルの時と比べて大きく変わったのは、`Dockerfile`が追加されたのと、`ecr.tf`が追加されています。逆に`src/*`と、`main.tf`は一才変わっていません。

```
.
├── .gitignore
├── docker
│   └── Dockerfile
├── image_digest.txt
├── src
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── terraform
    ├── .gitignore
    ├── .terraform.lock.hcl
    ├── ecr.tf
    ├── lambda-assume-role.json
    ├── lambda.tf
    ├── main.tf
    └── terraform.tfstate
```

### Dockerイメージの用意
[AWSの公式のドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/go-image.html)を参考に、以下の様に`Dockerfile`を用意します。私の場合は`./docker/Dockerfile`に配置しました。

中身としては標準的な**マルチステージビルド**の記述です。初めにビルドしてバイナリを用意し、AWSの用意しているLambda用のベースイメージ`provided:al2023`で実行するようにしています。

```docker:./docker/Dockerfile
################## ビルドステージ ##################
FROM golang:1.22 as build

WORKDIR /app

COPY ./src/go.mod ./src/go.sum ./
RUN go mod download

COPY ./src .
RUN go build -tags lambda.norpc -o main main.go

################## 実行ステージ ##################
FROM public.ecr.aws/lambda/provided:al2023

COPY --from=build /app/main ./main
ENTRYPOINT [ "./main" ]
```

### DockerイメージをアップロードするECRの設定
DockerイメージでのLambdaへのデプロイは、事前にビルドしたDockerイメージを**AWSのECRというリポジトリにアップロード**し、**そこのURIを指定**することで行います。

今回はそのECRのリポジトリ自体も、Terraformによる設定でつくります。**リポジトリ名は各自のものに変更してください**。

:::note warn
ECRの**ストレージ容量に対する課金**にはご注意ください。使わないときは、ECRにアップロードしたイメージを消しましょう。
:::

```terraform:./terraform/ecr.tf
locals {
  ecr_repository_name = "test-lambda"  # FIXME
}

resource "aws_ecr_repository" "test_lambda" {
  name = local.ecr_repository_name
}
```

### 変数の用意
変数は以下の様になっています。今回も、デプロイイメージのハッシュ値（ダイジェスト）を保持しておくためにS3を利用しているため、**各自のS3のバケット名**が必要になります。

```terraform:terraform/lambda.tf（一部）
locals {
  s3_bucket = "hoge"  # FIXME
  s3_key_prefix = "test-lambda"
  s3_base_path = "${local.s3_bucket}/${local.s3_key_prefix}"

  golang_codedir = "${path.module}/../src"

  hash_file_name = "image_digest.txt"
}
```

### イメージのビルドとアップロード
このブロックでは、zip化の時と同様に`provisioner "local-exec"`のブロックを用いてローカルでコマンドを実行し、**Dockerイメージのビルド**、**ECRへのアップロード**、**ハッシュ値（ダイジェスト）の取得とS3へのアップロード**といった処理を実施しています。

イメージのビルドとECRへのプッシュは、一般的なECRのユースケースとも同じであるため説明は省きます。Lambdaにおける実行リソースの置き換えをトリガーするためのハッシュ値には、**コンテナのダイジェスト**を使用しています。

ダイジェストの取得は`docker inspect`コマンドを用いて行い、結果をテキストファイルに保存しS3へアップロードしています。

```terraform:terraform/lambda.tf（一部）
resource "null_resource" "lambda_build" {
  depends_on = [ aws_ecr_repository.test_lambda ]

  triggers = {
    code_diff = sha256(join("", [
      for file in fileset(local.golang_codedir, "*")
      : filesha256("${local.golang_codedir}/${file}")
    ]))
  }

  # イメージのビルド
  provisioner "local-exec" {
    command = "cd ${path.module}/.. && docker build . -f docker/Dockerfile --platform linux/amd64 -t ${aws_ecr_repository.test_lambda.repository_url}:latest"
  }

  # イメージをECRへプッシュ
  provisioner "local-exec" {
    command = "aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${aws_ecr_repository.test_lambda.repository_url} && docker push ${aws_ecr_repository.test_lambda.repository_url}:latest"
  }

  # ハッシュの生成
  provisioner "local-exec" {
    command = "cd ${path.module}/.. && docker inspect --format='{{index .RepoDigests 0}}' ${aws_ecr_repository.test_lambda.repository_url}:latest > ${local.digest_file_name}"
  }

  # ハッシュのs3へのアップロード
  provisioner "local-exec" {
    command = "cd ${path.module}/.. && aws s3 cp ${local.digest_file_name} s3://${local.s3_base_path}/${local.digest_file_name} --content-type \"text/plain\""
  }
}
```

### Lambda本体の設定
最後にLambda本体の設定です。ロールポリシーなどはzipの時と変わっていません。`resource "aws_lambda_function"`の中で`image_uri`にイメージのURIを指定し、`package_type`も`"Image"`としています。また、`runtime`や`handler`を指定するとエラーになったため記述を省いています。

```terraform:terraform/lambda.tf（一部）
resource "aws_lambda_function" "test_lambda" {
  function_name    = "test-lambda"
  package_type     = "Image"
  image_uri        = "${aws_ecr_repository.test_lambda.repository_url}:latest"
  role             = aws_iam_role.lambda_role.arn
  source_code_hash = base64sha256(data.aws_s3_object.image_hash.body)
}

resource "aws_iam_role" "lambda_role" {
  name = "role-for-test_lambda"
  assume_role_policy = file("lambda-assume-role.json")
}
```

```json:terraform/lambda-assume-role.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Sid": "",
      "Effect": "Allow"
    }
  ]
}
```

### Lambdaのデプロイ
ここまできたら前回と同様に`terraform apply`でデプロイします。前回と同じようにLambdaがデプロイされ、実行されているかと思います。

今回は、ECRのような**課金がすぐ行われるリソースを使用**しているため、**使わないときは忘れずに`terraform destroy`を実行しておきましょう**。

:::note warn
**ECRにイメージが残っている場合**、`terraform destroy`でも**ECRのリポジトリが削除されない**場合があります。その際は**手動でECRに残っているイメージを消してから**、`terraform destroy`を再度実行してください。
:::

## 終わりに
zip化してデプロイする方法と比べて、ビルドなどのコマンドがDockerfileにまとめられているのは便利だなと感じました。処理の手順もシンプルで、個人的にはzipでデプロイするよりもDockerイメージを使う方が好きかなーといった印象でした。

## 参考

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/go-image.html

https://qiita.com/neruneruo/items/d395fef4929c9486ec0a