AWS サーバーレスを Terraform で構築しようとしたらやってられなかった話
以前、こんな記事を書きました。
Terraform の充実したエコシステムに比べて SAM はその辺りが貧弱であり、なんか微妙という意見でした。
今回はそれに対しての謝罪記事です😢
結論
AWS でサーバーレスアーキテクチャを構築する際の IaC ツールに関しては SAM 一択。
アーキテクチャのメインが ECS などその他になるのであれば、 Terraform 推奨。
理由
Terraform 大好きなので一旦サーバーレスアーキテクチャの王道の構成を Terraform で作ってみようと思いました。
- Lambda
- DynamoDB
- API Gateway
みたいな割と王道な構成です。
しかしやってみたら以下の点で地獄でし😢
- 1つの API Gateway エンドポイントと、Lambda関数を統合するために必要な記述量が非常に多い
- 「いや、そこは良しなにやってよ」という部分も全部細かく設定しなくてはいけない
- 結果インフラを構成するのにに時間がかかる
1つの Lambda 関数を1エンドポイントに関連付けたい時の SAM は以下のようになります。
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: hello-function
CodeUri: hello/
Handler: handler
Runtime: go1.x
Role: !GetAtt LambdaRole.Arn
Events:
CatchAll:
Type: Api
Properties:
Path: /hello
Method: GET
注目すべきは Event
ブロックです。
Type → API
Path → /hello
HTTP Method → GET
初見の人でも、このLambda関数と API Gateway がどのように関連づけられているかすぐに分かるのではないでしょうか?
もちろん意味は、
「/hello というパスに対して GET でアクセスしたときに、 hello-function を呼び出す」
ということです。何の問題もありません。
が、同じことを Terraform でやろうとするとこんな感じになります。
resource "aws_api_gateway_rest_api" "main" {
name = local.app_id
}
resource "aws_api_gateway_deployment" "main" {
rest_api_id = aws_api_gateway_rest_api.main.id
triggers = {
redeployment = filesha1("./api_gateway.tf")
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_api_gateway_integration.hello_get,
]
}
resource "aws_api_gateway_stage" "main" {
deployment_id = aws_api_gateway_deployment.main.id
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = terraform.workspace
}
resource "aws_api_gateway_resource" "hello" {
rest_api_id = aws_api_gateway_rest_api.main.id
parent_id = aws_api_gateway_rest_api.main.root_resource_id
path_part = "hello"
}
resource "aws_api_gateway_method" "hello_get" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.hello.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "hello_get" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_method.hello_get.resource_id
http_method = aws_api_gateway_method.hello_get.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.hello_world.invoke_arn
}
記述量が全然違いますが、何が恐ろしいかって、これはまだ API Gateway に関連する部分だけだということです。
Lambda部分はざっと以下のような形なります。
data "archive_file" "hello_world" {
type = "zip"
source_file = replace(var.source_file, var.replaced_dir, "hello")
output_path = replace(var.output_path, var.replaced_dir, "hello")
}
resource "aws_lambda_function" "hello_world" {
function_name = "${local.app_id}-hello-world"
handler = "handler"
role = aws_iam_role.lambda.arn
runtime = "go1.x"
filename = data.archive_file.hello_world.output_path
source_code_hash = data.archive_file.hello_world.output_base64sha256
}
resource "aws_lambda_permission" "hello_world" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello_world.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.main.execution_arn}/${terraform.workspace}/GET/hello"
}
まとめると、
API Gateway
- API それ自体
- 依存関係も考慮して記述されたAPI のデプロイメント(有効化みたいなもの)
- API のステージ
- API のリソース(
hello
など REST で表した時のリソース1つ1つ) - HTTP メソッド
- Lambda関数とリソース、HTTP メソッドの統合設定
Lambda
- アーカイブファイルを作る(コンテナではなく zip 形式前提です)
- 関数事態の記述
- 先ほど作成した API がこのLambdaを実行するためのパーミッション
全てでは無いものの、このほとんどを1つLambda関数を追加する度に記述しなくてはいけません。。。
これでは開発スピードが出ないのも当然ですし、細かい関連付けをミスってたりして確認に時間がかかります。
SAM の記述がいかにシンプルで美しいものになっているかに気付かされます。
ただし、 Terraform の名誉にために補足しておくと、そもそもCloudFormationでも同様の問題があり、記述を簡単にしたいという思いから SAM というソフトウェアが開発されたと思います(完全な予測ですが)。
なのでそもそもサーバーレスアーキテクチャを実装するのに向いていないという話であって、そこを Terraform に求めるのは酷でしょう。
また、Terraform は主に「インフラ」を構成・管理するツールなので、Lambdaのように関数のソースコード(インフラではなくアプリケーションロジック側)も扱わないといけないのは違和感があります。
できなくは無いですが、その場合は自分でビルドの設定をする必要がありますし、やはり面倒が増えます。
Terraform のエコシステムや Golang に近い感じはすごく好きなのですが、ことサーバーレスに関しては大人しく SAM を使った方が良さそうです😊
以上現場からでした🏄♂️
参考資料
Discussion