💡

法人のAWS Lambdaの歩き方 (令和 2026版)

に公開

はじめまして @shimma です。業務支援として複数社、インフラを中心に直接手を動かして、社内で横展開できるような設計・コードベースをご提供しています。

  • 枯れた技術で
  • コード行数少なく
  • 運用コストかからず
  • 8-9割くらいのことを解決できる

こちらが私の設計がポリシーです。

はじめに

AWS Lambdaについて、そのポテンシャルにもかかわらず、簡単なスクリプトを作る程度の事例が占めており、本格的にチーム開発、DevOps、アプリケーション開発者の全観点からシステムに組み込む解説記事がないと感じ、今回は具体事例を1個公開します。

想定読者

  • 社内で全てのインフラはTerraform管理されている
  • 一部AWS Lambdaは使っているが、ライブラリ入れるのが手間でzipにまとめたPython/JSを使っている程度の活用しかしていない
  • 本番運用はECSである

結論: Dockerイメージでデプロイ管理する

AWS Lambdaは2020年ごろにコンテナイメージがサポートされ、RustからGoまで一通り動きます。

コンテナで動かす場合、Lambda対応イメージを含める必要はありますが、これも最新provided.al2023の指定すれば、殆どバイナリサイズも大きくなりません。(過去はLambda化するとイメージサイズがプラスで400MBほど膨れていましたが、今は純粋なバイナリに近いサイズ感になります)

ローカルにてDockerイメージで好きな言語で開発して、テストコードまでしっかり書いて、あとはそれをECRにPushするのみです。本番環境でインフラコストがほぼかからない夢のシステムを運用可能です。

Lambdaの立ち位置的に、しっかり管理しようとすると

  • デプロイ管理の手間などからKlayers + Pythonだったり
  • TerraformのArchive Provider使って簡単なJSスクリプト程度

で収まりがちですが、Docker化すれば全部がシンプルになります。AWS公式のLambda Layerは全く使う必要がありません。

コード開発編: Go + Docker (Lambda向け)

今回は特定のGETリクエストをレスポンスする、というユースケースで実装します。LambdaはPublic Function URLという外部公開用のHTTPSエンドポイントを簡単に作れますので、それを利用します。

ローカル開発でも動き、Lambdaでも同様に起動するイメージとなります。
流れとしては以下のように組むことで、ローカルでも同じように開発出来ます。

  • Local HTTPリクエストならば、LambdaFunctionURLRequestにその場で変換
  • Lambda Eventとして共通で処理する
Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app

COPY go.mod go.sum* ./

RUN go mod download && go mod tidy

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bootstrap .

FROM public.ecr.aws/lambda/provided:al2023

COPY --from=builder /app/bootstrap /var/task/bootstrap

RUN chmod +x /var/task/bootstrap

ENTRYPOINT [ "/var/task/bootstrap" ]
main.go
func main() {
  if os.Getenv("LOCAL_MODE") == "true" {
    port := os.Getenv("PORT")
    if port == "" {
      port = "8080"
    }
    http.HandleFunc("/", localHandler)
    if err := http.ListenAndServe(":"+port, nil); err != nil {
      log.Fatalf("Failed to start server: %v", err)
    }
  } else {
    lambda.Start(handler)
  }
}

func handler(ctx context.Context, request events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
  if request.RequestContext.HTTP.Method == "GET" {
    return events.LambdaFunctionURLResponse{
      StatusCode: http.StatusOK,
      Body:       `{"status": "ok!”}`,
      Headers: map[string]string{
        "Content-Type": "application/json",
      },
    }, nil
  }
}

テスト編: ローカル開発環境をPublic公開する

あとはcompose.yamlに以下のように準備します。

compose.yaml
services:
  dev:
    image: golang:1.21-alpine
    environment:
      - LOCAL_MODE=true
      - PORT=8080
    volumes:
      - .:/app
    working_dir: /app
    command: sh -c "go mod download && go run main.go"
    ports:
      - "8080:8080"

  cloudflared:
    image: cloudflare/cloudflared:latest
    command:
      - tunnel
      - --no-autoupdate
      - run
    environment:
      - TUNNEL_TOKEN=${TUNNEL_TOKEN:-}
    depends_on:
      - dev

後は以下のように実行するだけで、ローカル開発環境でPublic URLが出力され、n8nなり自由に外部ツールとのWebhookなどをローカルで実装検証できます。

docker compose up dev cloudflared

デプロイ編: Terraform

基本はTerraformで管理されているので、これを統合して利用します。AWS公式が提供するAWS SAMやServerless Frameworkは個人的に使いにくいため利用しません。

null_resourceを利用して、local execでビルドからデプロイまで行います。変更対象ファイルhashを取ることで、差分がある時だけデプロイが動くようにします。

aws_lambda.tf
locals {
  app_image_tag = substr(sha256(join("", [
    filebase64sha256("${path.module}/aws-lambda/app/Dockerfile"),
    filebase64sha256("${path.module}/aws-lambda/app/main.go"),
    filebase64sha256("${path.module}/aws-lambda/app/go.mod"),
    filebase64sha256("${path.module}/aws-lambda/app/go.sum")
  ])), 0, 7)
}

resource "null_resource" "app_ecr" {
  triggers = {
    repository_url = aws_ecr_repository.app.repository_url
    image_tag      = local.app_image_tag
  }

  provisioner "local-exec" {
    working_dir = "${path.module}/aws-lambda/app"
    command     = <<-EOT
      set -e

      docker build --platform linux/amd64 --provenance=false \
        -t app-for-terraform:latest .

      aws ecr get-login-password --region ${local.aws_region} \
        | docker login --username AWS --password-stdin \
        ${data.aws_caller_identity.self.account_id}.dkr.ecr.${local.aws_region}.amazonaws.com

      docker tag app-for-terraform:latest \
        ${aws_ecr_repository.app.repository_url}:${local.app_image_tag}

      docker tag app-for-terraform:latest \
        ${aws_ecr_repository.app.repository_url}:latest

      docker push ${aws_ecr_repository.app.repository_url}:${local.app_image_tag}
      docker push ${aws_ecr_repository.app.repository_url}:latest
    EOT
  }
}

resource "aws_lambda_function" "app" {
  function_name = "${local.app_env}-app"
  role          = aws_iam_role.app_lambda.arn
  package_type  = "Image"
  image_uri     = "${aws_ecr_repository.app.repository_url}:${local.app_image_tag}"

  timeout     = 30
  memory_size = 256

  depends_on = [null_resource.app_ecr]

  environment {
    variables = {
      SOME_ENV_VAR = "<redacted>"
    }
  }
}

resource "aws_lambda_function_url" "app" {
  function_name      = aws_lambda_function.app.function_name
  authorization_type = "NONE"

  cors {
    allow_credentials = false
    allow_origins     = ["*"]
    allow_methods     = ["POST"]
    allow_headers     = ["*"]
    expose_headers    = []
    max_age           = 0
  }
}

よりデプロイが高頻度なプロダクトであれば、Lambda版ecspressoであるlambrollを利用しGithub Actions経由のデプロイによるイメージ差し替えをおすすめします。

終わりに

「SendGridの開封確認のコールバックをメインサーバで受ける設計にしたところ、サービス提供に影響受けるレベルで負担がかかってしまった」など、そういうケースも今回のようなLambdaに流すことでコスト削減とシステム安定化が可能です。是非、高度な活用事例をお知らせ下さい。

COUNTERWORKS テックブログ

Discussion