TL;DR;
この記事を読めば、Terraformを用いてLambdaリソースを管理することができるようになり、その周辺技術に対する知識がつきます。
AWSの当たり前であり、「IAM」の考え方などです。
§1 今回作るリソース
この頭で書いたようなリソースを作成します。
AWSを利用するので、せっかくなら一人開発でもtfstateをs3で管理しましょう!
ということで、S3もAPIには直接関係ないですが置かれています。
具体的には、/api/v1/usersというエンドポイントをGET/POSTの2つに対してAPI Gatewayを使って解放します。
次に、それらのハンドリング先をLambdaに設定します。
そして、その処理のデータ永続化ストレージとしてDynamoDBを利用します。
そして、今回は、APIエンドポイントのサンプルとして、Goとnet/httpを用いています。
§2 GitHubサンプルリポジトリ
ここにすべてのファイルが保存されています。
Cloneして覗いてみてください。
§3 プロジェクトの概要
おおまかなディレクトリ構成配下のとおりです。
.
├── backend
│ ├── Makefile # 実行コマンドをまとめたMakefile
│ ├── bin
│ ├── cmd # エントリポイント
│ ├── db.go # dynamodbの接続コード
│ ├── docs # swaggerのAPI定義書
│ ├── go.mod
│ ├── go.sum
│ ├── user_handler.go # db操作のリポジトリ
│ ├── user_handler_db.go # ハンドラー
│ └── user_handler_test.go # ハンドラーのテスト
└── infra
├── bootstrap # tfstateを置くためのs3バケットを初回作成す
├── env # dev/prod分離を行うための部分
└── modules # lambdaやnetworkなどのモジュール単体
Goの方はほぼ適当です。
AWS SDK v2 for Go のテストぐらいに思ってもらえたら嬉しいです。
§4 Terraformの使い方が概要
Terraformは大まかに言うと、「インフラの設定をコードでかけるようにしたやつ」っていう一言につきます。
IaCといって、古より使われている技術の1つです。
これまでは、定義するコマンドを並べたバッシュスクリプトなどを用意して使っていたところを、もっと簡単に、プログラムブルに、かつ再利用可能性高く、かけるようにしているのがTerraformはじめとしたIaCの存在意義です。
他にはCloudFormationなどがあります。
しかももっと嬉しいのは、変更履歴などを残しておける機能がついていることです。
いわゆるバージョニングと呼ばれるやつです。
さっきのシェルスクリプトでもインフラ定義の自動化はできますが、問題が1つ。
変更履歴を統一的に管理する方法がないってことです。
いわゆる、SQLのマイグレーションみたいに、何かしらで統一的な履歴を残す方法が提供されていればいいですが、それはインフラの提供元によって様々。
AWS、Cloudflare, Azure, GCP, NTT はたまたオンプレまで。
多岐にわたるわけです。
となると、変更履歴はどうやって残すか。
開発者の頭の中です。
これじゃ、行けませんよね?
そこで生まれたのがTerraformという、インフラ定義もできるし、その経校も残せちゃうIaCの登場です。
なので、Terraformには状態という概念、というかファイルが存在します。
これがtfstateというファイルです。
これは、常に細心でなければいけません。
しかし、gitなどで管理するには重すぎるし、インフラの状態というのは、開発のライフサイクルと比べてあまりにも離れています。
なので、この変更履歴は「開発者みんなが、常に最新情報で」見れなければいけません。
でもGitは使えない。
ならどうするか。
オブジェクトストレージに置くしかないわけです。
でもGoogleDriveとか、DropBoxとか、Icloudとかは、開発者ライクではないですよね?
そこで、登場するのがS3というわけです。
実務の開発では、このS3にtfstateをおいているとよく聞きます。
§5 でもそんなS3もインフラじゃん
ここが問題です。
S3はあくまでもAWSリソース。
区分で言うと、他のLambdaとかと同じく栗なわけで、Terraformで管理する先のひとつなわけです。
ですが、そこにTerraformのファイルを起きたいのです。
ちょっと気持ち悪いですよね。
インフラの変更を定義する履歴をインフラのに残したい。
いわゆる卵が先か鶏が先か問題みたいな気持ち悪さです。
この気持ち悪さは、インフラと変更履歴を同列のライフサイクルで考えるから気持ち悪いのです。
インフラを一本先に進ませましょう。
いわゆるS3のバケットを先に作っちゃうのです。
これがよく言うbootstrapディレクトリに存在するTerraformコードの正体です。
§6 こっからは簡単。あとは、dev/prod分離して、モジュールごとに機能を書いていくだけ。
dev/prod分離には、様々な方法があります。
workspace分離や、ディレクトリ分離、またはその両方など。
主な違いは、モジュールの記述負担が大きいのはworkspace分離、dev/prod自体を分けることに容量やコードを増やしてしまうのが、ディレクトリ分割。
使い分け方は、モジュールの数が多ければ、負担をdev/prodに寄せて、module記述自体の負担を下げるディレクトリ分割、でなければworkspace分割というのが正規の流れかと思います。
これがenvディレクトリによくあるコードの正体です。
§7 モジュール分離とは?
Terraformは実は、実行時に「すべてのファイルを一列につなげて実行」します。
この解釈というか言い方が正しいのかわかりませんが、書いてある内容的にはそうだと踏んでいます。
なので、locals.tfとか名前をつけて上げても、ファイルの最初はlocalsというプレフィックスから始まるわけですね。
なので、実は、普通のプログラミングより、モジュール分割という考え方は簡単です。
いわゆる、ひとつなぎにしたいファイルを同じディレクトリに集めればいいのです。
で、環境側からはmodule, sourceで呼び出すだけ。
これだけです。
めっちゃ簡単ですね。
§8 APIの建て方
これまでVPSなどで、APIを立てていた個人開発税からすると、かなりびっくりかもしれません。
- まず、terraform側でAPIエンドポイントを切ります(正確には、Lambdaに流したいルートを定義)
- 次に、Terraform側からLambdaをそれぞれのエンドポイントに接続させます
- 次に、Lambdaの中でそのリクエストをルーティングします
- 最後に処理します
という流れでAPI定義を書きます。
いわゆる、プロキシみたいな役割をAPIGatewayは担ってくれて、その後の処理系として、Lambdaを選ぶという感じです。
なので、実は、処理系として選べるものはLambdaだけではなく、EC2やECSなど様々分かれています!
ちょっとつかれたので、こっからはAntigravityに書かせました(Claudeはリミットきてました(;´д`)トホホ…)
ごめんなさい🙇
内容的には正しいです
§9 今回このリポジトリでチャレンジしたこと(Go × Lambda × Terraform 実践編)
さて、ここからが本題です。
「ただLambdaとAPIGatewayを動かしただけ」ならチュートリアルで終わっちゃいますが、このリポジトリでは実務で通用するレベルのこだわりをいくつか詰め
込んでみました。
ざっくりまとめると、以下の3本柱です!
---
## 1. 【Go側】Lambda臭を消し去る!`net/http` と `httpadapter` の合わせ技
LambdaでGoを書くとき、一番嫌なのが「AWSの独自型(`events.APIGatewayProxyRequest`
とか)にコードが引きずられること」なんですよね。
そこで今回は、AWS公式が出している `aws-lambda-go-api-proxy` を使って、**完全に普通の Go Web アプリ(`net/http`)として書く**
というチャレンジをしました。
### ① Go 1.22 の新しいルーティングをそのまま使う
```go
// cmd/lambda/main.go
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/users", service.GetUsers)
mux.HandleFunc("POST /v1/users", service.PostUsers)
adaptor = httpadapter.New(mux)
これ、めちゃくちゃ美しくないですか?
API Gatewayから来たリクエストをアダプターが一瞬で http.Request に変換して流し込んでくれます。
② httptest で「普通のWeb API」としてテストができる!
Lambdaの独自イベントをモックするのって超面倒ですよね。
でも net/http ベースで書いておけば、標準パッケージの net/http/httptest を使ってサクッとテストが書けちゃいます。
③ init() でコールドスタート対策
DBクライアントの初期化やDI、ルーターの生成はすべて init() で行い、グローバル変数に保持しています。
これでLambdaのウォームスタート時(2回目以降の呼び出し)に無駄な初期化処理が走らず、爆速でレスポンスを返せます。
──────
2. 【ビルド側】Makefile 一発で軽量バイナリ作成&デプロイ
GoのLambdaは provided.al2023(カスタムランタイム)を採用し、bootstrap という名前の単一バイナリで動かしています。
# backend/Makefile
build:
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o bin/bootstrap cmd/lambda/main.go
zip:
make build
zip -j bin/lambda.zip bin/bootstrap
• GOOS=linux GOARCH=amd64: Linux環境向けにクロスコンパイル
• -ldflags="-s -w" -trimpath: デバッグ情報やパス情報を削ぎ落として、バイナリサイズを極限まで削る(コールドスタートの短縮に直結!)
• make deploy 一発で Zip 化から AWS へのアップロードまで完了!
──────
3. 【Terraform側】個人開発の域を超えた実践的なインフラ設計
① bootstrap による「鶏と卵問題」の完全解消
infra/bootstrap で、tfstateを保管するためのS3バケットを事前に作成。
versioning { status = "Enabled" } と lifecycle { prevent_destroy = true }
をしっかり設定して、状態ファイルが事故で消えない・過去の状態に戻せるセーフティネットを張っています。
② API Gateway は {proxy+} でスマートに流す
API Gateway側でエンドポイント(/users とか)を細かくポチポチ定義するのではなく、{proxy+} で丸ごと Lambda
にリクエストをパススルー。
ルーティングの責務を Go の http.ServeMux 側に寄せることで、エンドポイントが増えても Terraform の修正を最小限に抑えています。
③ 最小権限の IAM ポリシー設計
Lambda に雑に AdministratorAccess を与えるのはご法度です。
• CloudWatch Logs への書き込み権限
• 対象の DynamoDB テーブル(aws_dynamodb_table.lambda_api.arn)へのアクセス権限
これだけに厳密に絞り込んだ IAM ロール&ポリシーを Terraform でコード化しています。
まとめ
- Terraform: bootstrap で土台を作り、modules と env(dev/prod)で綺麗に疎結合化。
- Go: net/http + httpadapter で Lambda の独自仕様に依存しないクリーンな実装。
- テスト&ビルド: モックを使った爆速単体テスト & Makefile による最適化ビルド。
ただ動くだけのサンプルではなく、「開発体験(DX)」と「実務での保守性・セキュリティ」を両立させる
というのが、今回このリポジトリでチャレンジした全体像です!
