AWS Lambda Web Adapter(以下 LWA)を使うと、net/ http や chi、Gin で書いた既存の Go の Web サーバーを、Lambda 用のコードを足さずに Lambda で動かせます。やることは、LWA のレイヤーを関数に付け、アプリが待ち受けるポートを環境変数 AWS_LWA_PORT で伝えるだけです1 。
この記事では、Go 1.22 以降の http.ServeMux で書いた小さなメモ API を、provided.al2023・arm64 の Lambda に載せます。Function URL でのレスポンスストリーミング(SSE)も設定します。コードは Go 1.26.2 で go vet・go test を通し、ローカルで起動して curl で確かめました。AWS SAM CLI 1.166.2 での sam validate --lint と sam build も通しています。最後に、aws-lambda-go のイベント型を使う方式との比較表を載せます。
LWA は、Lambda のイベントとふつうの HTTP リクエストを相互に変換する「Lambda 拡張機能(Extension)」です。AWS が GitHub の awslabs で公開しており、Rust で書かれています1 2 。
クライアントの HTTPS リクエストが Function URL などで Lambda のイベントになり、Lambda Web Adapter が HTTP リクエストに戻して 127.0.0.1:8000 の Go サーバーに渡す流れ 図: 筆者作成。LWA の公式ドキュメントの説明をもとに作成
動きは次の順です2 。
Lambda が実行環境を作ると、LWA(拡張機能)とアプリ(bootstrap)が起動する
LWA はアプリに readiness check(既定は GET http:/ / 127.0.0.1:8080/)を 10 ミリ秒ごとに送り、応答があるまで待つ
応答があれば、LWA が Lambda のランタイムクライアントとして呼び出しを受け始める
呼び出しのたびに、イベントを HTTP リクエストに変えてアプリに送り、応答をイベントの応答に戻す
Lambda の外(ローカルや EC2、Fargate)では LWA は動きません。アプリはふつうの Web サーバーとして動きます2 。同じバイナリを、Lambda とコンテナの両方で使い回せるということです。
対応している呼び出し元は、API Gateway の REST API と HTTP API、Lambda の Function URL、Application Load Balancer(ALB)です1 。
LWA は 2026 年に 1.0 になり、9 月 18 日に v1.1.0 が出ました3 。1.0 で環境変数の名前が AWS_LWA_ で始まる形にそろい、古い名前は 2.0 で消える予定です1 。いま書く設定は、新しい名前で書いておくのが安全です。
もう 1 つの理由は、Go のランタイムの入れ替えです。Go 専用の go1.x に続き、provided.al2 も 2026 年 7 月 31 日に廃止されました。新しく作るなら provided.al2023 です4 。作り直すついでに、lambda.Start に縛られない形に移すのは良い機会です。
前提は Go 1.26.2(macOS)、provided.al2023、arm64 です。既存のサーバーがある場合は手順 2 から読んでください。
ルーターは Lambda をまったく意識しません。chi や Gin でも同じで、http.Handler を返せば十分です。
func newRouter (store *memoryStore) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz" , func (w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map [string ]string {"status" : "ok" })
})
mux.HandleFunc("GET /notes" , func (w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, store.list())
})
mux.HandleFunc("POST /notes" , func (w http.ResponseWriter, r *http.Request) {
var in struct {
Text string `json:"text"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1 <<20 )).Decode(&in); err != nil || strings.TrimSpace(in.Text) == "" {
writeJSON(w, http.StatusBadRequest, map [string ]string {"error" : "text is required" })
return
}
writeJSON(w, http.StatusCreated, store.add(strings.TrimSpace(in.Text)))
})
mux.HandleFunc("GET /events" , func (w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported" , http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type" , "text/event-stream" )
w.Header().Set("Cache-Control" , "no-cache" )
for i := 1 ; i <= 3 ; i++ {
select {
case <-r.Context().Done():
return
case <-time.After(500 * time.Millisecond):
}
fmt.Fprintf(w, "data: tick %d\n\n" , i)
flusher.Flush()
}
})
return mux
}
memoryStore(sync.Mutex で守ったスライス)と writeJSON は記事では省略します。
main で気をつけるのは 2 点です。
ポート : LWA は AWS_LWA_PORT に転送します。無ければ PORT、どちらも無ければ 8080 です1 。アプリも同じ順で読めば、設定が 1 か所で済みます
終了 : 拡張機能を使う関数では、Lambda は実行環境を止める前にランタイムへ SIGTERM を送ります2 。http.Server.Shutdown で処理中のリクエストを終えてから止めます
func main () {
port := os.Getenv("AWS_LWA_PORT" )
if port == "" {
port = os.Getenv("PORT" )
}
if port == "" {
port = "8080"
}
srv := &http.Server{
Addr: ":" + port,
Handler: newRouter(newMemoryStore()),
ReadHeaderTimeout: 5 * time.Second,
}
go func () {
log.Printf("listening on %s" , srv.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2 *time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v" , err)
}
log.Print("server stopped" )
}
Lambda 用のコードが無いので、ローカルでは go run や go build でそのまま起動できます。テストも標準の httptest で書けます。
func TestRouter (t *testing.T) {
h := newRouter(newMemoryStore())
tests := []struct {
name, method, path, body string
wantStatus int
wantBody string
}{
{"ヘルスチェック" , "GET" , "/healthz" , "" , 200 , `{"status":"ok"}` },
{"作成" , "POST" , "/notes" , `{"text":"hello"}` , 201 , `{"id":1,"text":"hello"}` },
{"一覧" , "GET" , "/notes" , "" , 200 , `[{"id":1,"text":"hello"}]` },
{"空の本文は 400" , "POST" , "/notes" , `{"text":""}` , 400 , `{"error":"text is required"}` },
{"メソッド違いは 405" , "DELETE" , "/notes" , "" , 405 , "" },
}
for _, tt := range tests {
t.Run(tt.name, func (t *testing.T) {
req := httptest.NewRequest(tt.method, tt.path, strings.NewReader(tt.body))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != tt.wantStatus {
t.Fatalf("status = %d, want %d" , rec.Code, tt.wantStatus)
}
if tt.wantBody != "" && strings.TrimSpace(rec.Body.String()) != tt.wantBody {
t.Errorf("body = %s, want %s" , rec.Body.String(), tt.wantBody)
}
})
}
}
SSE の / events は httptest.NewServer で立てて、data: tick が 3 回届くことを確かめるテストも用意しました。
ローカルで AWS_LWA_PORT=8000 を付けてサーバーを起動し、curl でヘルスチェック・メモの作成・SSE の受信を確かめ、SIGTERM で止めたあと arm64 の bootstrap をビルドする様子 筆者の環境(macOS・Go 1.26.2)で実行した出力
provided.al2023 では、実行ファイルの名前を bootstrap にします4 。LWA の方式では aws-lambda-go を使わないので、-tags lambda.norpc は要りません。
build-NotesFunction:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o $(ARTIFACTS_DIR) /bootstrap .
このターゲット名は、次の SAM テンプレートの BuildMethod: makefile から呼ばれます(build-{論理 ID})。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 既存の net/http サーバーを Lambda Web Adapter で動かす
Resources:
NotesFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
CodeUri: .
Handler: bootstrap
Runtime: provided.al2023
Architectures:
- arm64
MemorySize: 256
Timeout: 30
Layers:
- !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerArm64:30
Environment:
Variables:
AWS_LWA_PORT: 8000
AWS_LWA_READINESS_CHECK_PATH: /healthz
AWS_LWA_INVOKE_MODE: response_stream
FunctionUrlConfig:
AuthType: AWS_IAM
InvokeMode: RESPONSE_STREAM
Outputs:
NotesFunctionUrl:
Description: Function URL
Value: !GetAtt NotesFunctionUrl.FunctionUrl
レイヤー ARN は 2026 年 10 月時点の README の値です。x86_64 なら LambdaAdapterLayerX86:30 を使います1 。末尾の 30 はレイヤーのバージョンなので、更新時は README で最新を確かめます。
AWS_LAMBDA_EXEC_WRAPPER は付けていません。README の zip の手順には「AWS_LAMBDA_EXEC_WRAPPER=/ opt/ bootstrap を設定し、ハンドラーに run.sh などの起動スクリプトを指定する」とあります1 。これは Node.js や Python などのマネージドランタイムで、起動スクリプトを走らせるための設定です。provided.al2023 では bootstrap そのものが起動するので要りません。公式の「Golang Gin in Zip」の例も、provided.al2023 で Handler: bootstrap とし、この変数を設定していません5 。
デプロイは次のコマンドです。AWS アカウントにリソースを作り、料金がかかります。この記事では sam build の成功までを確かめ、デプロイはしていません。
sam build
sam deploy --guided
AuthType: AWS_IAM の Function URL は、署名付きのリクエストでしか呼べません。誰でも呼べる NONE にする場合は、アプリ側の認証を必ず入れます6 。
よく使うものを表にしました。2026 年 10 月時点の README の値です1 。
環境変数 既定値 用途 AWS_LWA_PORT8080(無ければ PORT)アプリが待ち受けるポート AWS_LWA_READINESS_CHECK_PATH/起動完了を確かめるパス AWS_LWA_READINESS_CHECK_PROTOCOLhttptcp にするとポートが開いたかだけを見るAWS_LWA_READINESS_CHECK_HEALTHY_STATUS100-499正常とみなすステータスコード AWS_LWA_INVOKE_MODEbufferedresponse_stream でストリーミングAWS_LWA_ENABLE_COMPRESSIONfalsegzip / br で圧縮(buffered のときだけ) AWS_LWA_REMOVE_BASE_PATHなし パスの先頭から取り除く部分(例: / api) AWS_LWA_ASYNC_INITfalse初期化が長いアプリのための非同期初期化 AWS_LWA_PASS_THROUGH_PATH/ eventsSQS など HTTP 以外のイベントを POST するパス
readiness check の既定は「100〜499 なら正常」です2 。今回のアプリは / を定義していないので 404 を返しますが、それでも正常とみなされて起動は進みます。意図をはっきりさせるため、AWS_LWA_READINESS_CHECK_PATH で / healthz を指定しました。
レスポンスストリーミングを使うと、応答を少しずつクライアントに送れます。SSE や大きなファイルのダウンロードに向いています7 。Lambda がストリーミングをそのまま扱えるのは Node.js のマネージドランタイムだけです。Go などほかの言語では、自前で Runtime API を扱うか、LWA を使うよう AWS のドキュメントが案内しています7 。
LWA で有効にするには、2 か所を合わせます2 6 。
環境変数 AWS_LWA_INVOKE_MODE=response_stream
Function URL の InvokeMode: RESPONSE_STREAM(既定は BUFFERED)
Go 側は、http.Flusher の Flush() を呼ぶたびに、そこまでの内容が送られます。手順 1 の / events がその例です。
Lambda と API Gateway の応答・ペイロードの上限(MB) データを表で見る Lambda と API Gateway の応答・ペイロードの上限(MB) 項目 上限(MB)(MB) Lambda の応答(buffered) 6 HTTP API のペイロード 10 Lambda の応答(ストリーミング) 200
出典: AWS Lambda Developer Guide「Response streaming for Lambda functions」、Amazon API Gateway Developer Guide「Quotas for HTTP APIs」(2026-10-05 参照)
ストリーミングなら、応答は最大 200 MB まで返せます。buffered の上限は 6 MB です7 。ただし 6 MB を超えた部分は、最大 2 MBps に帯域が絞られます7 。
ストリーミングには制約もあります。
圧縮(AWS_LWA_ENABLE_COMPRESSION)とは併用できず、両方有効にすると圧縮が無効になる2
ALB はストリーミングに対応しない2
Function URL は VPC の中ではストリーミングできない7
クライアントが切断しても処理は止まらず、実行時間の分は課金される7
Go を Lambda で動かす方法は、大きく 2 つあります。aws-lambda-go のイベント型でハンドラーを書く方法と、この記事の LWA です。前者は AWS Lambda で Go の API サーバーを作る で、HTTP API につなぐ手順を説明しています。
観点 aws-lambda-go(イベント型) Lambda Web Adapter アプリのコード events.APIGatewayV2HTTPRequest などに依存ふつうの net/ http。Lambda 用のコード無し 追加するもの github.com/ aws/ aws-lambda-goLWA のレイヤー(拡張機能) ルーティング RouteKey の switch や API Gateway のルートchi、Gin、http.ServeMux などそのまま ローカル実行 テストでイベントを流すか sam local go run で起動して curl で試せる他の環境への移植 ハンドラーの書き換えが要る 同じバイナリを EC2・Fargate・手元で使える1 レスポンスストリーミング Runtime API を自前で扱う必要がある7 環境変数 1 つで有効2 HTTP 以外のイベント(SQS など) 型付きの構造体でそのまま受ける / events に POST されたものを受ける1 起動時の処理 バイナリが直接 Lambda とやり取りする 拡張機能の起動と readiness check が加わる
選び方の目安です。
既存の Web サーバーがある、コンテナでも動かしたい、SSE を返したい : LWA
新しく小さな API を作る、SQS や S3 のイベントも同じ関数で型付きに扱いたい : aws-lambda-go
認証やスロットリングを API Gateway に任せたい : どちらでも API Gateway の背後に置ける。Function URL との違いは AWS の比較ページが詳しい8
LWA を使っても、Lambda の性質は変わりません。実行環境ごとにプロセスがあり、メモリ上の状態は共有されません。今回の memoryStore は確認用で、本番では DynamoDB などに置きます。Cloudflare Workers との比較は Cloudflare Workers と AWS Lambda の比較 にまとめています。
readiness check が通らないと、LWA はアプリの準備ができるまで待ち続けます1 。よくある原因は、アプリのポートと AWS_LWA_PORT の食い違いです。もう 1 つは、readiness check のパスが 500 番台を返していることです。ログに listening on が出ているか、そのポートが AWS_LWA_PORT と同じかを確かめます。待つ時間に上限を付けたい場合は AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS を使います1 。
sam local は Lambda の Runtime Interface Emulator を 8080 番で起動します。アプリは 8080 以外で待ち受けるよう、AWS_LWA_PORT を変えます2 。この記事で 8000 番にしているのはこのためです。
REST API のステージ(/ prod など)やカスタムドメインのベースパスが付くと、アプリが期待するパスとずれます。AWS_LWA_REMOVE_BASE_PATH で先頭の 1 つを取り除けます1 。
LWA は API Gateway のリクエストコンテキストを x-amzn-request-context ヘッダーに、Lambda のコンテキストを x-amzn-lambda-context ヘッダーに JSON で入れて渡します2 。Go では r.Header.Get("x-amzn-request-context") を json.Unmarshal して読みます。
READINESS_CHECK_PATH のような AWS_LWA_ の付かない名前は非推奨で、2.0 で消える予定です。PORT だけは引き続き使えます1 。
LWA はイベントと HTTP を変換する拡張機能。Go のコードは net/ http のままでよい
provided.al2023 なら、レイヤーを付けて Handler: bootstrap、AWS_LWA_PORT を合わせるだけ。AWS_LAMBDA_EXEC_WRAPPER は要らない
readiness check のパスは AWS_LWA_READINESS_CHECK_PATH で明示し、SIGTERM で Shutdown する
ストリーミングは AWS_LWA_INVOKE_MODE=response_stream と Function URL の RESPONSE_STREAM の両方が必要
既存のサーバーを移すなら LWA、新しい小さな API や非 HTTP のイベントが中心なら aws-lambda-go
次にやることは、手元の chi や Gin のサーバーに SIGTERM の処理とポートの読み込みを足し、sam build で bootstrap を作ってみることです。
LWA に専用の料金はありません。かかるのは、ふつうの Lambda の料金(リクエストと実行時間)です。拡張機能の起動や readiness check も実行環境の初期化の中で行われます。Function URL には URL 自体の追加料金はありません8 。
動きます。LWA は HTTP/1.1 か 1.0 を話すアプリならフレームワークを問いません1 。公式の例には Gin の zip 版とコンテナ版があります5 。chi でも、ルーターを http.Server の Handler に渡すだけです。
Dockerfile に COPY --from=public.ecr.aws/ awsguru/ aws-lambda-adapter:1.1.0 / lambda-adapter / opt/ extensions/ lambda-adapter の 1 行を足します1 。拡張機能が / opt/ extensions に入れば、レイヤーと同じように動きます。同じイメージは Fargate などでもそのまま動きます。
置けます。SAM なら Events に Type: HttpApi を足します。公式の Gin の例もこの形です5 。ただし HTTP API の統合タイムアウトは 30 秒で、ペイロードは 10 MB までです9 。長い応答やストリーミングが要るなら Function URL を使います。
LWA の README は、Lambda Managed Instances での複数リクエストの同時処理に対応していると書いています1 。Managed Instances については Lambda Managed Instances とは で扱っています。