GolangのLambda関数をTerraformだけでデプロイする
やりたいこと
GolangのLambda関数をTerraformを使ってデプロイしたいです。普段ならcdkとかslsとかを使いますが、今回のプロジェクトはTerraformでガッツリ他リソースを作成しており、今更Lambda関数ひとつをデプロイするためだけに別ツールを使いたくなかったからです。
さて、GolangのLambda関数をデプロイするためには、以下のステップが必要になります。
- Golangのコードを書く
- ↑をビルドしてバイナリファイルを作成する
- ↑のバイナリファイルをzip圧縮してデプロイパッケージを作成する
- 関数デプロイ時に↑のzipファイルを指定する
Terraformにはaws_lambda_functionというリソースがありますが、これがやってくれるのは最後の関数のデプロイだけです。コードを書いた後このaws_lambda_functionリソースを使うまでの間にビルドとzipファイル化というステップがまだ残っています。
上記ビルドとzipファイル化、そしてデプロイ(=terraform apply)を一気通貫で実施するために、makefileでそれらの処理をラップするという方法があります。以下弊社新井のブログではその方法を採っています。
しかし今回は前述のとおりTerraformですでにガッツリ他リソースを作成しており、今更makefileでラップするのもできればやりたくないなぁと思いました。そこでTerraform内で、つまりterraform apply
コマンドだけでこれらの処理が完結できる構成を模索しました。
全体像
結果、できあがったコードは以下の様になりました。
├── lambdas │ ├── archive │ │ └── .gitignore │ ├── bin │ │ └── .gitignore │ └── cmd │ └── sample │ ├── go.mod │ ├── go.sum │ └── main.go ├── .gitignore ├── .go-version ├── .terraform-version ├── .terraform.lock.hcl ├── go.mod ├── go.sum ├── lambda.tf ├── locals.tf ├── main.tf └── providers.tf
ざっくりとした処理の流れは以下になります。
./lambdas/cmd
以下にコードを書く./lambdas/bin
以下にバイナリファイルを作成する./lambdas/archive
以下にzipファイル(=デプロイパッケージ)を作成する- デプロイパッケージをS3にアップロードする
- アップロードしたデプロイパッケージを指定してLambda関数をデプロイ
2〜5までが terraform apply
でできます。かつ2回目以降の実行ではコードに変更があったときだけこれら(2〜5)の処理が走ります。
動作環境
- MacBook Pro (13-inch, M1, 2020)
- macOs Big Sur v11.6.5
- Terraform
- v1.1.9
- aws provider
- v4.11.0
- null provider
- v3.1.1
- Golang
- v1.16.15
- 以下コマンドが使える
- aws cli
- openssl
- zip
GitHub
以下で中身を解説していきます。
Lambda関数コード
Golangのコードの中身自体は今回は特に重要ではないので、公式Lambda デベロッパーガイドに出てくる以下のコードを使いました。
package main import ( "fmt" "context" "github.com/aws/aws-lambda-go/lambda" ) type MyEvent struct { Name string `json:"name"` } func HandleRequest(ctx context.Context, name MyEvent) (string, error) { return fmt.Sprintf("Hello %s!", name.Name ), nil } func main() { lambda.Start(HandleRequest) }
Resource: aws_lambda_function
まずはこの構成のゴールとなるLambda関数のデプロイを担うaws_lambda_functionリソースについて説明します。
resource "aws_lambda_function" "sample" { function_name = "sample" s3_bucket = aws_s3_bucket.lambda_assets.bucket s3_key = data.aws_s3_object.golang_zip.key role = aws_iam_role.iam_for_lambda.arn handler = "sample" source_code_hash = data.aws_s3_object.golang_zip_hash.body runtime = "go1.x" timeout = "10" }
ここで説明したい点は2点です。
デプロイパッケージの指定
aws_lambda_functionではデプロイパッケージの指定方法が2種類、ローカル上に存在するものを使う方法と、S3にアップされているものを使う方法があります。今回は後者を採用しました。
ローカルの方法の方がS3へのアップロードが不要な分簡単で、ググるとそちらを使っている例の方が多いと思います。ですが今回、
- ビルドしたバイナリファイルやzipのデプロイパッケージをGit管理外にする
という要件があり、この要件を踏まえると以下いずれかが回避できなさそうだったので、S3を使う方法を採用しました。
- ローカルにバイナリやzipファイルがある/ない状況どちらもありえて、これをうまくハンドリングできなかった(エラーになるパターンがある)
- コードに変更がなくても毎度ビルドとzip化が実行される =ムダ
S3を使う方法はs3_bucket
,s3_key
attributeの指定が必須で、オプションでs3_object_version
も指定できます。逆にローカルファイルを指定する場合はfilename
を使います。
source_code_hash
このattributeは関数の再デプロイのトリガーに使われます。この値が更新されると再デプロイされます。
Must be set to a base64-encoded SHA256 hash of the package file
という要件があります。
ローカルのデプロイパッケージを使う場合は filebase64sha256("file.zip")
とか base64sha256(file("file.zip"))
といった方法が使えます。
また、ローカルのデプロイパッケージを使い、さらにzip化までTerraform管理内にする場合は Data Sources: archive_file が使えて、このAttributes Referenceにoutput_base64sha256
というのがあるので、 source_code_hash = data.archive_file.deploy_package.output_base64sha256
みたいな書き方が使えます。
しかしながら、もう一方の方法、S3にアップされているファイルを使う方法の場合の指定方法が調べても中々見つからず、苦労しました。。最終的にbase64-encoded SHA256 hashの結果だけを格納したファイルを作成して、それをS3にアップし参照するという方法に落ち着きました。この点は後述します。
デプロイパッケージアップロード用S3バケット
前述したとおりデプロイパッケージをS3にアップロードする必要があるので、それ用のS3バケットをプロビジョニングします。セキュリティ面の最低限の設定を加えています。
resource "aws_s3_bucket" "lambda_assets" {} resource "aws_s3_bucket_acl" "lambda_assets" { bucket = aws_s3_bucket.lambda_assets.bucket acl = "private" } resource "aws_s3_bucket_server_side_encryption_configuration" "lambda_assets" { bucket = aws_s3_bucket.lambda_assets.bucket rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } resource "aws_s3_bucket_public_access_block" "lambda_assets" { bucket = aws_s3_bucket.lambda_assets.bucket block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true }
デプロイパッケージを参照する Datasource: aws_s3_bucket_objects
前述のaws_lambda_function.s3_key
で参照されているdata sourceです。
depends_on = [null_resource.lambda_build]
を指定しているのがポイントです。後述しますがこのnull_resource.lambda_build
内でこのdata sourceで参照しているS3オブジェクトを作成しています。ですので、null_resource.lambda_build
の実行がこのdata sourceの参照より先に行われることを保証するためにdepends_on
が必要なのです。またこのdepends_on
のおかげでnull_resource.lambda_build
が再実行される度に参照も再度行われます。
data "aws_s3_object" "golang_zip" { depends_on = [null_resource.lambda_build] bucket = aws_s3_bucket.lambda_assets.bucket key = local.golang_zip_s3_key // "archive/sample.zip" }
ビルド、zip化、S3アップロードをnull_resourceでやる
null_resource
はちょっと特殊なリソースで、簡単にいうと指定したコマンドを実行するリソースです。 provisioner "local-exec"
というブロック内で書いたコマンドはローカルで実行され、provisioner "remote-exec"
ブロック内に書いたコマンドは外部サーバー上で実行されます。(接続設定が別途必要です)
local-exec
今回以下のように3つprovisioner "local-exec"
ブロックを定義していて、それぞれ上からビルド、zip化(デプロイパッケージ作成)、S3へのアップロードを行なっています。(provisioner "local-exec"
ブロックは他にもあと2つありますが、こちらは後述します。)
provisioner "local-exec" { command = "GOARCH=amd64 GOOS=linux go build -o ${local.golang_binary_local_path} ${local.golang_codedir_local_path}/*.go" } provisioner "local-exec" { command = "zip -j ${local.golang_zip_local_path} ${local.golang_binary_local_path}" } provisioner "local-exec" { command = "aws s3 cp ${local.golang_zip_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.golang_zip_s3_key}" }
locals { golang_codedir_local_path = "${path.module}/lambdas/cmd/sample" golang_binary_local_path = "${path.module}/lambdas/bin/sample" golang_zip_local_path = "${path.module}/lambdas/archive/sample.zip" golang_zip_s3_key = "archive/sample.zip" }
AWS認証設定についての注意点
最後のS3へのアップロード処理でaws cliを使っています。もしTerraform aws providerの認証設定を例えば以下のようにprovider "aws"
の中でやっている場合、
provider "aws" { shared_config_files = ["/Users/tf_user/.aws/conf"] shared_credentials_files = ["/Users/tf_user/.aws/creds"] profile = "customprofile" }
このクレデンシャル設定は上記 null_resource内でやっているaws cliでは使われませんのでご注意ください。provider "aws"
で定義した認証設定はaws providerの各resource / data sourceとやり取りするために使われるものであり、null provider管轄のnull_resourceとは無関係です。
AWS_PROFILE
やAWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
といった環境変数でprovider "aws"
外で認証設定している場合は、その認証がaws providerでもnull_resource内のaws cliコマンドでも使われるでしょう。
triggers
null_resource内で定義されたコマンドが実行されるタイミングは以下2つです。
- 初回、リソース作成時
- triggers attribute以下のいずれかの値が更新された際
今回triggers attributeは以下のように設定しています。
triggers = { code_diff = join("", [ for file in fileset(local.golang_codedir_local_path, "*.go") : filebase64("${local.golang_codedir_local_path}/${file}") ]) }
./lambdas/cmd
以下においたgolangのファイルすべてからハッシュを生成しています。コードに何かしら変更があった場合このハッシュ値が変わるので、コマンドが再実行つまりビルド、zip化、S3アップロードの処理が走ります。
aws_lambda_function.source_code_hash
provisioner "local-exec"
のまだ説明していない残りの2つは、aws_lambda_function.source_code_hash
値を更新して、Lambda関数再デプロイを走らせるための前処理です。
provisioner "local-exec" { command = "openssl dgst -sha256 -binary ${local.golang_zip_local_path} | openssl enc -base64 | tr -d \"\n\" > ${local.golang_zip_base64sha256_local_path}" } provisioner "local-exec" { command = "aws s3 cp ${local.golang_zip_base64sha256_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.golang_zip_base64sha256_s3_key} --content-type \"text/plain\"" }
source_code_hash
値は「base64-encoded SHA256 hash of the package file」である必要があるとResource: aws_lambda_functionの項でご説明しました。1つ目のprovisioner "local-exec"
でこのハッシュ値を作成してファイル出力しています。そして2つ目でS3にアップロードしています。
アップロードしたファイルは以下Data Sourceで参照します。これもdepends_on = [null_resource.lambda_build]
を入れることで、必ず S3にファイルがアップロードされた後に参照されるようにしています。
data "aws_s3_object" "golang_zip_hash" { depends_on = [null_resource.lambda_build] bucket = aws_s3_bucket.lambda_assets.bucket key = local.golang_zip_base64sha256_s3_key }
そして、aws_lambda_functionにて上記Data Sourceを参照します。aws_s3_object Data Sourceにはbodyというfieldがあるので、オブジェクトの中身を使えるというわけです。
resource "aws_lambda_function" "sample" { function_name = "sample" s3_bucket = aws_s3_bucket.lambda_assets.bucket s3_key = data.aws_s3_object.golang_zip.key role = aws_iam_role.iam_for_lambda.arn handler = "sample" source_code_hash = data.aws_s3_object.golang_zip_hash.body runtime = "go1.x" timeout = "10" }
ポイントとしては、null_resource内でaws s3 cp
コマンドでS3にハッシュ文字列を格納したファイルをアップロードする際に、Content Typeを指定している点です。 aws_s3_object Data Sourceでbody値を参照するには、該当オブジェクトのContent Typeがhuman-readableなもの(text/*
もしくは application/json
) である必要があるためです。
Note: The content of an object (
body
field) is available only for objects which have a human-readableContent-Type
(text/*
andapplication/json
). This is to prevent printing unsafe characters and potentially downloading large amount of data which would be thrown away in favour of metadata.
指定なしでアップロードするとContent Typeがbinary/octet-stream
になってしまい、bodyフィールドはnullを返していました。