RitoLabo

Amazon API Gateway エンドポイント(REST API)のカスタムドメイン設定・認可・アクセス制限

  • 公開:
  • 更新:
  • カテゴリ: AWS
  • タグ: AWS,ApiGateway

サーバレスアーキテクチャ構築の第二弾です。

今回は Amazon API Gateway で作成したエンドポイントを使いやすくしたり制限をかけたりしていきます。

アジェンダ
  1. 開発環境
  2. カスタムドメインを設定する
  3. IAM 認証を導入してエンドポイントに認可制限を設ける
  4. IP Address でアクセス制限を掛ける
  5. CORS の有効化を行う

開発環境

今回の開発環境は以下になります。

  • Terraform v1.0.2

なお、今回は前回の続きになるので、操作する Amazon API Gateway については、AWS Lambda / Amazon API Gateway の連携・エンドポイント作成 で作成したものをベースに行っていきます。

(API のタイプは REST API です)

カスタムドメインを設定する

デフォルトでは 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 カスタムドメイン登録

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
date: Mon, 19 Jul 2021 13:26:06 GMT
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 リクエストに認証情報を追加する必要があります。

署名バージョン 4 の署名プロセス
https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html
署名バージョン 4 を使用した AWS リクエストへの署名
https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4_signing.html

ここはアプリケーション側の実装になりますが、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!

こちらについては以下の記事を参考にさせていただきました
https://dev.classmethod.jp/articles/api-gateway-iam-authentication-sigv4/
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);
});
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-generate-sdk-javascript.html
AWS SDK for PHP v3
/** @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 制限をかけたい場合は、リソースポリシーを用いることで実現できます。

API Gateway リソースポリシーを使用して API へのアクセスを制御する
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-resource-policies.html

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 を絡めてデータ操作周りをやっていこうと思います。