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

この記事は個人ブログと同じ内容です

www.ritolab.com


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

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

開発環境

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

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
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 リクエストに認証情報を追加する必要があります。

docs.aws.amazon.com

docs.aws.amazon.com

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

こちらについては以下の記事を参考にさせていただきました

dev.classmethod.jp

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);
            });

docs.aws.amazon.com

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!"

指定している AWSACCESS_KEY と SECRET_KEY は、API Gateway へのリクエスタとして(このセクションの冒頭で)作成した IAM ユーザーのものになります。

IP Address でアクセス制限を掛ける

API Gateway のエンドポイントに対して IP 制限をかけたい場合は、リソースポリシーを用いることで実現できます。

docs.aws.amazon.com

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