KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

AWS Client VPN で API Gateway Private REST API にアクセスする方法

処方箋データ基盤チームでエンジニアをしている岩佐 (孝浩) です。 カケハシには「岩佐」さんが複数名在籍しており、社内では「わささん」と呼ばれています。

先日、社外のオンプレミス環境から API Gateway Private REST API にアクセスしたいという要件を検証する機会がありましたので、備忘録も兼ねて紹介します。

この投稿では、どのような AWS リソースを作成しているのかを、ステップごとに確認しながら進めていきます。 まずは AWS CLI で構築を行い、最後に AWS CDK のコードを掲載します。

AWS 構成

社外のオンプレミス環境と AWS は、AWS Site-to-Site VPN での接続を想定していましたが、個人で検証用の IPsec 対応 VPN ルーターを所有していなかったので、AWS Client VPN で検証しました。

クライアント認証

AWS Client VPN は、以下のクライアント認証をサポートしています。

認証方法 認証手段
Active Directory ユーザー
Mutual Authentication 証明書
Single Sign-on (SAML 連携) ユーザー

この投稿では、Mutual Authentication (証明書) を利用します。 証明書の作成と ACM へのインポート手順は割愛しますので、AWS Client VPN 公式ページの手順にしたがって証明書を用意してください。

AWS リソース

AWS CLI の実行は、AWS CloudShell の利用が便利です。

この投稿では jq を利用するため、ローカルで実行する場合、事前にインストールしてください。

VPC

VPN Client が接続する VPC を作成します。

項目
名前 aws-client-vpn-test-vpc
CIDR 172.0.0.0/16
vpc_id=$( \
  aws ec2 create-vpc \
    --cidr-block 172.0.0.0/16 \
    --tag-specifications ResourceType=vpc,Tags='[{Key=Name,Value=aws-client-vpn-test-vpc}]' \
  | jq -r '.Vpc.VpcId' \
)

VPC を作成後、DNS hostnames を有効にします。

aws ec2 modify-vpc-attribute \
  --vpc-id $vpc_id \
  --enable-dns-hostnames

Internet Gateway

VPN Client にインターネットへのアクセスを許可したい場合、Internet Gateway を作成します。

この投稿では、aws-client-vpn-test-igw という名前で作成します。

igw_id=$( \
  aws ec2 create-internet-gateway \
    --tag-specifications ResourceType=internet-gateway,Tags='[{Key=Name,Value=aws-client-vpn-test-igw}]' \
  | jq -r '.InternetGateway.InternetGatewayId' \
)

Internet Gateway を作成後、VPC にアタッチします。

aws ec2 attach-internet-gateway \
  --vpc-id $vpc_id \
  --internet-gateway-id $igw_id

Subnets

Private Subnet

VPC に Private Subnet を作成します。

項目
名前 aws-client-vpn-test-private-subnet-1a
CIDR 172.0.0.0/24
Availability Zone ap-northeast-1a
private_subnet_id=$( \
  aws ec2 create-subnet \
    --vpc-id $vpc_id \
    --cidr-block 172.0.0.0/24 \
    --availability-zone ap-northeast-1a \
    --tag-specifications ResourceType=subnet,Tags='[{Key=Name,Value=aws-client-vpn-test-private-subnet-1a}]' \
  | jq -r '.Subnet.SubnetId' \
)

Public Subnet

VPC に Public Subnet を作成します。

項目
名前 aws-client-vpn-test-public-subnet-1a
CIDR 172.0.1.0/24
Availability Zone ap-northeast-1a
public_subnet_id=$( \
  aws ec2 create-subnet \
    --vpc-id $vpc_id \
    --cidr-block 172.0.1.0/24 \
    --availability-zone ap-northeast-1a \
    --tag-specifications ResourceType=subnet,Tags='[{Key=Name,Value=aws-client-vpn-test-public-subnet-1a}]' \
  | jq -r '.Subnet.SubnetId' \
)

NAT Gateway

Private Subnet からインターネットへのアクセスを許可したい場合、NAT Gateway を Public Subnet に作成する必要があります。

この投稿では、aws-client-vpn-test-nat-gw という名前で作成します。

# EIP 作成
eip_allocation_id=$( \
  aws ec2 allocate-address \
    --tag-specifications ResourceType=elastic-ip,Tags='[{Key=Name,Value=aws-client-vpn-test-eip}]' \
  | jq -r '.AllocationId' \
)

# NAT Gateway 作成
nat_gw_id=$( \
  aws ec2 create-nat-gateway \
    --subnet-id $public_subnet_id \
    --allocation-id $eip_allocation_id \
    --tag-specifications ResourceType=natgateway,Tags='[{Key=Name,Value=aws-client-vpn-test-nat-gw}]' \
  | jq -r '.NatGateway.NatGatewayId' \
)

Route Tables

Private Subnet 用 Route Table

Private Subnet 用の Route Table を作成します。

この投稿では、aws-client-vpn-test-private-rtb という名前で作成します。

private_rtb_id=$( \
  aws ec2 create-route-table \
    --vpc-id $vpc_id \
    --tag-specifications ResourceType=route-table,Tags='[{Key=Name,Value=aws-client-vpn-test-private-rtb}]' \
  | jq -r '.RouteTable.RouteTableId' \
)

Route Table を作成後、Private Subnet にアタッチします。

private_rtb_association_id=$( \
  aws ec2 associate-route-table \
    --subnet-id $private_subnet_id \
    --route-table-id $private_rtb_id \
  | jq -r '.AssociationId' \
)

0.0.0.0/0 のトラフィックを NAT Gateway にルーティングするためのレコードを追加します。

aws ec2 create-route \
  --route-table-id $private_rtb_id \
  --destination-cidr-block 0.0.0.0/0 \
  --nat-gateway-id $nat_gw_id

Public Subnet 用 Route Table

Public Subnet 用の Route Table を作成します。

この投稿では、aws-client-vpn-test-public-rtb という名前で作成します。

public_rtb_id=$( \
  aws ec2 create-route-table \
    --vpc-id $vpc_id \
    --tag-specifications ResourceType=route-table,Tags='[{Key=Name,Value=aws-client-vpn-test-public-rtb}]' \
  | jq -r '.RouteTable.RouteTableId' \
)

Route Table を作成後、Public Subnet にアタッチします。

public_rtb_association_id=$( \
  aws ec2 associate-route-table \
    --subnet-id $public_subnet_id \
    --route-table-id $public_rtb_id \
  | jq -r '.AssociationId' \
)

0.0.0.0/0 のトラフィックを Internet Gateway にルーティングするためのレコードを追加します。

aws ec2 create-route \
  --route-table-id $public_rtb_id \
  --destination-cidr-block 0.0.0.0/0 \
  --gateway-id $igw_id

Client VPN

Client VPN Endpoint

VPN Client の接続先となるエンドポイントを作成します。

項目
名前 aws-client-vpn-test-client-vpn-endpoint
CIDR 192.168.0.0/16
クライアント認証 証明書
接続ログ OFF

ACM にインポートした証明書の ARN を、<YOUR_SERVER_CERTIFICATE_ARN><YOUR_CLIENT_CERTIFICATE_ARN> にセットして実行してください。

cvpn_endpoint_id=$( \
  aws ec2 create-client-vpn-endpoint \
    --client-cidr-block 192.168.0.0/16 \
    --server-certificate-arn <YOUR_SERVER_CERTIFICATE_ARN> \
    --authentication-options Type=certificate-authentication,MutualAuthentication={ClientRootCertificateChainArn=<YOUR_CLIENT_CERTIFICATE_ARN>} \
    --connection-log-options Enabled=false \
    --tag-specifications ResourceType=client-vpn-endpoint,Tags='[{Key=Name,Value=aws-client-vpn-test-client-vpn-endpoint}]' \
  | jq -r '.ClientVpnEndpointId' \
)

Target Network

Client VPN Endpoint に Private Subnet を紐付けます。

cvpn_association_id=$( \
  aws ec2 associate-client-vpn-target-network \
    --client-vpn-endpoint-id $cvpn_endpoint_id \
    --subnet-id $private_subnet_id \
  | jq -r '.AssociationId' \
)

紐付けが完了すると、対象のサブネットに ENI (Elastic Network Interface) が2つ作成されます。Client VPN Endpoint の作成に数分かかるため、タイミングによって表示されない場合があることにご注意ください。

aws ec2 describe-network-interfaces \
  --filters Name=subnet-id,Values=$private_subnet_id \
| jq -r '.NetworkInterfaces[].Description'

ClientVPN Endpoint resource. EndpointId: cvpn-endpoint-xxxxxxxxxxxxxxxxx
ClientVPN Endpoint resource. EndpointId: cvpn-endpoint-xxxxxxxxxxxxxxxxx

Authorization Rule

VPN Client に接続を許可する宛先のルールを作成します。

宛先 CIDR
Private Subnet 172.0.0.0/24
Internet 0.0.0.0/0
aws ec2 authorize-client-vpn-ingress \
  --client-vpn-endpoint-id $cvpn_endpoint_id \
  --target-network-cidr 172.0.0.0/24 \
  --authorize-all-groups

aws ec2 authorize-client-vpn-ingress \
  --client-vpn-endpoint-id $cvpn_endpoint_id \
  --target-network-cidr 0.0.0.0/0 \
  --authorize-all-groups

Route Table

VPC の CIDR はデフォルトで追加されています。ここでは、0.0.0.0/0 を Private Subnet (172.0.0.0/24) にルーティングするためのレコードを追加します。

aws ec2 create-client-vpn-route \
  --client-vpn-endpoint-id $cvpn_endpoint_id \
  --destination-cidr-block 0.0.0.0/0 \
  --target-vpc-subnet-id $private_subnet_id

Route Table を確認してみます。2つのルートが設定されていることが分かります。

aws ec2 describe-client-vpn-routes \
  --client-vpn-endpoint-id $cvpn_endpoint_id

{
  "Routes": [
    {
      "ClientVpnEndpointId": "cvpn-endpoint-xxxxxxxxxxxxxxxxx",
      "DestinationCidr": "172.0.0.0/16",
      "TargetSubnet": "subnet-xxxxxxxxxxxxxxxxx",
      "Type": "Nat",
      "Origin": "associate",
      "Status": {
        "Code": "creating"
      },
      "Description": "Default Route"
    },
    {
      "ClientVpnEndpointId": "cvpn-endpoint-xxxxxxxxxxxxxxxxx",
      "DestinationCidr": "0.0.0.0/0",
      "TargetSubnet": "subnet-xxxxxxxxxxxxxxxxx",
      "Type": "Nat",
      "Origin": "add-route",
      "Status": {
        "Code": "creating"
      }
    }
  ]
}

接続設定ファイル

Client VPN Endpoint に接続するための接続設定ファイルをダウンロードします。

aws ec2 export-client-vpn-client-configuration \
  --client-vpn-endpoint-id $cvpn_endpoint_id \
  --output text > ./downloaded-client-config.ovpn

ダウンロードが完了したら、downloaded-client-config.ovpn をエディタで開いて、クライアント証明書と鍵の値を追加してください。

--- downloaded-client-config.ovpn.old    2024-10-26 21:36:56
+++ downloaded-client-config.ovpn 2024-10-26 21:52:37
@@ -34,0 +35,22 @@
+<cert>
+-----BEGIN CERTIFICATE-----
+...
+-----END CERTIFICATE-----
+</cert>
@@ -35,0 +58,31 @@
+<key>
+-----BEGIN PRIVATE KEY-----
+...
+-----END PRIVATE KEY-----
+</key>
+

詳細は、公式ドキュメントをご参照ください。

Client VPN Endpoint 接続テスト

OpenVPN Client であれば、何でも利用可能なはずですが、この投稿では、AWS から提供されている公式クライアントを利用します。

AWS VPN Client を開いて、メニューの File > Manage Profiles を選択し、Add Profile をクリックします。

プロファイル名に aws-client-vpn-test を入力し、VPN 接続設定ファイルに downloaded-client-config.ovpn を選択して、プロファイルを追加します。

AWS VPN Client のプロファイル選択画面で、先ほど追加したプロファイルを選択し、Connect ボタンを押すと Client VPN Endpoint に接続できます。

接続されると、Connected と表示されます。

接続履歴を確認してみます。反映されるまで時間がかかるため、表示されない場合は時間を空けて再度ご確認ください。

aws ec2 describe-client-vpn-connections \
  --client-vpn-endpoint-id $cvpn_endpoint_id

{
    "Connections": [
        {
            "ClientVpnEndpointId": "cvpn-endpoint-xxxxxxxxxxxxxxxxx",
            "Timestamp": "2024-10-27 18:08:13",
            "ConnectionId": "cvpn-connection-xxxxxxxxxxxxxxxxx",
            "ConnectionEstablishedTime": "2024-10-27 18:05:12",
            "IngressBytes": "3861644",
            "EgressBytes": "4386234",
            "IngressPackets": "6191",
            "EgressPackets": "7172",
            "ClientIp": "192.168.0.130",
            "CommonName": "client1.domain.tld",
            "Status": {
                "Code": "active"
            },
            "ConnectionEndTime": "-"
        }
    ]
}

インターネットへの接続も確認してみます。Client VPN Endpoint に接続した状態で、KAKEHASHI Tech Blog にアクセスすると、コンテンツが表示されるはずです。

以上で、Client VPN Endpoint への接続が可能になりました。以降では API Gateway Private REST API を構築します。

API Gateway Private REST API

VPC Endpoint

API Gateway Private REST API にアクセスするための VPC Endpoint を作成します。

項目
名前 aws-client-vpn-test-private-rest-api-vpce
Type Interface
Subnet Private
Service com.amazonaws.ap-northeast-1.execute-api
vpce_id=$( \
  aws ec2 create-vpc-endpoint \
    --vpc-endpoint-type Interface \
    --vpc-id $vpc_id \
    --subnet-ids $private_subnet_id \
    --service-name com.amazonaws.ap-northeast-1.execute-api \
    --tag-specifications ResourceType=vpc-endpoint,Tags='[{Key=Name,Value=aws-client-vpn-test-private-rest-api-vpce}]' \
  | jq -r '.VpcEndpoint.VpcEndpointId' \
)

Private REST API Endpoint

上記で作成した VPC Endpoint の Endpoint ID を指定して、Private REST API Endpoint を作成します。

この投稿では、aws-client-vpn-test-private-rest-api という名前で作成します。

api_id=$( \
  aws apigateway create-rest-api \
    --name aws-client-vpn-test-private-rest-api \
    --endpoint-configuration types=PRIVATE,vpcEndpointIds=$vpce_id \
  | jq -r '.id' \
)

Resource Policy

Private REST API は、あくまでも AWS 内部ネットワークに閉じたエンドポイントであるため、Resource Policy で適切にアクセスを制限しないと、無関係の AWS アカウントの VPC からアクセスされる可能性があります。

aws:sourceVpce を利用して、上記で作成した VPC Endpoint 以外からのアクセスを拒否します。

以下のコマンドで、policy.json を生成してください。

echo '
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:ap-northeast-1:<AWS_ACCOUNT>:<REST_API_ID>/*/*/*",
      "Condition": {
        "StringNotEquals": {
          "aws:sourceVpce": "<VPCE_ID>"
        }
      }
    },
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:ap-northeast-1:<AWS_ACCOUNT>:<REST_API_ID>/*/*/*"
    }
  ]
}
' > policy.json

aws_account=$(aws sts get-caller-identity | jq -r '.Account')
sed -i "s/<AWS_ACCOUNT>/$aws_account/g" policy.json
sed -i "s/<REST_API_ID>/$api_id/g" policy.json
sed -i "s/<VPCE_ID>/$vpce_id/g" policy.json

Resource Policy を policy.json の内容で更新します。 jqpolicy.json を変数 (policy) にロードして、value= にセットしています。

policy=$(jq tostring policy.json)

aws apigateway update-rest-api \
  --rest-api-id $api_id \
  --patch-operations op=replace,path=/policy,value=$policy

Resource Policy の例については、公式ドキュメントをご参照ください。

Mock Integration

テスト用に Mock リソースを作成します。

# / の Resource ID 取得
parent_id=$(aws apigateway get-resources \
  --rest-api-id $api_id \
  | jq -r '.items[0].id' \
)

# GET method 追加
aws apigateway put-method \
  --rest-api-id $api_id \
  --resource-id $parent_id \
  --http-method GET \
  --authorization-type NONE

# MOCK integration 追加
aws apigateway put-integration \
  --rest-api-id $api_id \
  --resource-id $parent_id \
  --http-method GET \
  --type MOCK \
  --request-templates '{"application/json": "{\"statusCode\": 200}"}'

# Integration response (200 OK) 追加
aws apigateway put-integration-response \
  --rest-api-id $api_id \
  --resource-id $parent_id \
  --http-method GET \
  --status-code 200 \
  --selection-pattern '' \
  --response-templates '{"application/json": ""}'

# Method response (200 OK) 追加
aws apigateway put-method-response \
  --rest-api-id $api_id \
  --resource-id $parent_id \
  --http-method GET \
  --status-code 200 \
  --response-models '{"application/json": "Empty"}'

Deploy

上記の手順を終えたら、API をデプロイしてください。

この投稿では、api というステージ名を利用します。

aws apigateway create-deployment \
  --rest-api-id $api_id \
  --stage-name api

動作確認

VPN Client から API Gateway Private REST API にアクセスできることを確認します。

AWS CloudShell ではなく、VPN Client のターミナルで動作確認してください。

アクセス方法

API Gateway Private REST API は、VPC Endpoint からのみアクセス可能です。

You can only invoke a private API from within a VPC using a VPC endpoint.

アクセス方法は、以下の3通りあります。

アクセス方法 エンドポイント
Route 53 Alias https://{rest-api-id}-{vpce-id}.execute-api.{region}.amazonaws.com/{stage}
Private DNS Name https://{restapi-id}.execute-api.{region}.amazonaws.com/{stage}
Public DNS Hostname https://{public-dns-hostname}.execute-api.{region}.vpce.amazonaws.com/{stage}

Route 53 Alias でアクセス

以下では、Route53 Alias を利用して Private REST API にアクセスしています。

curl --dump-header - \
https://{rest-api-id}-{vpce-id}.execute-api.ap-northeast-1.amazonaws.com/api

HTTP/1.1 200 OK
...

Client VPN Endpoint に接続していない場合、レスポンスが返ってきません。dig で確認すると、Private IP に解決されていることが分かります。

% dig +short {rest-api-id}-{vpce-id}.execute-api.ap-northeast-1.amazonaws.com
172.0.0.183

Private DNS Name でアクセス

VPC Endpoint の Private DNS を有効にした場合、Private DNS Name ({restapi-id}.execute-api.{region}.amazonaws.com) を利用して Private REST API にアクセスできます。

Direct Connect や VPN で接続されたオンプレミス環境から、Private DNS Name を利用してアクセスしたい場合、Route 53 Inbound Resolver が必要です。 オンプレミス環境で、DNS クエリーを Route 53 Inbound Resolver にフォワードするよう設定することでアクセス可能になります。Route 53 Inbound Resolver は、別途コストが生じるため、この投稿では割愛します。

VPC Endpoint の Private DNS を有効にした場合、Public API のデフォルトのエンドポイントにアクセスできなくなります。詳細は、AWS Knowledge Center をご参照ください。

Public DNS Hostname でアクセス

Public DNS Hostname は、以下のコマンドで取得できます。

aws ec2 describe-vpc-endpoints \
    --vpc-endpoint-ids $vpce_id \
  | jq '.VpcEndpoints[0].DnsEntries[].DnsName'

Public DNS Hostname を利用する場合、以下のいずれかのヘッダーが必要な点にご注意ください。

ヘッダー
x-apigw-api-id REST API ID
Host <REST_API_ID>.execute-api.<AWS_REGION>.amazonaws.com

以下では、Public DNS Hostname を利用して Private REST API にアクセスしています。

x-apigw-api-id ヘッダーを指定してアクセス

curl -H 'x-apigw-api-id: <REST_API_ID>' \
--dump-header - \
https://{public-dns-hostname}.execute-api.ap-northeast-1.vpce.amazonaws.com/api

HTTP/1.1 200 OK
...

Host ヘッダーを指定してアクセス

curl -H 'Host: <REST_API_ID>.execute-api.ap-northeast-1.amazonaws.com' \
--dump-header - \
https://{public-dns-hostname}.execute-api.ap-northeast-1.vpce.amazonaws.com/api

HTTP/1.1 200 OK
...

上述のヘッダーを指定しなかった場合、403 Forbidden のレスポンスになります。

curl --dump-header - \
https://$vpce_public_dns_hostname.execute-api.ap-northeast-1.vpce.amazonaws.com/api

HTTP/1.1 403 Forbidden
...

クリーンアップ

最後に、この投稿で作成した AWS リソースをクリーンアップします。

# API Gateway
aws apigateway delete-rest-api \
  --rest-api-id $api_id

# VPC Endpoint
aws ec2 delete-vpc-endpoints \
  --vpc-endpoint-ids $vpce_id

# Client VPN Endpoint
aws ec2 disassociate-client-vpn-target-network \
  --client-vpn-endpoint-id $cvpn_endpoint_id \
  --association-id $cvpn_association_id
aws ec2 delete-client-vpn-endpoint \
  --client-vpn-endpoint-id $cvpn_endpoint_id

# NAT Gateway
aws ec2 delete-nat-gateway \
  --nat-gateway-id $nat_gw_id
# ** NAT Gateway 削除完了後に実行 **
aws ec2 release-address \
  --allocation-id $eip_allocation_id

# Route Tables
aws ec2 disassociate-route-table \
  --association-id $private_rtb_association_id
aws ec2 delete-route-table \
  --route-table-id $private_rtb_id
aws ec2 disassociate-route-table \
  --association-id $public_rtb_association_id
aws ec2 delete-route-table \
  --route-table-id $public_rtb_id

# Subnets
aws ec2 delete-subnet \
  --subnet-id $private_subnet_id
aws ec2 delete-subnet \
  --subnet-id $public_subnet_id

# Internet Gateway
aws ec2 detach-internet-gateway \
  --vpc-id $vpc_id \
  --internet-gateway-id $igw_id
aws ec2 delete-internet-gateway \
  --internet-gateway-id $igw_id

# VPC
aws ec2 delete-vpc \
  --vpc-id $vpc_id

(補足) AWS CDK コード

この投稿で作成した AWS リソースとほぼ同じ AWS リソースを作成するための CDK コードを掲載しておきます。

vpc.addClientVpnEndpointserverCertificateArnclientCertificateArn に、ACM にインポートしたクライアント認証用の証明書の ARN をセットしてください。

import * as cdk from 'aws-cdk-lib';
import { Stack } from 'aws-cdk-lib';
import {
  EndpointType,
  MockIntegration,
  RestApi,
} from 'aws-cdk-lib/aws-apigateway';
import {
  ClientVpnRouteTarget,
  type ISubnet,
  InterfaceVpcEndpoint,
  InterfaceVpcEndpointAwsService,
  IpAddresses,
  SubnetType,
  Vpc,
} from 'aws-cdk-lib/aws-ec2';
import {
  AnyPrincipal,
  Effect,
  PolicyDocument,
  PolicyStatement,
} from 'aws-cdk-lib/aws-iam';
import type { Construct } from 'constructs';

export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const { account, region } = Stack.of(this);

    // VPC with subnets and NAT gateway
    const vpc = new Vpc(this, 'Vpc', {
      vpcName: 'aws-client-vpn-test-vpc',
      ipAddresses: IpAddresses.cidr('172.0.0.0/16'),
      enableDnsSupport: true,
      enableDnsHostnames: true,
      availabilityZones: ['ap-northeast-1a'],
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'private',
          subnetType: SubnetType.PRIVATE_WITH_EGRESS,
        },
        {
          cidrMask: 24,
          name: 'public',
          subnetType: SubnetType.PUBLIC,
        },
      ],
    });

    // AWS Client VPN Endpoint
    const cvpnEndpoint = vpc.addClientVpnEndpoint('ClientVpnEndpoint', {
      cidr: '192.168.0.0/16',
      serverCertificateArn: `arn:aws:acm:${region}:${account}:certificate/<YOUR_SERVER_CERT_ID>`,
      clientCertificateArn: `arn:aws:acm:${region}:${account}:certificate/<YOUR_CLIENT_CERT_ID>`,
    });
    // Authorization rule
    cvpnEndpoint.addAuthorizationRule('AuthorizationRule', {
      cidr: '0.0.0.0/0',
    });
    // Route
    cvpnEndpoint.addRoute('Route', {
      cidr: '0.0.0.0/0',
      target: ClientVpnRouteTarget.subnet(vpc.privateSubnets.at(0) as ISubnet),
    });

    // VPC Endpoint for API Gateway
    const vpcEndpoint = new InterfaceVpcEndpoint(this, 'InterfaceVpcEndpoint', {
      vpc,
      service: InterfaceVpcEndpointAwsService.APIGATEWAY,
      subnets: {
        subnets: vpc.privateSubnets,
      },
    });

    // API Gateway private REST api
    const api = new RestApi(this, 'RestApi', {
      restApiName: 'aws-client-vpn-test-rest-api',
      endpointConfiguration: {
        types: [EndpointType.PRIVATE],
        vpcEndpoints: [vpcEndpoint],
      },
      deployOptions: {
        stageName: 'api',
      },
      policy: new PolicyDocument({
        statements: [
          new PolicyStatement({
            effect: Effect.DENY,
            principals: [new AnyPrincipal()],
            actions: ['execute-api:Invoke'],
            resources: [`arn:aws:execute-api:${region}:${account}:*/*/*/*`],
            conditions: {
              StringNotEquals: {
                'aws:sourceVpce': vpcEndpoint.vpcEndpointId,
              },
            },
          }),
          new PolicyStatement({
            effect: Effect.ALLOW,
            principals: [new AnyPrincipal()],
            actions: ['execute-api:Invoke'],
            resources: [`arn:aws:execute-api:${region}:${account}:*/*/*/*`],
          }),
        ],
      }),
    });

    // Mock integration
    api.root.addMethod(
      'GET',
      new MockIntegration({
        requestTemplates: {
          'application/json': JSON.stringify({ statusCode: 200 }),
        },
        integrationResponses: [{ statusCode: '200' }],
      }),
      {
        methodResponses: [{ statusCode: '200' }],
      },
    );
  }
}

まとめ

AWS Client VPN で API Gateway Private REST API にアクセスできることが確認できました。Direct Connect や Site-to-Site VPN で接続されたオンプレミス環境からもアクセスできるため、エンタープライズ向けのサーバーレス API 構築に有用です。

AWS 等のパブリック・クラウドが普及する前は、このような検証をするだけでも時間とコストが必要でしたが、現在では数時間あれば構築できてしまうことに改めて便利だなと感じました。

以上、お役に立てれば幸いです。