要約
最終的なソースコード全体は以下のリンクからご確認いただけます。
関連記事
Go言語で記述したスクリプトを、AWSのLambdaにTerraformを用いてデプロイする機会があり、その過程の試行錯誤を整理して一連の記事にまとめました。
この記事はそのうちの1本目として、zipファイルを用いてLambdaにスクリプトをデプロイしてみたいと思います。
- zipファイルを用いてGo言語の処理をAWS Lambdaにデプロイする(この記事)
- Dockerイメージを用いてGo言語の処理をAWS Lambdaにデプロイする
- EventBridgeからAWS Lambdaを定期実行する処理をデプロイする
背景
最近、個人開発でもAWSのようなクラウド環境を使う時には、リソース管理のためにterraform
を使うようにしています。
手動のポチポチ操作を減らして自動化できたり、どのような設定だったのかを忘れることがなくなったり、再利用して別のプロジェクトに応用できたりととても便利です。
加えて最近は、Webのバックエンドやスクリプトの実装にはGo言語を用い、ローカル環境以外で実行したい場合には AWS Lambda
というサービスへデプロイして利用することが多いです。
ということで、この記事ではそのAWS Lambdaへのデプロイをterraformを用いて行う方法をまとめたいと思います。
なお、lambdaへのデプロイ方法は実行バイナリをzip形式でまとめてアップロードする方法と、Dockerのコンテナイメージをアップロードする方法が主にあります。この記事は前者のzip形式でのアップロードについてです。
実装
ファイル構成は次のようにしています。
.
├── .DS_Store
├── archive
│ ├── .gitignore
│ ├── archive.zip
│ └── archive_hash.txt
├── bin
│ ├── .gitignore
│ └── bootstrap
├── src
│ ├── go.mod
│ ├── go.sum
│ └── main.go
└── terraform
├── .gitignore
├── .terraform.lock.hcl
├── lambda-assume-role.json
├── lambda.tf
├── main.tf
└── terraform.tfstate
Lambdaで実行するプログラムを用意する
今回は簡単に以下のようなプログラムを用意しました。ここが本題ではないので、Lambdaの処理をGo言語で書くことについては、別の記事や公式のドキュメントを参照してください。
処理は簡単に、イベントとして名前を受け取って、挨拶と現在時刻を返すというものです。
今回実行するプログラム
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/aws/aws-lambda-go/lambda"
)
type MyEvent struct {
Name string `json:"name"`
}
type MyResponse struct {
Message string `json:"msg"`
Time string `json:"time"`
}
func HandleRequest(ctx context.Context, event *MyEvent) (*MyResponse, error) {
if event == nil {
return nil, fmt.Errorf("received nil event")
}
msg := "Hello, " + event.Name + "!"
res := MyResponse{
Message: msg,
Time: time.Now().String(),
}
log.Print(res)
return &res, nil
}
func main() {
lambda.Start(HandleRequest)
}
TerraformのLambda以外の部分を用意する
ここはシンプルに、awsのプロバイダーを使用することなどを設定しておきます。
terraform {
required_version = ">=1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
Lambda関数の設定に関する部分
長くなりますが、lambda関係の設定の全文は以下のとおりです。それぞれ部分ごとに区切って説明していきます。
全文を表示
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"
binary_file_name = "bootstrap"
zip_file_name = "archive.zip"
hash_file_name = "archive_hash.txt"
binary_file_path = "${local.golang_codedir}/../bin/${local.binary_file_name}"
zip_file_path = "${local.golang_codedir}/../archive/${local.zip_file_name}"
hash_file_path = "${local.golang_codedir}/../archive/${local.hash_file_name}"
}
resource "aws_lambda_function" "test_lambda" {
function_name = "test-lambda"
s3_bucket = local.s3_bucket
s3_key = data.aws_s3_object.zip.key
role = aws_iam_role.lambda_role.arn
handler = "main"
source_code_hash = data.aws_s3_object.zip_hash.body
runtime = "provided.al2"
}
resource "aws_iam_role" "lambda_role" {
name = "role-for-test_lambda"
assume_role_policy = file("lambda-assume-role.json")
}
resource "null_resource" "lambda_build" {
triggers = {
code_diff = join("", [
for file in fileset(local.golang_codedir, "**/{*.go,go.mod,go.sum}")
: filesha256("${local.golang_codedir}/${file}")
])
}
# コードのビルド
provisioner "local-exec" {
command = "cd ${local.golang_codedir} && CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -tags lambda.norpc -o ../bin/${local.binary_file_name} ./*.go"
}
# バイナリのzip化
provisioner "local-exec" {
command = "zip -j ${local.zip_file_path} ${local.binary_file_path}"
}
# バイナリのs3へのアップロード
provisioner "local-exec" {
command = "aws s3 cp ${local.zip_file_path} s3://${local.s3_base_path}/${local.zip_file_name}"
}
# ハッシュの生成
provisioner "local-exec" {
command = "openssl dgst -sha256 -binary ${local.zip_file_path} | openssl enc -base64 | tr -d \"\n\" > ${local.hash_file_path}"
}
# ハッシュのs3へのアップロード
provisioner "local-exec" {
command = "aws s3 cp ${local.hash_file_path} s3://${local.s3_base_path}/${local.hash_file_name} --content-type \"text/plain\""
}
}
data "aws_s3_object" "zip" {
depends_on = [null_resource.lambda_build]
bucket = local.s3_bucket
key = "${local.s3_key_prefix}/${local.zip_file_name}"
}
data "aws_s3_object" "zip_hash" {
depends_on = [null_resource.lambda_build]
bucket = local.s3_bucket
key = "${local.s3_key_prefix}/${local.hash_file_name}"
}
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Sid": "",
"Effect": "Allow"
}
]
}
各種変数の定義
はじめのブロックでは実行に関係する変数を定義しています。
s3_bucket = "hoge"
の箇所でzipファイルなどをアップロードするS3バケットの指定をしていますが、こちらは各自でご用意ください。この記事のTerraformではS3のバケット作成は行なっていません。すでにあるものと仮定しています。
S3のファイルパスは、私の環境の都合上s3_key_prefix = "test-lambda"
というprefixを指定しています。これも各自の環境に合わせてご設定ください。
これらの変数のパスやファイル名は基本的に変更できますが、実行ファイルのファイル名binary_file_name = "bootstrap"
だけはLambdaの仕様上変更できませんのでご注意ください。
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"
binary_file_name = "bootstrap"
zip_file_name = "archive.zip"
hash_file_name = "archive_hash.txt"
binary_file_path = "${local.golang_codedir}/../bin/${local.binary_file_name}"
zip_file_path = "${local.golang_codedir}/../archive/${local.zip_file_name}"
hash_file_path = "${local.golang_codedir}/../archive/${local.hash_file_name}"
}
lambdaのロールの設定
ここでlambda用のロールを作成しています。今回の記事の中ではCloud Watchへのログ出力やDBアクセスなどを行なっていませんが、これらを実施する場合には、このroleに適切な許可ポリシーをアタッチしてください。
resource "aws_iam_role" "lambda_role" {
name = "role-for-test_lambda"
assume_role_policy = file("lambda-assume-role.json")
}
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Sid": "",
"Effect": "Allow"
}
]
}
ソースコードのビルド、zip圧縮、S3へのアップロード
ここが一番重要な箇所かと思います。Go言語のソースのビルド、zip圧縮、S3へのアップロードといった処理を順番に行なっています。
null_resource
というリソースは、それによって生成されるAWSのリソースの様なものはありませんが、代わりに内部のlocal-exec
によりローカルでコマンドを実行するのに利用しています。何らかのトリガーを指定することができ、また、これを他のリソースのdepends_on
にも指定し処理を待つこともできます。
resource "null_resource" "lambda_build" {
triggers = {
code_diff = join("", [
for file in fileset(local.golang_codedir, "**/{*.go,go.mod,go.sum}")
: filesha256("${local.golang_codedir}/${file}")
])
}
# コードのビルド
provisioner "local-exec" {
command = "cd ${local.golang_codedir} && CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -tags lambda.norpc -o ../bin/${local.binary_file_name} ./*.go"
}
# バイナリのzip化
provisioner "local-exec" {
command = "zip -j ${local.zip_file_path} ${local.binary_file_path}"
}
# バイナリのs3へのアップロード
provisioner "local-exec" {
command = "aws s3 cp ${local.zip_file_path} s3://${local.s3_base_path}/${local.zip_file_name}"
}
# ハッシュの生成
provisioner "local-exec" {
command = "openssl dgst -sha256 -binary ${local.zip_file_path} | openssl enc -base64 | tr -d \"\n\" > ${local.hash_file_path}"
}
# ハッシュのs3へのアップロード
provisioner "local-exec" {
command = "aws s3 cp ${local.hash_file_path} s3://${local.s3_base_path}/${local.hash_file_name} --content-type \"text/plain\""
}
}
triggers
のブロックでは、Go言語のソースファイルからハッシュ値を計算し、その値が変わった場合(すなわちソースコードに変更があった場合)に内部の処理が実行されるようになっています。
fileset
関数でパターンにマッチするファイルを全て取得し、for文でfilesha256
関数に渡すことでハッシュ値を求め、それをjoin
関数でくっつけています。ファイル数が多くなるとこの部分が長くなることが考えられるので、修正が必要かもしれません…。
そのあとはprovisioner "local-exec"
でビルド・zip化・S3へのアップロード、といった処理を行なっています。
一連の処理の中ではバイナリをzip圧縮した後に、そのzipファイルのハッシュ値を求めるという処理をしています。後述しますがlambdaの仕様で source_code_hash
というパラメータでソースの変更をLambda側に通知し、Lambdaにおける実行ファイルの置き換えをトリガーする仕組みがあります。そのためのハッシュ値がここで用意しているハッシュになります。
S3のオブジェクトを参照する
ここではaws_s3_object
でS3のオブジェクトを参照しています。具体的には実行バイナリのzipファイルと、zipのハッシュ値を記録したテキストファイルです。
今回の方針ではLambdaの実行ファイルはS3にアップロードされているzipファイルを使用するようにしています。その「S3にアップロードされているファイル」の参照を、このデータソースから行なっている形です。
データソースはdepends_on = [null_resource.lambda_build]
となっており、先ほどのビルドなどの処理の後に実行されます。すなわち、ソースコードに変更があった場合には、必ずビルドおよびS3へのアップロードを待ってから、新たにアップロードされたファイルが参照されるという形になります。
data "aws_s3_object" "zip" {
depends_on = [null_resource.lambda_build]
bucket = local.s3_bucket
key = "${local.s3_key_prefix}/${local.zip_file_name}"
}
data "aws_s3_object" "zip_hash" {
depends_on = [null_resource.lambda_build]
bucket = local.s3_bucket
key = "${local.s3_key_prefix}/${local.hash_file_name}"
}
Lambda本体を設定する
最後に、Lambda本体を設定します。関数の名前、ソースのS3バケットとオブジェクトのキー、Lambda用のロール、ソースのハッシュ値などを指定します。runtimeは"provided.al2"
の他にprovided.al2023
も使用できます。
resource "aws_lambda_function" "test_lambda" {
function_name = "test-lambda"
s3_bucket = local.s3_bucket
s3_key = data.aws_s3_object.zip.key
role = aws_iam_role.lambda_role.arn
handler = "bootstrap"
source_code_hash = data.aws_s3_object.zip_hash.body
runtime = "provided.al2"
}
Lambdaのデプロイ
ここまできたらterraform apply
で構成を適用しましょう。
AWSのコンソールに行ってみて、Lambda関数がこの様にデプロイされており、テストが成功すればOKです。
確認用のコンテンツであれば、terraform destroy
の実行を忘れない様にしましょう。
参考