これは、なにをしたくて書いたもの?
以前に、LocalStackでAmazon API Gateway+AWS Lambdaを動かしてみるエントリを書きました。
LocalStackでAmazon API Gateway+AWS Lambdaを動かしてみる - CLOVER🍀
今回は、こちらをTerraformで実現できないかなと思いまして。
LocalStackに対してTerraformを使うことを試す、という意味でもやってみることにしました。
LocalStackに対して、Terraformでリソースを作成する
LocalStackはあくまでローカルでAWSのサービスをエミュレーションするだけなので、過度な期待をするものでは
ないと思いますが。
TerraformのドキュメントにもLocalStack向けの設定が書いてあり、せっかくなら試してみようかなと。
Custom Service Endpoint Configuration / Connecting to Local AWS Compatible SolutionsLocalStack
リソース定義は、aws_api_gateway_integration
の記載のものを少しカスタマイズする感じにします。
Resource: aws_api_gateway_integration / Lambda integration
どうカスタマイズするかというと、こちらのエントリと似たような感じにします。
LocalStackでAmazon API Gateway+AWS Lambdaを動かしてみる - CLOVER🍀
- AWS Lambda関数は、Pythonで作成する
- AWS Lambda関数のデプロイは、Terraformでzipファイルを作成して行う
- AWS LambdaとAmazon API Gatewayの統合は、プロキシ統合を使う
ステージ名は前のエントリではtest
でしたが、今回はProd
にしておきます。
環境
今回の環境は、こちら。
LocalStack。
$ localstack --version 0.12.18.1 $ LAMBDA_EXECUTOR=docker-reuse localstack start
$ awslocal --version aws-cli/2.2.43 Python/3.8.8 Linux/5.4.0-88-generic exe/x86_64.ubuntu.20 prompt/off
Terraform。
$ terraform version Terraform v1.0.8 on linux_amd64 + provider registry.terraform.io/hashicorp/archive v2.2.0 + provider registry.terraform.io/hashicorp/aws v3.61.0
構成
最終的にできあがった構成は、こちら。
$ tree . ├── lambda_app │ └── function.py └── main.tf 1 directory, 2 files
AWS Lambda関数としてのソースコードが1ファイル、Terraformでのリソース定義をまとめたものが1ファイルです。
AWS Lambda関数
デプロイするAWS Lambda関数の定義は、こちら。
lambda_app/function.py
import json def handler(event, context): print(f'event = {event}') path = event['path'] http_method = event['httpMethod'] word = None if 'queryStringParameters' in event: if 'word' in event['queryStringParameters']: word = event['queryStringParameters']['word'] if 'body' in event and len(event['body']) != 0: body = json.loads(event['body']) if 'word' in body: word = body['word'] if word is None: word = 'World' return { 'statusCode': 200, 'body': { 'message': f'Hello {word}!!', 'path': path, 'http_method': http_method } }
元ネタになったブログエントリ、そのままです。
LocalStackでAmazon API Gateway+AWS Lambdaを動かしてみる - CLOVER🍀
Terraformリソース定義
Terraformのリソース定義はひとつのmain.tf
ファイルにまとめていますが、いくつかに区切って書くことにしましょう。
Terraformのバージョン制約と、Providerの定義。
terraform { required_version = "1.0.8" required_providers { aws = { source = "hashicorp/aws" version = "3.61.0" } archive = { source = "hashicorp/archive" version = "2.2.0" } } } provider "aws" { access_key = "mock_access_key" region = "us-east-1" s3_force_path_style = true secret_key = "mock_secret_key" skip_credentials_validation = true skip_metadata_api_check = true skip_requesting_account_id = true endpoints { apigateway = "http://localhost:4566" cloudformation = "http://localhost:4566" cloudwatch = "http://localhost:4566" dynamodb = "http://localhost:4566" es = "http://localhost:4566" firehose = "http://localhost:4566" iam = "http://localhost:4566" kinesis = "http://localhost:4566" lambda = "http://localhost:4566" route53 = "http://localhost:4566" redshift = "http://localhost:4566" s3 = "http://localhost:4566" secretsmanager = "http://localhost:4566" ses = "http://localhost:4566" sns = "http://localhost:4566" sqs = "http://localhost:4566" ssm = "http://localhost:4566" stepfunctions = "http://localhost:4566" sts = "http://localhost:4566" } } provider "archive" {}
AWS Providerは、LocalStack向けの設定にしています。内容的には、ドキュメントの内容をそのまま使っています。
Custom Service Endpoint Configuration / Connecting to Local AWS Compatible SolutionsLocalStack
## API Gateway resource "aws_api_gateway_rest_api" "rest_api" { name = "my-rest-api" } resource "aws_api_gateway_resource" "proxy_resource" { rest_api_id = aws_api_gateway_rest_api.rest_api.id parent_id = aws_api_gateway_rest_api.rest_api.root_resource_id path_part = "{proxy+}" } resource "aws_api_gateway_method" "method" { rest_api_id = aws_api_gateway_rest_api.rest_api.id authorization = "NONE" http_method = "ANY" resource_id = aws_api_gateway_resource.proxy_resource.id } resource "aws_api_gateway_integration" "integration" { rest_api_id = aws_api_gateway_rest_api.rest_api.id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = aws_api_gateway_method.method.http_method integration_http_method = "ANY" type = "AWS_PROXY" uri = aws_lambda_function.lambda.invoke_arn } resource "aws_api_gateway_deployment" "deployment" { rest_api_id = aws_api_gateway_rest_api.rest_api.id triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_rest_api.rest_api.body, aws_api_gateway_resource.proxy_resource.id, aws_api_gateway_method.method.id, aws_api_gateway_integration.integration.id, ])) } lifecycle { create_before_destroy = true } depends_on = [aws_api_gateway_integration.integration] } resource "aws_api_gateway_stage" "stage" { deployment_id = aws_api_gateway_deployment.deployment.id rest_api_id = aws_api_gateway_rest_api.rest_api.id stage_name = "Prod" }
基本的には、こちらのドキュメントの例に習ったものです。
Resource: aws_api_gateway_integration / Lambda integration
Resource: aws_api_gateway_deployment / Example Usage / Terraform Resources
aws_api_gateway_deployment
については、先にaws_api_gateway_integration
ができている必要があり、LocalStackだと
実行が速すぎてエラーになるのでdepends_on
を入れました…。
resource "aws_api_gateway_deployment" "deployment" { rest_api_id = aws_api_gateway_rest_api.rest_api.id triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_rest_api.rest_api.body, aws_api_gateway_resource.proxy_resource.id, aws_api_gateway_method.method.id, aws_api_gateway_integration.integration.id, ])) } lifecycle { create_before_destroy = true } depends_on = [aws_api_gateway_integration.integration] }
AWS Lambdaに関するリソース定義は、こちら。
## Lambda data "aws_caller_identity" "current" {} data "aws_region" "current" {} resource "aws_lambda_permission" "lambda" { statement_id = "AllowExecutionFromApiGateway" action = "lambda:InvokeFunction" function_name = aws_lambda_function.lambda.function_name principal = "apigateway.amazonaws.com" source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.rest_api.id}/*/${aws_api_gateway_method.method.http_method}${aws_api_gateway_resource.proxy_resource.path}" } data "archive_file" "lambda_source" { type = "zip" source_dir = "lambda_app" output_path = "archive/my_lambda.zip" } resource "aws_lambda_function" "lambda" { filename = data.archive_file.lambda_source.output_path function_name = "my_lambda" role = aws_iam_role.lambda_role.arn handler = "function.handler" runtime = "python3.8" source_code_hash = data.archive_file.lambda_source.output_base64sha256 } # IAM resource "aws_iam_role" "lambda_role" { name = "MyLambdaRole" assume_role_policy = data.aws_iam_policy_document.assume_role.json } data "aws_iam_policy_document" "assume_role" { statement { actions = ["sts:AssumeRole"] effect = "Allow" principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } }
作成したAWS Lambda関数のソースコードをzipにして、関連するIAMロール、パーミッションを作成してデプロイする
リソース定義です。
リソースを作成する
では、必要なファイルは作成できたので、実際のリソース作成に移ります。
Terraformの初期化を行い
$ terraform init
デプロイ。
$ terraform apply -auto-approve
すごくあっさり完了します。
Plan: 9 to add, 0 to change, 0 to destroy. aws_api_gateway_rest_api.rest_api: Creating... aws_iam_role.lambda_role: Creating... aws_api_gateway_rest_api.rest_api: Creation complete after 0s [id=1iwuj3c56v] aws_iam_role.lambda_role: Creation complete after 0s [id=MyLambdaRole] aws_api_gateway_resource.proxy_resource: Creating... aws_lambda_function.lambda: Creating... aws_api_gateway_resource.proxy_resource: Creation complete after 0s [id=qxag9cz9k5] aws_api_gateway_method.method: Creating... aws_api_gateway_method.method: Creation complete after 0s [id=agm-1iwuj3c56v-qxag9cz9k5-ANY] aws_lambda_function.lambda: Creation complete after 6s [id=my_lambda] aws_lambda_permission.lambda: Creating... aws_api_gateway_integration.integration: Creating... aws_api_gateway_integration.integration: Creation complete after 0s [id=agi-1iwuj3c56v-qxag9cz9k5-ANY] aws_api_gateway_deployment.deployment: Creating... aws_api_gateway_deployment.deployment: Creation complete after 0s [id=kyylnunoct] aws_api_gateway_stage.stage: Creating... aws_lambda_permission.lambda: Creation complete after 0s [id=AllowExecutionFromApiGateway] aws_api_gateway_stage.stage: Creation complete after 0s [id=ags-1iwuj3c56v-Prod] Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
デプロイできたら、REST APIのIDを取得して
$ REST_API_ID=`awslocal apigateway get-rest-apis --query 'items[0].id' --output text`
動作確認。
## GET $ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/request_from_query?word=WordFromQuery {"message": "Hello WordFromQuery!!", "path": "/request_from_query", "http_method": "GET"} ## POST $ curl -XPOST http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/request_post/json -d '{"word": "Lambda"}' {"message": "Hello Lambda!!", "path": "/request_post/json", "http_method": "POST"}
OKですね。
困ったこと
リソース定義がうまくできさえすればあっさりと動くのですが、そこまでにいろいろハマりまして。
実行が速すぎて、aws_api_gateway_integration
へのdepends_on
を書かないとaws_api_gateway_deployment
の
作成に失敗したり
resource "aws_api_gateway_deployment" "deployment" { ... depends_on = [aws_api_gateway_integration.integration] }
aws_api_gateway_resource
のparent_id
に間違った値を書いてしまったら応答が返ってこなくなったり
resource "aws_api_gateway_resource" "proxy_resource" { rest_api_id = aws_api_gateway_rest_api.rest_api.id parent_id = aws_api_gateway_rest_api.rest_api.root_resource_id path_part = "{proxy+}" }
Amazon CloudWatch Logsロググループを作成しようとしたらクレデンシャルでエラーになったり。
最終的には、今回書いた定義でなんとかなりました…。
オマケ
最後に、Terraformのリソース構成ファイル全体を載せておきます。
main.tf
terraform { required_version = "1.0.8" required_providers { aws = { source = "hashicorp/aws" version = "3.61.0" } archive = { source = "hashicorp/archive" version = "2.2.0" } } } provider "aws" { access_key = "mock_access_key" region = "us-east-1" s3_force_path_style = true secret_key = "mock_secret_key" skip_credentials_validation = true skip_metadata_api_check = true skip_requesting_account_id = true endpoints { apigateway = "http://localhost:4566" cloudformation = "http://localhost:4566" cloudwatch = "http://localhost:4566" dynamodb = "http://localhost:4566" es = "http://localhost:4566" firehose = "http://localhost:4566" iam = "http://localhost:4566" kinesis = "http://localhost:4566" lambda = "http://localhost:4566" route53 = "http://localhost:4566" redshift = "http://localhost:4566" s3 = "http://localhost:4566" secretsmanager = "http://localhost:4566" ses = "http://localhost:4566" sns = "http://localhost:4566" sqs = "http://localhost:4566" ssm = "http://localhost:4566" stepfunctions = "http://localhost:4566" sts = "http://localhost:4566" } } provider "archive" {} ## API Gateway resource "aws_api_gateway_rest_api" "rest_api" { name = "my-rest-api" } resource "aws_api_gateway_resource" "proxy_resource" { rest_api_id = aws_api_gateway_rest_api.rest_api.id parent_id = aws_api_gateway_rest_api.rest_api.root_resource_id path_part = "{proxy+}" } resource "aws_api_gateway_method" "method" { rest_api_id = aws_api_gateway_rest_api.rest_api.id authorization = "NONE" http_method = "ANY" resource_id = aws_api_gateway_resource.proxy_resource.id } resource "aws_api_gateway_integration" "integration" { rest_api_id = aws_api_gateway_rest_api.rest_api.id resource_id = aws_api_gateway_resource.proxy_resource.id http_method = aws_api_gateway_method.method.http_method integration_http_method = "ANY" type = "AWS_PROXY" uri = aws_lambda_function.lambda.invoke_arn } resource "aws_api_gateway_deployment" "deployment" { rest_api_id = aws_api_gateway_rest_api.rest_api.id triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_rest_api.rest_api.body, aws_api_gateway_resource.proxy_resource.id, aws_api_gateway_method.method.id, aws_api_gateway_integration.integration.id, ])) } lifecycle { create_before_destroy = true } depends_on = [aws_api_gateway_integration.integration] } resource "aws_api_gateway_stage" "stage" { deployment_id = aws_api_gateway_deployment.deployment.id rest_api_id = aws_api_gateway_rest_api.rest_api.id stage_name = "Prod" } ## Lambda data "aws_caller_identity" "current" {} data "aws_region" "current" {} resource "aws_lambda_permission" "lambda" { statement_id = "AllowExecutionFromApiGateway" action = "lambda:InvokeFunction" function_name = aws_lambda_function.lambda.function_name principal = "apigateway.amazonaws.com" source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.rest_api.id}/*/${aws_api_gateway_method.method.http_method}${aws_api_gateway_resource.proxy_resource.path}" } data "archive_file" "lambda_source" { type = "zip" source_dir = "lambda_app" output_path = "archive/my_lambda.zip" } resource "aws_lambda_function" "lambda" { filename = data.archive_file.lambda_source.output_path function_name = "my_lambda" role = aws_iam_role.lambda_role.arn handler = "function.handler" runtime = "python3.8" source_code_hash = data.archive_file.lambda_source.output_base64sha256 } # IAM resource "aws_iam_role" "lambda_role" { name = "MyLambdaRole" assume_role_policy = data.aws_iam_policy_document.assume_role.json } data "aws_iam_policy_document" "assume_role" { statement { actions = ["sts:AssumeRole"] effect = "Allow" principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } }