0
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

AWS LambdaにGo言語のスクリプトをzipファイル形式でデプロイする [Terraformを使用]

Last updated at Posted at 2024-07-16

要約

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

関連記事

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

この記事はそのうちの1本目として、zipファイルを用いて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言語で書くことについては、別の記事や公式のドキュメントを参照してください。

処理は簡単に、イベントとして名前を受け取って、挨拶と現在時刻を返すというものです。

今回実行するプログラム
src/main.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/main.tf
terraform {
  required_version = ">=1.7"

  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

Lambda関数の設定に関する部分

長くなりますが、lambda関係の設定の全文は以下のとおりです。それぞれ部分ごとに区切って説明していきます。

全文を表示
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"
  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}"
}
terraform/lambda-assume-role.json
{
  "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の仕様上変更できませんのでご注意ください。

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"
  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に適切な許可ポリシーをアタッチしてください

terraform/lambda.tf(一部)
resource "aws_iam_role" "lambda_role" {
  name = "role-for-test_lambda"
  assume_role_policy = file("lambda-assume-role.json")
}
terraform/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にも指定し処理を待つこともできます。

terraform/lambda.tf(一部)
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へのアップロードを待ってから、新たにアップロードされたファイルが参照されるという形になります。

terraform/lambda.tf(一部)
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も使用できます

terraform/lambda.tf(一部)
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です。

go_terraform_zip-1.png

go_terraform_zip-2.png

確認用のコンテンツであれば、terraform destroyの実行を忘れない様にしましょう。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0