トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。

terraformでAPI Gatewayを構築してAPI Key認証を設定してみた

本記事はトレタアドベントカレンダーの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を用います。

f:id:hiroki_tanaka:20201130141152j:plain

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_methodANYでも良いのですが、ここでは明示的にLambda側にPOSTリクエストを送るのでPOSTを選択します。 そして、API Keyを用いた認証を行うため、api_key_requiredtrueとします。

そして、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_integrationtypeAWSを指定すると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が返ってくるという事態になります。
そのため、lifecyclecreate_before_destroytrueにすることでそれを防止しています。

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_levelINFOとします。
(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_onaws_api_gateway_deploymentを指定しています。

最後にaws_api_gateway_usage_plan_keyでAPI Keyと使用量プランの関連を定義します。
key_typeは現在、API_KEYのみ使用できます。

構築結果

これまでで必要なterraformのコードで準備は出来ました。
なので、いざapply!

  • API Gatewayリソースのメソッド
    API Keyでの認証が必須となっています。

f:id:hiroki_tanaka:20201202185254p:plain

  • API Gatewayリソースのメソッドの詳細
    Lambda関数:hello_worldと正常に接続しています。

f:id:hiroki_tanaka:20201202185827p:plain

  • API Gatewayステージ
    ログ出力設定及びURIのセグメント設定がterraformで定義した通りとなっています。

f:id:hiroki_tanaka:20201202185639p:plain

  • 使用量プラン
    使用量プランを介してAPI Gatewayステージ及びAPI Keyと紐付いています。

f:id:hiroki_tanaka:20201202190119p:plain f:id:hiroki_tanaka:20201202190156p:plain

ここまでマネジメントコンソール上で確認すると正常に出来ているように思えるので、最終確認としてコンソールから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デフォルトのものをそのままで運用したので独自ドメインでの運用したい」などです。
そして、今回の構築を踏まえて今更ながらサーバレスアーキテクチャの世界に入門できたと思っているので、これからは色々なサーバレスシステムに携わって運用負荷の低減に貢献していきたいです!

終わりの終わりに

トレタに少しでも興味を持っていただいた方がいれば、ぜひ遊びに来てくださいヽ(*´∀`)/
仲間も募集しています!

© Toreta, Inc.

Powered by Hatena Blog