はじめに
AWS re:Invent 2024に先駆けて発表されたアップデートの一つとして、Amazon API GatewayのPrivate APIがカスタムドメインに対応しました。本記事では当該機能の設定方法に加え、実践的()な活用の一例として、内部ALBとCognitoを統合する際のALB/Cognito間の経路閉域化にチャレンジしたので、その内容をご紹介します。
当該機能に関するAWS公式情報はこちらをご参照ください:
記事の背景(読み飛ばして構いません)
Amazon API Gatewayは、AWS Lambdaと組み合わせるだけに留まらず、さまざまな使い方ができる面白いサービスだと思っています。一方で、Amazon VPCに対してAPIを提供する Private API は(ワークアラウンドがなかったわけではありませんが)長らくカスタムドメインに対応していませんでした。
筆者は某SIerに所属しており、エンタープライズのお客様とお仕事をさせていただく機会がありますが、厳格なセキュリティ要件をお持ちで、通信をできるだけAWS Direct Connect経由に寄せて閉域化したいというご要望をいただくケースが多々あります。Private APIがカスタムドメインに非対応だったことがオンプレミス向けの閉域サービスをサーバーレスアーキテクチャで実現するのを妨げる一因だったことは否めず、その観点で地味ながらもありがたいアップデートの一つだと捉えています。
ちなみに筆者がこのアップデートを知ったのは、同じくこれを待ち望んでいたお客様からのご指摘がきっかけでした。re:Invent現地参加の準備で忙しかったという言い訳はありますが、その時期のアップデートを把握するのは並大抵のことではないですね。
ともかく、アップデートを知ったからには実際に触ってみて使い方を理解したく、どうせなら別の課題と結びつけてソリューションに仕立ててみるかと思い至ったのが本記事執筆の背景です。
第1段階|まずはCustom Domain Name for Private APIを触ってみる
何はともあれ触ってみましょう。ALB/Cognito統合に興味のない場合は、この章をご覧いただくだけで十分です。
構成図
設定方法
前提
- Private APIに割り当てるカスタムドメイン用のACMパブリック証明書の発行またはインポートが完了していること
手順
- VPC、サブネット、VPCエンドポイントのセキュリティグループを作成する
- VPCエンドポイント(execute-api)を作成する
- Private APIを作成する
- 適当なリソースとメソッドを定義する
- Private APIにリソースポリシーを定義する
- Private APIをパブリッシュする
- カスタムドメイン名をAPI Gatewayに登録する(ここでACM証明書が必要)
- Private APIをカスタムドメインにマップする
- カスタムドメインにリソースポリシーを定義する
- VPCエンドポイントにカスタムドメインを関連付ける
- (任意)Route 53 Private Hosted Zoneを作成する
- (任意)PHZにカスタムドメインをVPCエンドポイントの別名(CNAME or ALIAS)として登録する
動作確認
便利で手軽なCloudShell VPC Environmentで確認してみましょう。API Gatewayで定義したレスポンスが返れば成功です。
$ curl https://<custom-domain-name>/
名前解決の方法としてRoute 53 Private Hosted Zoneを用いましたが、それ以外のDNSサーバー(Route 53 Public Hosted Zoneを含む)でもよいですし、あるいは、もっと簡易的に以下でも構いません。
curl -H 'Host: <custom-domain-name>' https://<vpce-id>-<random-id>.execute-api.<region>.amazonaws.com/
触ってみた感想
思いのほか、設定にかかる手数が多いという印象を持ちました。また、API本体だけではなくカスタムドメインにもリソースポリシーを記述するという概念がややこしいと感じました。AWSブログ を読むと同一AWSアカウント内でのAPI利用に限らず、あらゆるアカウントに対してAPIを公開することを前提に設計されていることが分かります。その点を踏まえれば、そこそこ合理的な仕様ではあるのでしょうね。
IaC
試すのに使ったSAMテンプレートはこちらです。
template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform:
- AWS::LanguageExtensions
- AWS::Serverless-2016-10-31
Description: Custom Domain Name for Private API
Outputs:
VpceDnsName:
Description: DNS Name for VPC Endpoint for API Gateway
Value: !Select
- 1
- !Split
- ":"
- !Select
- 0
- !GetAtt VpceExecuteApi.DnsEntries
Parameters:
Prefix:
Description: Prefix of Resource Name
Type: String
MinLength: "1"
VpcCidr:
Description: VPC CIDR
Type: String
MinLength: "1"
AZ1:
Description: "Availability Zone #1"
Type: AWS::EC2::AvailabilityZone::Name
AZ2:
Description: "Availability Zone #2"
Type: AWS::EC2::AvailabilityZone::Name
CustomDomainName:
Description: Custom Domain Name for REST API
Type: String
MinLength: "1"
RestApiCertArn:
Description: Certificate ARN for REST API
Type: String
AllowedPattern: "^arn:aws:acm:.+$"
RestApiStageName:
Description: Stage Name of REST API
Type: String
MinLength: "1"
Resources:
Vpc:
Type: AWS::EC2::VPC
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-VPC
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-Subnet-Private1
VpcId: !Ref Vpc
CidrBlock: !Select
- 0
- !Cidr [!GetAtt Vpc.CidrBlock, 2, 5]
AvailabilityZone: !Ref AZ1
MapPublicIpOnLaunch: false
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-Subnet-Private2
VpcId: !Ref Vpc
CidrBlock: !Select
- 1
- !Cidr [!GetAtt Vpc.CidrBlock, 2, 5]
AvailabilityZone: !Ref AZ2
MapPublicIpOnLaunch: false
VpceSg:
Type: AWS::EC2::SecurityGroup
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-SG-VPCE
GroupName: !Sub ${Prefix}-SG-VPCE
GroupDescription: for VPC Endpoint
VpcId: !Ref Vpc
SecurityGroupIngress:
- Description: from local
IpProtocol: tcp
CidrIp: 10.0.0.0/8
FromPort: 443
ToPort: 443
- Description: from local
IpProtocol: tcp
CidrIp: 172.16.0.0/12
FromPort: 443
ToPort: 443
- Description: from local
IpProtocol: tcp
CidrIp: 192.168.0.0/16
FromPort: 443
ToPort: 443
SecurityGroupEgress:
- Description: dummy
IpProtocol: "-1"
CidrIp: 127.0.0.1/32
VpceExecuteApi:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref Vpc
VpcEndpointType: Interface
ServiceName: !Sub com.amazonaws.${AWS::Region}.execute-api
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroupIds:
- !Ref VpceSg
RestApi:
Type: AWS::Serverless::Api
Properties:
Tags:
Name: !Sub ${Prefix}-RestApi
Description: Private API
Name: !Sub ${Prefix}-RestApi
StageName: !Ref RestApiStageName
EndpointConfiguration:
Type: PRIVATE
OpenApiVersion: 3.0.1
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: ./openapi.yaml
Auth:
ResourcePolicy:
CustomStatements:
- Effect: Deny
Principal: "*"
Action: execute-api:Invoke
Resource: execute-api:/*
Condition:
StringNotEquals:
aws:sourceVpc: !Ref Vpc
- Effect: Allow
Principal: "*"
Action: execute-api:Invoke
Resource: execute-api:/*
RestApiCustomDomain:
Type: AWS::ApiGateway::DomainNameV2
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-CustomDomain
DomainName: !Ref CustomDomainName
CertificateArn: !Ref RestApiCertArn
EndpointConfiguration:
Types:
- PRIVATE
Policy:
Fn::ToJsonString:
Statement:
- Effect: Deny
Principal: "*"
Action: execute-api:Invoke
Resource: execute-api:/*
Condition:
StringNotEquals:
aws:SourceVpc: !Ref Vpc
- Effect: Allow
Principal: "*"
Action: execute-api:Invoke
Resource: execute-api:/*
DomainNameAccessAssociation:
Type: AWS::ApiGateway::DomainNameAccessAssociation
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-DomainNameAccess
AccessAssociationSourceType: VPCE
AccessAssociationSource: !Ref VpceExecuteApi
DomainNameArn: !Ref RestApiCustomDomain
RestApiMapping:
# NOTE: 実際の依存性はありません。SAMがStageのデプロイを完了するまで遅延させるにあたり、VPCエンドポイントのプロビジョニング所要時間がちょうどよいというだけです。。。
DependsOn: VpceExecuteApi
Type: AWS::ApiGateway::BasePathMappingV2
Properties:
DomainNameArn: !Ref RestApiCustomDomain
RestApiId: !Ref RestApi
Stage: !Ref RestApiStageName
PrivateHostedZone:
Type: AWS::Route53::HostedZone
Properties:
HostedZoneTags:
- Key: Name
Value: !Sub ${Prefix}-PHZ
HostedZoneConfig:
Comment: !Sub ${Prefix}-PHZ
Name: !Ref CustomDomainName
VPCs:
- VPCId: !Ref Vpc
VPCRegion: !Ref AWS::Region
RecordSet:
Type: AWS::Route53::RecordSet
Properties:
Comment: !Sub ${Prefix}-RecordSet
HostedZoneId: !Ref PrivateHostedZone
Type: A
Name: !Ref CustomDomainName
AliasTarget:
HostedZoneId: !Select
- 0
- !Split
- ":"
- !Select
- 0
- !GetAtt VpceExecuteApi.DnsEntries
DNSName: !Select
- 1
- !Split
- ":"
- !Select
- 0
- !GetAtt VpceExecuteApi.DnsEntries
openapi.yaml
openapi: 3.0.1
info:
title: na
version: na
paths:
/:
get:
responses:
"200":
description: "200 response"
content:
application/json:
schema:
type: string
x-amazon-apigateway-integration:
type: mock
passthroughBehavior: when_no_templates
requestTemplates:
application/json: '{ "statusCode": 200 }'
responses:
default:
statusCode: "200"
responseTemplates:
application/json: '{ "statusCode": 200 }'
第2段階|この機能を使ってALB/Cognito統合を閉域化してみる
いよいよタイトルの課題にチャレンジします。このソリューションの骨子は、Amazon API GatewayをAmazon Cognitoに対するリバースプロキシとして構成し、それをPrivate APIとしてVPCに提供することで、ALBに対してCognitoへの代替のエンドポイントを提供することです。実現にあたってのトリックの一つがPrivate APIのカスタムドメイン機能です。
構成図
前提知識
手順の説明を始める前に、なぜこんなまどろっこしいことを考える必要があるのか前提を整理しておきます。
ALBは OIDCプロバイダーと統合する機能 を備えており、認証されたユーザーからのリクエストだけをターゲットグループに転送することが可能です。外部のIdPとともに、Cognitoユーザープールとの統合にも対応します。一方で、ユーザーガイド に掲載されている認証フロー(下図)をご覧いただくと、ALBとIdPの間で通信が発生することが分かります。
このことはALBを通常のL7-SWの範囲で使っている場合には考えなくてよい新たな考慮事項をもたらします。それは、ALBからIdPへのHTTPSの通信経路を確保する必要があるということです。
外部ALBであれば、もとよりパブリックサブネットに配置されているわけですからセキュリティグループで443/tcpへのアウトバウンドを空けるだけで済みますが、内部ALBとなるとNATゲートウェイ経由でインターネットへ接続可能なサブネット(いわゆるプロテクテッドサブネット)に配置する必要性が生じます1。インターネットへのアウトバウンドが同じVPC内で元々想定されているのであれば大きな問題はないかもしれませんが、仮にOIDC統合のためだけにインターネットゲートウェイとNATゲートウェイを配置するのだとすれば抵抗感があるかもしれませんし、まして統合先がCognitoとなればVPCエンドポイントで何とかならないのかと考えるのが人情()ではないでしょうか。
関連情報:
設定方法
前提
- ALB に割り当てるカスタムドメイン用のACM証明書の発行またはインポートが完了していること
- Private API に割り当てるカスタムドメイン用のACM証明書の発行またはインポートが完了していること
- Cognito に割り当てるカスタムドメイン用のACM証明書の発行またはインポートが us-east-1において 完了していること
手順
- Cognito User Pool、App Clientを作成する
- User Poolにカスタムドメインを登録する(ここでus-east-1のACM証明書が必要)
- 「Alias target」を控える
- User PoolのManaged Login2(旧 Hosted UI)をセットアップする
- User Poolに1人以上のユーザーを作成する
- VPC、サブネット、VPCエンドポイントとALBのセキュリティグループを作成する【再掲】
- VPCエンドポイント(execute-api)を作成する【再掲】
- Private APIを作成する【再掲】
- CognitoをバックエンドとするHTTPプロキシ統合を構成する3
- Private APIにリソースポリシーを定義する【再掲】
- Private APIをパブリッシュする【再掲】
-
手順2と同一の カスタムドメイン名をAPI Gatewayに登録する(ここでACM証明書が必要)
- Private APIをカスタムドメインにマップする【再掲】
- カスタムドメインにリソースポリシーを定義する【再掲】
- VPCエンドポイントにカスタムドメインを関連付ける【再掲】
- ALBを作成する(ここでもACM証明書が必要)
<省略> - ターゲットグループまたは固定レスポンスで、認証後に表示されるべきレスポンスを生成できるようにする
<省略> - HTTPSリスナーでCognito統合をセットアップする
- 任意のパブリック権威DNSサーバーにおいて、Private API(= Cognito)のカスタムドメイン名をVPCエンドポイントの別名として設定する
(筆者はAmazon Lightsailユーザーなのでこんな感じ4)
- 任意の手段でクライアントがALBのカスタムドメイン名を解決できるようにする(Lightsailの場合は)
動作確認
それでは実際にクライアントからALBにアクセスしてみましょう。オンプレミスを模した別VPCを用意するのがベターでしょうが、面倒なので同じVPC内にパブリックサブネットを作成して、WindowsのEC2インスタンスを起動します。
確認手順
- Fleet Managerでリモートデスクトップ接続する
- Edgeを起動してALBのカスタムドメイン名にアクセスする
- Cognito Managed Loginへリダイレクトされるので、ユーザー名と仮パスワードを入力する
(Hosted UIに比べて華やかになりましたね )
- 正式なパスワードを設定する
- ALBへ戻ってきて期待どおりの表示が出れば成功
(Managed Loginに比べて画面がしょぼすぎる )
考察
果たしてこれは閉域と言えるのか
ALB/Cognitoの通信を真に閉域化できているかというとそうではなく、VPC側のインターネット出口を不要にした一方で、API Gatewayという裏口をつくっているのも事実です。したがって、セキュリティ要件によっては、ここで示したアプローチでは要件を充足できないケースもあるはずです5。
加えて、Managed LoginまたはHosted UIの静的コンテンツ(CSS、JavaScript)はCloudFrontから配信される構成であり、クライアントがインターネットにも接続できることは依然として必要です。
VPCエンドポイントのカスタムドメイン名はPrivate Hosted Zoneでホストすればいいのではないか
そんなふうに考えていた時期が私にもありました。
すでに述べたとおり、ALBからOIDCエンドポイントへのHTTPSトラフィックはカスタマーVPCを流れますが、ALBによるCognitoエンドポイントの名前解決はカスタマーVPCを介さず、AWSマネージドの空間に完結しているようです。したがって、カスタマーVPCにPrivate Hosted Zoneを割り当てたとしてもALBがそれを参照することはなく、パブリックに名前解決できることが必須となります6。
もちろん、第1段階 で紹介した使い方においてはPrivate Hosted Zoneを使用することに何ら問題はありません。
IaC
試すのに使ったSAMテンプレートはこちらです。
template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform:
- AWS::LanguageExtensions
- AWS::Serverless-2016-10-31
Description: Internal ALB with Cognito and Private API
Outputs:
UserPoolId:
Description: User Pool ID
Value: !Ref UserPool
AlbDnsName:
Description: DNS Name for ALB
Value: !GetAtt Alb.DNSName
VpceDnsName:
Description: DNS Name for VPC Endpoint for API Gateway
Value: !Select
- 1
- !Split
- ":"
- !Select
- 0
- !GetAtt VpceExecuteApi.DnsEntries
Parameters:
Prefix:
Description: Prefix of Resource Name
Type: String
MinLength: "1"
VpcCidr:
Description: VPC CIDR
Type: String
MinLength: "1"
AZ1:
Description: "Availability Zone #1"
Type: AWS::EC2::AvailabilityZone::Name
AZ2:
Description: "Availability Zone #2"
Type: AWS::EC2::AvailabilityZone::Name
AlbCustomDomainName:
Description: Custom Domain Name for ALB
Type: String
MinLength: "1"
UserPoolCustomDomainName:
Description: Custom Domain Name for Cognito User Pool
Type: String
MinLength: "1"
AlbCertArn:
Description: Certificate ARN for ALB
Type: String
AllowedPattern: "^arn:aws:acm:.+$"
RestApiCertArn:
Description: Certificate ARN for REST API
Type: String
AllowedPattern: "^arn:aws:acm:.+$"
UserPoolCertArn:
Description: Certificate ARN for Cognito User Pool
Type: String
AllowedPattern: "^arn:aws:acm:us-east-1:.+$"
RestApiStageName:
Description: Stage Name of REST API
Type: String
MinLength: "1"
CreateEC2ForTesting:
Type: String
Default: "false"
AllowedValues:
- "true"
- "false"
KeyPairName:
Type: String
Conditions:
IsDebug: !Equals
- !Ref CreateEC2ForTesting
- "true"
Resources:
Ec2:
Condition: IsDebug
Type: AWS::EC2::Instance
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-EC2-Windows
ImageId: "{{resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2025-English-Full-Base}}"
InstanceType: t3a.medium
SubnetId: !Ref PublicSubnet
SecurityGroupIds:
- !Ref Ec2Sg
IamInstanceProfile: !Ref InstanceProfile
KeyName: !Ref KeyPairName
Ec2Sg:
Condition: IsDebug
Type: AWS::EC2::SecurityGroup
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-SG-EC2
GroupName: !Sub ${Prefix}-SG-EC2
GroupDescription: for EC2
VpcId: !Ref Vpc
Ec2Role:
Condition: IsDebug
Type: AWS::IAM::Role
Properties:
Description: EC2 Role
RoleName: !Sub ${Prefix}-Role-EC2
Path: /
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: ec2.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
InstanceProfile:
Condition: IsDebug
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub ${Prefix}-Role-EC2
Path: /
Roles:
- !Ref Ec2Role
PublicSubnet:
Condition: IsDebug
Type: AWS::EC2::Subnet
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-Subnet-Public
VpcId: !Ref Vpc
CidrBlock: !Select
- 2
- !Cidr [!GetAtt Vpc.CidrBlock, 3, 5]
AvailabilityZone: !Ref AZ1
MapPublicIpOnLaunch: true
RTA:
Condition: IsDebug
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
PublicRouteTable:
Condition: IsDebug
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-RTB-Public
VpcId: !Ref Vpc
Igw:
Condition: IsDebug
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-IGW
IgwAttachment:
Condition: IsDebug
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref Vpc
InternetGatewayId: !Ref Igw
RouteToIgw:
Condition: IsDebug
DependsOn: IgwAttachment
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
GatewayId: !Ref Igw
DestinationCidrBlock: 0.0.0.0/0
Vpc:
Type: AWS::EC2::VPC
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-VPC
CidrBlock: !Ref VpcCidr
EnableDnsHostnames: true
EnableDnsSupport: true
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-Subnet-Private1
VpcId: !Ref Vpc
CidrBlock: !Select
- 0
- !Cidr [!GetAtt Vpc.CidrBlock, 2, 5]
AvailabilityZone: !Ref AZ1
MapPublicIpOnLaunch: false
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-Subnet-Private2
VpcId: !Ref Vpc
CidrBlock: !Select
- 1
- !Cidr [!GetAtt Vpc.CidrBlock, 2, 5]
AvailabilityZone: !Ref AZ2
MapPublicIpOnLaunch: false
VpceSg:
Type: AWS::EC2::SecurityGroup
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-SG-VPCE
GroupName: !Sub ${Prefix}-SG-VPCE
GroupDescription: for VPC Endpoint
VpcId: !Ref Vpc
SecurityGroupIngress:
- Description: from local
IpProtocol: tcp
CidrIp: 10.0.0.0/8
FromPort: 443
ToPort: 443
- Description: from local
IpProtocol: tcp
CidrIp: 172.16.0.0/12
FromPort: 443
ToPort: 443
- Description: from local
IpProtocol: tcp
CidrIp: 192.168.0.0/16
FromPort: 443
ToPort: 443
SecurityGroupEgress:
- Description: dummy
IpProtocol: "-1"
CidrIp: 127.0.0.1/32
VpceExecuteApi:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref Vpc
VpcEndpointType: Interface
ServiceName: !Sub com.amazonaws.${AWS::Region}.execute-api
PrivateDnsEnabled: true
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroupIds:
- !Ref VpceSg
AlbSg:
Type: AWS::EC2::SecurityGroup
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-SG-ALB
GroupName: !Sub ${Prefix}-SG-ALB
GroupDescription: for ALB
VpcId: !Ref Vpc
SecurityGroupIngress:
- Description: from local
IpProtocol: tcp
CidrIp: 10.0.0.0/8
FromPort: 443
ToPort: 443
- Description: from local
IpProtocol: tcp
CidrIp: 172.16.0.0/12
FromPort: 443
ToPort: 443
- Description: from local
IpProtocol: tcp
CidrIp: 192.168.0.0/16
FromPort: 443
ToPort: 443
SecurityGroupEgress:
- Description: to VPC
IpProtocol: tcp
CidrIp: !GetAtt Vpc.CidrBlock
FromPort: 443
ToPort: 443
Alb:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-ALB
Name: !Sub ${Prefix}-ALB
Type: application
Scheme: internal
IpAddressType: ipv4
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroups:
- !Ref AlbSg
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref Alb
Protocol: HTTPS
Port: 443
SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
Certificates:
- CertificateArn: !Ref AlbCertArn
DefaultActions:
- Order: 1
Type: authenticate-cognito
AuthenticateCognitoConfig:
UserPoolArn: !GetAtt UserPool.Arn
UserPoolClientId: !Ref UserPoolClient
UserPoolDomain: !Ref UserPoolDomain
OnUnauthenticatedRequest: authenticate
- Order: 2
Type: fixed-response
FixedResponseConfig:
StatusCode: "200"
ContentType: text/html
MessageBody: "<html><body><h1>It works!</h1></body></html>"
UserPool:
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Type: AWS::Cognito::UserPool
Properties:
UserPoolTags:
Name: !Sub ${Prefix}-UserPool
UserPoolName: !Sub ${Prefix}-UserPool
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
AccountRecoverySetting:
RecoveryMechanisms:
- Name: admin_only
Priority: 1
EmailConfiguration:
EmailSendingAccount: COGNITO_DEFAULT
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: !Sub ${Prefix}-UserPoolClient
UserPoolId: !Ref UserPool
ExplicitAuthFlows:
- ALLOW_REFRESH_TOKEN_AUTH
SupportedIdentityProviders:
- COGNITO
AllowedOAuthFlows:
- code
AllowedOAuthScopes:
- openid
AllowedOAuthFlowsUserPoolClient: true
GenerateSecret: true
PreventUserExistenceErrors: ENABLED
CallbackURLs:
- !Sub https://${AlbCustomDomainName}/oauth2/idpresponse
LogoutURLs: []
UserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
UserPoolId: !Ref UserPool
Domain: !Ref UserPoolCustomDomainName
CustomDomainConfig:
CertificateArn: !Ref UserPoolCertArn
ManagedLoginVersion: 2
ManagedLoginBranding:
Type: AWS::Cognito::ManagedLoginBranding
Properties:
UserPoolId: !Ref UserPool
ClientId: !Ref UserPoolClient
UseCognitoProvidedValues: true
RestApi:
Type: AWS::Serverless::Api
Properties:
Tags:
Name: !Sub ${Prefix}-RestApi
Description: Private API
Name: !Sub ${Prefix}-RestApi
StageName: !Ref RestApiStageName
EndpointConfiguration:
Type: PRIVATE
OpenApiVersion: 3.0.1
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: ./openapi.yaml
Auth:
ResourcePolicy:
CustomStatements:
- Effect: Deny
Principal: "*"
Action: execute-api:Invoke
Resource: execute-api:/*
Condition:
StringNotEquals:
aws:sourceVpc: !Ref Vpc
- Effect: Allow
Principal: "*"
Action: execute-api:Invoke
Resource: execute-api:/*
RestApiCustomDomain:
Type: AWS::ApiGateway::DomainNameV2
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-CustomDomain
DomainName: !Ref UserPoolCustomDomainName
CertificateArn: !Ref RestApiCertArn
EndpointConfiguration:
Types:
- PRIVATE
Policy:
Fn::ToJsonString:
Statement:
- Effect: Deny
Principal: "*"
Action: execute-api:Invoke
Resource: execute-api:/*
Condition:
StringNotEquals:
aws:SourceVpc: !Ref Vpc
- Effect: Allow
Principal: "*"
Action: execute-api:Invoke
Resource: execute-api:/*
DomainNameAccessAssociation:
Type: AWS::ApiGateway::DomainNameAccessAssociation
Properties:
Tags:
- Key: Name
Value: !Sub ${Prefix}-DomainNameAccess
AccessAssociationSourceType: VPCE
AccessAssociationSource: !Ref VpceExecuteApi
DomainNameArn: !Ref RestApiCustomDomain
RestApiMapping:
# NOTE: 実際の依存性はありません。SAMがStageのデプロイを完了するまで遅延させるにあたり、VPCエンドポイントのプロビジョニング所要時間がちょうどよいというだけです。。。
DependsOn: VpceExecuteApi
Type: AWS::ApiGateway::BasePathMappingV2
Properties:
DomainNameArn: !Ref RestApiCustomDomain
RestApiId: !Ref RestApi
Stage: !Ref RestApiStageName
openapi.yaml
openapi: 3.0.1
info:
title: na
version: na
paths:
/{proxy+}:
x-amazon-apigateway-any-method:
parameters:
- in: path
name: proxy
required: true
schema:
type: string
x-amazon-apigateway-integration:
httpMethod: ANY
type: http_proxy
uri:
Fn::Sub: https://${UserPoolDomain.CloudFrontDistribution}/{proxy}
requestParameters:
integration.request.path.proxy: method.request.path.proxy
integration.request.header.Host:
Fn::Sub: "'${UserPoolDomain}'"
おわりに
本記事では、実例を交えてPrivate APIにカスタムドメインを付ける方法をご紹介しました。第2段階 で示した手法は非常にトリッキーであり、ここまで無理をせずに済むのが望ましく、必要に迫られればこのようなアプローチもあるというぐらいの受け止めが適切に思えます。
本記事が皆さまの理解の一助となりましたら幸いです。
-
オンプレミス経由でインターネットへ抜ける経路でも構いません。 ↩
-
What's New at AWS | Amazon Cognito がエンドユーザー体験に対する豊富なブランディングをサポートする Managed Login を導入 ↩
-
AWS Developer Guide | チュートリアル: HTTP プロキシ統合を使用して REST API を作成する ↩
-
筆者は貧乏なのでRoute 53 Hosted Zoneの$0.5/月を払う余裕がないのです Lightsail DNSは無料利用枠が豊富なのでお財布に優しいですね ↩
-
それでもCognitoエンドポイントのみに対するリバースプロキシに過ぎないので、具体的な危険があるとは思えないです。 ↩
-
The DNS entries for the endpoints must be publicly resolvable, even if they resolve to private IP addresses. ↩