この記事は個人ブログと同じ内容です
サーバレスアーキテクチャ構築の第二弾です。
今回は Amazon API Gateway で作成したエンドポイントを使いやすくしたり制限をかけたりしていきます。
開発環境
今回の開発環境は以下になります。
Terraform v1.0.2
なお、今回は前回の続きになるので、操作する Amazon API Gateway については、AWS Lambda / Amazon API Gateway の連携・エンドポイント作成 で作成したものをベースに行っていきます。
カスタムドメインを設定する
デフォルトでは API Gateway で作成されるエンドポイントは以下のようになります。
https://<REST_API_ID>.execute-api.<REGION>.amazonaws.com/<LAMBDA_FUNCTION_NAME>/<RESOURCE_NAME>...
これをカスタムドメインを設定することで以下のように短くする事ができます。
https://<CUSTOM_DOMAIN>/<RESOURCE_NAME>...
以下、カスタムドメインを設定していきます。
ACM 証明書発行
ACM で証明書を発行します。ネイキッドドメインは Route 53 に登録済みの前提です。
main.tf
locals { api_gateway_sub_domain_name = "${var.sub_domain_host_name}.${var.domain_name}" } # 作成済みホストゾーン情報の取得 data "aws_route53_zone" "main" { name = var.domain_name } # ACM 証明書作成 resource "aws_acm_certificate" "APIGateway" { domain_name = local.api_gateway_sub_domain_name validation_method = "DNS" tags = { Name = var.domain_name } } ## ACM 検証用 CNAME レコード resource "aws_route53_record" "api_gateway_acm_c" { for_each = { for d in aws_acm_certificate.APIGateway.domain_validation_options : d.domain_name => { name = d.resource_record_name record = d.resource_record_value type = d.resource_record_type } } zone_id = data.aws_route53_zone.main.zone_id name = each.value.name type = each.value.type records = [each.value.record] ttl = 60 allow_overwrite = true } ## ACM 証明書 / CNAME レコード 連携 resource "aws_acm_certificate_validation" "APIGateway" { certificate_arn = aws_acm_certificate.APIGateway.arn validation_record_fqdns = [for record in aws_route53_record.api_gateway_acm_c : record.fqdn] depends_on = [ aws_acm_certificate.APIGateway, aws_route53_record.api_gateway_acm_c ] }
API Gateway カスタムドメイン登録
main.tf
# カスタムドメイン resource "aws_api_gateway_domain_name" "main" { domain_name = local.api_gateway_sub_domain_name regional_certificate_arn = aws_acm_certificate.APIGateway.arn security_policy = "TLS_1_2" endpoint_configuration { types = ["REGIONAL"] } } ## API マッピング ### ドメイン名から API ステージへのパスをマッピング resource "aws_api_gateway_base_path_mapping" "main" { api_id = aws_api_gateway_rest_api.to_lambda_node.id stage_name = aws_api_gateway_stage.hello_world.stage_name domain_name = aws_api_gateway_domain_name.main.domain_name } # route53 A レコード作成 resource "aws_route53_record" "APIGatewayCustomDomain" { name = aws_api_gateway_domain_name.main.domain_name type = "A" zone_id = data.aws_route53_zone.main.id alias { evaluate_target_health = true name = aws_api_gateway_domain_name.main.regional_domain_name zone_id = aws_api_gateway_domain_name.main.regional_zone_id } }
カスタムドメイン名を登録したら API のマッピングを行い、カスタムドメイン名を A レコードで登録(ルーティング先に API Gateway ドメイン名を指定)します。
これでエンドポイントがカスタムドメインを用いた URI になります。
https://<CUSTOM_DOMAIN_NAME>/hello_world
こんな感じです。
IAM 認証を導入してエンドポイントに認可制限を設ける
前回までの時点ではこのエンドポイントはどこから・誰からでも叩ける状態なので、必要に応じて何らかの制限を掛けてあげる必要があります。
サーバレスという事で今回のアプリケーションを静的サイトと想定した場合、S3 にホスティングして CloudFront で配信の構成が多いと考えます。
その場合 VPC や IP で制限を掛けるといった事が難しいので、IAM 認証を導入して API Gateway で作成したエンドポイントに認可制限を掛けたいと思います。
IAM ユーザー作成
まずはエンドポイントにリクエストする際の IAM ユーザーを作成します。
main.tf
# IAM Role for ApiGateway ## AWS管理ポリシー data "aws_iam_policy" "AmazonAPIGatewayInvokeFullAccess" { arn = "arn:aws:iam::aws:policy/AmazonAPIGatewayInvokeFullAccess" } # IAM User for ApiGateway execution resource "aws_iam_user" "api_gateway_requester" { name = "api_gateway_requester" } resource "aws_iam_user_policy_attachment" "api_gateway_requester" { user = aws_iam_user.api_gateway_requester.name policy_arn = data.aws_iam_policy.AmazonAPIGatewayInvokeFullAccess.arn }
今回はテストなのでポリシーは AWS 管理ポリシーである AmazonAPIGatewayInvokeFullAccess を使用しています。
サービスやエンドポイントによって細かくリクエストを制限したい場合は管理ポリシーではなく個別にリソースを指定します。
メソッドの authorization を IAM 認証へ変更
次に、作成したメソッド(GET)の authorization を IAM 認証へ変更します
main.tf
### メソッド設定 resource "aws_api_gateway_method" "hello_world" { rest_api_id = aws_api_gateway_rest_api.to_lambda_node.id resource_id = aws_api_gateway_resource.hello_world.id http_method = "GET" authorization = "AWS_IAM" // <- ここを NONE から AWS_IAM へ変更 }
これまで NONE としていた部分を AWS_IAM へ変更します。
動作確認
AWS 側の設定はこれだけなので、動作確認を行なってみます。
まずはローカルのターミナルから curl コマンドを叩いてみた結果です。
% curl -IX GET https://<CUSTOM_DOMAIN_NAME>/hello_world HTTP/2 403 content-type: application/json content-length: 42 x-amzn-requestid: xxxx-xxx-xxx-xxxx x-amzn-errortype: MissingAuthenticationTokenException x-amz-apigw-id: xxxxxxxxxxxxx
認可エラーを示す HTTP ステータスコード 403 が返ってきたので、しっかり制限がかかっている事が確認できます。
一方で、IAM 認証を行なった上でリクエストを行えば、認可で弾かれずにエンドポイントにアクセスする事ができます。
SigV4 - API リクエストに認証情報を追加する
AWS API Gateway で作成したエンドポイントのリクエストで IAM 認証を通すためには、Signature Version 4 (SigV4) を用いて AWS API リクエストに認証情報を追加する必要があります。
ここはアプリケーション側の実装になりますが、JavaScript 及び PHP の AWS SDK を用いて実装してみたので参考までに記します。
AWS SDK for JavaScript v2(SigV4 部分を抜粋)
import aws from 'aws-sdk' import core from 'aws-sdk/lib/core' ----------------------------------------------- const awsAccessKey = '<AWS_ACCESS_KEY>'; const awsSecretKey = '<AWS_SECRET_KEY>' const awsRegion = '<AWS_REGION>'; const awsComponentServiceName = "execute-api"; const awsAPiGatewayHelloWorldUrl = 'https://<CUSTOM_DOMAIN>/hellow_world'; const splits = awsAPiGatewayHelloWorldUrl.split('?'); const host = splits[0].substr(8, splits[0].indexOf("/", 8) - 8); const path = splits[0].substr(splits[0].indexOf("/", 8)); const query = splits[1]; const options = { url: awsAPiGatewayHelloWorldUrl, region: awsRegion, method: 'GET', headers: { host: host, }, pathname: () => path, search: () => query ? query : '', }; const signer = new core.Signers.V4(options, awsComponentServiceName); signer.addAuthorization(new aws.Credentials(awsAccessKey, awsSecretKey), new Date()); const response = await axios.get(awsAPiGatewayHelloWorldUrl, { 'headers': { 'authorization': options.headers['Authorization'], 'x-amz-date': options.headers['X-Amz-Date'] } }); // => Hello from Lambda!
こちらについては以下の記事を参考にさせていただきました
API Gateway で生成された JavaScript SDK を使用
var apigClient = apigClientFactory.newClient({ accessKey: awsAccessKey, secretKey: awsSecretKey, region: awsRegion }); apigClient.helloWorldGet() .then(function(result) { console.log(result.data); // => Hello from Lambda! }) .catch(function(result) { console.log(result); });
/** @var \GuzzleHttp\Client $client */ $client = new Client(); /** @var \GuzzleHttp\Psr7\Request $request */ $request = new Request('GET', 'https://<CUSTOM_DOMAIN>/hellow_world'); /** @var \Aws\Credentials\Credentials $credentials */ $credentials = new Credentials('<AWS_ACCESS_KEY>', '<AWS_SECRET_KEY>'); /** @var \Aws\Signature\SignatureV4 $signer */ $signer = new SignatureV4('execute-api', 'ap-northeast-1'); // クレデンシャルを使用し必要なヘッダーをリクエストに追加することで指定されたリクエストに SigV4 で署名します。 $requestWithSign = $signer->signRequest($request, $credentials); // エンドポイントへ HTTP リクエストを送信 $response = $client->send($requestWithSign); $response->getBody()->getContents() // -> "Hello from Lambda!"
指定している AWS の ACCESS_KEY と SECRET_KEY は、API Gateway へのリクエスタとして(このセクションの冒頭で)作成した IAM ユーザーのものになります。
IP Address でアクセス制限を掛ける
API Gateway のエンドポイントに対して IP 制限をかけたい場合は、リソースポリシーを用いることで実現できます。
main.tf
resource "aws_api_gateway_rest_api_policy" "to_lambda_node" { rest_api_id = aws_api_gateway_rest_api.to_lambda_node.id policy = jsonencode({ Version : "2012-10-17", Statement : [ { Effect : "Allow", Principal : "*", Action : "execute-api:Invoke", Resource : "arn:aws:execute-api:${var.aws_region}:${var.aws_id}:${aws_api_gateway_rest_api.to_lambda_node.id}/*", Condition : { // IP アドレス制限 "IpAddress": { "aws:SourceIp": "xx.xxx.xx.xxx/32" } } } ] }) }
CORS の有効化を行う
作成したリソースの CORS を有効化するのなら、terraform モジュール api-gateway-enable-cors が簡単でいい感じに使えます。
OPTIONS メソッドを追加してクロスオリジンリソースシェアリング(CORS)プリフライトリクエストを許可する Terraform モジュールです。
main.tf
## CORS 設定 module "cors" { source = "squidfunk/api-gateway-enable-cors/aws" version = "0.3.3" api_id = aws_api_gateway_rest_api.to_lambda_node.id api_resource_id = aws_api_gateway_resource.hello_world.id allow_headers = ["Content-Type", "authorization", "x-amz-date"] allow_methods = ["OPTIONS", "GET"] allow_origin = "*" }
必要な項目を設定すると、OPTIONS メソッドが作成され CORSプリフライトリクエストが可能になります。
今回はここまでになります。Amazon API Gateway で作成したエンドポイントの方はあらかた使いやすくなったので、次回は DynamoDB を絡めてデータ操作周りをやっていこうと思います。