本記事はトレタアドベントカレンダーの6日目の記事です。
始めに
皆さま、こんにちは!
『劇場版ヴァイオレット・エヴァーガーデン』で映画館にいる誰よりも号泣していたトレタのエンジニア兼北条加蓮ちゃんのプロデューサーの@hiroki_tanakaです。
先日、API Gateway~Lambda構成のサーバレスシステムをterraform+Lambrollで構築しました。
本記事ではterraformを用いてのAPI Gatewayの構築とAPI Keyでの認証設定に関してお話します。
※Lambrollに関しては昨日投稿された下記の記事を参考にしてください(o*。_。)oペコッ
tech.toreta.in
システム構成
今回は下記のサーバレスアーキテクチャの基本型のようなシステム構成です。
アプリケーションからAPI Gatewayをキックして、キックしたパスに応じて対応するLambda関数を呼び出します。
アプリケーションからAPI Gatewayの認証方法はAPI Keyを用います。
API Gatewayをterraformで構築
1. Lambda関数の取得
data "aws_lambda_function" "hello_world" { function_name = "hello_world" }
dataのaws_lambda_function
を用いて定義されたLambda関数を取得します。
今回はリクエストにHello world
と書くと、レスポンスでもHello world
と返してくれるLambda関数を定義しました。
2. API Gateway REST APIの定義
resource "aws_api_gateway_rest_api" "example" { name = "example" description = "example API Gateway" }
aws_api_gateway_rest_api
を用いてAPI Gateway REST APIを定義します。
これがAPI Gatewayの大元になります。
3. API Gatewayに紐づくリソース・メソッドの定義
resource "aws_api_gateway_resource" "hello_world" { rest_api_id = aws_api_gateway_rest_api.example.id parent_id = aws_api_gateway_rest_api.example.root_resource_id path_part = "hello_world" } resource "aws_api_gateway_method" "hello_world" { rest_api_id = aws_api_gateway_rest_api.example.id resource_id = aws_api_gateway_resource.hello_world.id http_method = "POST" authorization = "NONE" api_key_required = true } resource "aws_api_gateway_method_response" "hello_world" { rest_api_id = aws_api_gateway_rest_api.example.id resource_id = aws_api_gateway_resource.hello_world.id http_method = aws_api_gateway_method.hello_world.http_method status_code = "200" response_models = { "application/json" = "Empty" } depends_on = [aws_api_gateway_method.hello_world] } resource "aws_api_gateway_integration" "hello_world" { rest_api_id = aws_api_gateway_rest_api.example.id resource_id = aws_api_gateway_resource.hello_world.id http_method = aws_api_gateway_method.hello_world.http_method integration_http_method = "POST" type = "AWS_PROXY" uri = data.aws_lambda_function.hello_world.invoke_arn }
API Gatewayに紐づくリソースやメソッド・統合リクエストを定義します。
まずaws_api_gateway_resource
でAPI Gatewayのリソース情報を定義し、大元のAPI Gateway REST APIと紐付けます。
path_part
がこのAPIリソースの最後のセグメントになります。
次にaws_api_gateway_method
でAPI GatewayのHTTPメソッド情報を定義し、API Gateway REST API・リソースと紐付けます。
http_method
はANY
でも良いのですが、ここでは明示的にLambda側にPOSTリクエストを送るのでPOST
を選択します。
そして、API Keyを用いた認証を行うため、api_key_required
をtrue
とします。
そして、aws_api_gateway_method_response
でAPI GatewayリソースのHTTPメソッドのレスポンスを定義し、API Gateway REST API・リソース・HTTPメソッドと紐付けます。
また、HTTPメソッドのレスポンスのためHTTPメソッドの後に作る必要があるため、depends_on
を用いて明示的な依存関係を宣言しています。
最後にaws_api_gateway_integration
で統合リクエストを定義し、API Gateway REST API・リソース・HTTPメソッドと紐付けます。
integration_http_method
はHTTPメソッドに合わせてPOST
を選択します。
type
は今回、API Gatewayの構築が簡単なLambda プロキシ統合を使用するAWS_PROXY
を設定します。
uri
は実行したいLambda関数のinvoke_arn
を設定します。
※補足:Lambda統合とLambdaプロキシ統合の違い
aws_api_gateway_integration
のtype
でAWS
を指定するとLambda統合・AWS_PROXY
を指定するとLambdaプロキシ統合になるが、両者の違いは下記です。
- Lambdaプロキシ統合はクライアントから連携されたAPI GatewayへのリクエストをLambda関数にraw形式でそのまま渡すことができる。
- Lambdaプロキシ統合を使用する場合、Lambda関数は決められた形式でレスポンスを返却する必要がある。
つまり、簡単に言うとLambdaプロキシ統合の場合は出来るだけLambda側で開発が完結するようにAPI Gateway側の処理を最小限にしている形になります。
4. API Gatewayのステージ及びデプロイを定義
resource "aws_api_gateway_deployment" "example" { rest_api_id = aws_api_gateway_rest_api.example.id stage_name = "example" stage_description = "timestamp = ${timestamp()}" depends_on = [ aws_api_gateway_integration.hello_world ] lifecycle { create_before_destroy = true } }
aws_api_gateway_deployment
でAPI Gatewayのステージを定義し、大元のAPI Gateway REST APIと紐付けます。
stage_name
で指定した名前がAPI GatewayのURIのセグメントに入ってきます。
aws_api_gateway_deployment
は紐づくREST APIのリソースやメソッドが変更になった場合に再デプロイされず、自身に変更が加わった時のみデプロイされます。
そこでリソースやメソッドに変更が入った場合にもステージを最新状態に保つために、stage_description
に${timestamp()}
を指定することで自身に必ず変更が加わるようにしています。
しかし、ステージは毎回replaceされるため後述するAPI Key・使用量プランとステージの紐づきがapplyの度に切れてしまう可能性があり、この紐づきが切れてしまうと正しいAPI Keyで正しいURIにアクセスしているのに404が返ってくるという事態になります。
そのため、lifecycle
のcreate_before_destroy
をtrue
にすることでそれを防止しています。
5. API Gatewayのログ出力を定義
resource "aws_api_gateway_method_settings" "example" { rest_api_id = aws_api_gateway_rest_api.example.id stage_name = aws_api_gateway_deployment.example.stage_name method_path = "*/*" settings { data_trace_enabled = true logging_level = "INFO" } }
aws_api_gateway_method_settings
ではログやモニタリングなどの設定を定義します。
settings
ブロックで詳細な設定を行います。
今回はデータトレースロギングを有効にするかのdata_trace_enabled
をtrueとして、logging_level
をINFO
とします。
(INFO
は全てのログが出力されますので、エラー時のみで良い場合はERROR
と設定すれば大丈夫です。)
また、ログの出力先ですがAPI GatewayはデプロイされるとCloudWatch Logsに自動でAPI-Gateway-Execution-Logs_{rest-api-id}/{stage_name}
というロググループが作られ、そこにログが出力されます。
6. API GatewayにLambda関数へのアクセスを許可
resource "aws_lambda_permission" "hello_world" { action = "lambda:InvokeFunction" function_name = data.aws_lambda_function.hello_world.function_name principal = "apigateway.amazonaws.com" source_arn = "${aws_api_gateway_rest_api.example.execution_arn}/*/${aws_api_gateway_method.hello_world.http_method}/${aws_api_gateway_resource.hello_world.path_part}" }
最後にaws_lambda_permission
でLambda関数へのアクセス許可をAPI Gatewayに対して定義します。
principal
で許可を与えるAWSサービスを指定しますが、今回はAPI Gatewayなのでapigateway.amazonaws.com
とします。
(S3に与える場合はs3.amazonaws.com
・SNSに与える場合はsns.amazonaws.com
といった形で付与します。)
API Keyの設定
resource "aws_api_gateway_api_key" "example" { name = "example_api_key" enabled = true } resource "aws_api_gateway_usage_plan" "example" { name = "example_usage_plan" depends_on = [aws_api_gateway_deployment.example] api_stages { api_id = aws_api_gateway_rest_api.example.id stage = aws_api_gateway_deployment.example.stage_name } } resource "aws_api_gateway_usage_plan_key" "example" { key_id = aws_api_gateway_api_key.example.id key_type = "API_KEY" usage_plan_id = aws_api_gateway_usage_plan.example.id }
まずaws_api_gateway_api_key
でAPI Keyを定義します。
次にAPI Keyは使用量プラン経由で使用するため、aws_api_gateway_usage_plan
で使用量プランを定義します。
api_stages
で使用量プランとAPI Gatewayのステージの紐付けを行います。
そのため、使用量プランの定義前にステージが出来上がっている必要があるので、depends_on
でaws_api_gateway_deployment
を指定しています。
最後にaws_api_gateway_usage_plan_key
でAPI Keyと使用量プランの関連を定義します。
key_type
は現在、API_KEY
のみ使用できます。
構築結果
これまでで必要なterraformのコードで準備は出来ました。
なので、いざapply!
- API Gatewayリソースのメソッド
API Keyでの認証が必須となっています。
- API Gatewayリソースのメソッドの詳細
Lambda関数:hello_world
と正常に接続しています。
- API Gatewayステージ
ログ出力設定及びURIのセグメント設定がterraformで定義した通りとなっています。
- 使用量プラン
使用量プランを介してAPI Gatewayステージ及びAPI Keyと紐付いています。
ここまでマネジメントコンソール上で確認すると正常に出来ているように思えるので、最終確認としてコンソールからcurlコマンドを実行してみます。
$curl -X POST "https://{api-id}.execute-api.ap-northeast-1.amazonaws.com/example/hello_world" -d "{"hello_world"}" --header 'x-api-key: {api_key}' {"body":"{hello_world}","time":"2020-12-02T10:07:37.703201622Z"}
正常にAPI Gateway経由でLambda関数が実行され、期待通りのレスポンスが戻ってきました。
無事にAPI Key認証を用いたAPI Gateway+Lambda構成、上手に出来ました!
終わりに
terraformを用いてのAPI Gatewayの構築は公式サイトにそれぞれのresourceのリファレンスが充実していることもあって、比較的簡単に構築することが出来ました。
今後はより応用的なAPI Gatewayの構築も行っていき、API Gatewayの様々な機能を使ってみたいです。
例えば「認証でCognito認証やBasic認証を使用してみたい」や「今回ドメインをAWSデフォルトのものをそのままで運用したので独自ドメインでの運用したい」などです。
そして、今回の構築を踏まえて今更ながらサーバレスアーキテクチャの世界に入門できたと思っているので、これからは色々なサーバレスシステムに携わって運用負荷の低減に貢献していきたいです!
終わりの終わりに
トレタに少しでも興味を持っていただいた方がいれば、ぜひ遊びに来てくださいヽ(*´∀`)/
仲間も募集しています!