2025年 10月15日の ALB アップデートで、URL とホストヘッダの書き換えができるようになりました。以前までこのようなことを行いたい場合、別途 nginx などでリバースプロキシを構成した EC2 や ECS のコンピューティングサービスのデプロイが必要でしたが、それが不要となるため、構成次第ではサーバーレス化が可能です。
AWS 公式ブログでもこの機能を応用し、S3 バケット名のドメイン名一致を ALB カスタムドメイン名ではなく、ホストヘッダ書き換え機能で行うことで、任意のドメイン名で S3 Interface VPC Endpoint リクエストを正常に行う構成が紹介されていました。
この記事を見てピンとくる方もいると思います。S3 エンドポイントと同様に、VPC エンドポイントリクエスト時、URL/ホストヘッダに制約のあるサービスがあります。API Gateway のプライベート API と、それで必要になる execute-api の VPC エンドポイントですね。
プライベート API でのカスタムドメイン名自体は去年のアップデートで利用できるようになったため、わざわざ ALB + URL/ホストヘッダ書き換えをせずとも、API Gateway の機能のみで完結できます。
ALB の転送先に、事実上 API Gateway も加わったことが肝でして、外部公開サービスの IP アドレス固定化パターンの1つである NLB と組み合わせることで、Elastic IP による API Gateway の IP アドレス固定化が可能になりました。
実を言えば、API Gateway の IP アドレス固定化自体は以前から可能で、Global Accelerator + ALB による構成が過去の AWS ブログで紹介されています。
ただし、手順で ACM や API Gataway カスタムドメインをセットアップしていたり、Private 型 API Gateway の仕様からもわかる通り、リクエスト段階でホスト名を合致させる必要があります。本アップデートによるアクセス方法は ALB 側でホスト名を書き換えるため、極端な例、DNS による名前解決を飛ばして、固定 IP アドレスで直接 API Gateway へのリクエストが可能です。
弊社ですが、サーバーレス化を積極的に進められており、内製部分のコンピューティングサービスは大多数が Lambda で上で実行されています。一方、過去には特別な事情で DNS 名前解決ができない、固定 IP アドレスの指定で API を呼び出す必要のある案件がございました。
API Gateway の仕組み上、これを完全なサーバーレスで行うことは難しく、やむを得ず NLB + ALB + EC2 or ESC on Fargate /w Auto Scaling 構成をとり、Lambda と同等の処理を実装 or API Gateway への転送処理を行っていました。
それが本アップデートによって、そこそこお安い料金 & VPC リソースこそあれどもサーバーレスでの IP アドレス固定化 & 任意のドメイン名 or IP アドレス指定で API Gateway を呼び出すことができるようになったというわけで、個人的には非常にありがたいアップデートでした。
検証のため、CloudFormation テンプレートを作成しました。
CloudFormation テンプレート
AWSTemplateFormatVersion: 2010-09-09
Transform:
- AWS::LanguageExtensions
Parameters:
# Security Group
AllowedCidr:
Type: String
EnableHttp:
Type: String
AllowedValues:
- 'false'
- 'true'
Default: 'false'
AllowBypassNlb:
Type: String
AllowedValues:
- 'false'
- 'true'
Default: 'false'
NlbPortsHttps:
Type: CommaDelimitedList
Default: '443'
NlbPortsHttp:
Type: CommaDelimitedList
Default: '80'
# API Gateway & VPC Endpoint
ApiGwRestApiId:
Type: String
ApiGatewayStageName:
Type: String
VpceExecuteApiIPv4AddressAz1:
Type: String
VpceExecuteApiAzName1:
Type: AWS::EC2::AvailabilityZone::Name
VpceExecuteApiIPv4AddressAz2:
Type: String
VpceExecuteApiAzName2:
Type: AWS::EC2::AvailabilityZone::Name
# ELB
VpcId:
Type: AWS::EC2::VPC::Id
ElbSubnetId1:
Type: AWS::EC2::Subnet::Id
ElbSubnetId2:
Type: AWS::EC2::Subnet::Id
DomainName:
Type: String
HostedZoneId:
#Type: AWS::Route53::HostedZone::Id
Type: String
EipAllocIdNlb1:
Type: String
AllowedPattern: '(^eipalloc-[0-9a-fA-F]{8,17}$|^$)'
EipAllocIdNlb2:
Type: String
AllowedPattern: '(^eipalloc-[0-9a-fA-F]{8,17}$|^$)'
# Resource Names
AlbName:
Type: String
AllowedPattern: '(^[a-z][a-z0-9-]{0,31}$|^$)'
NlbName:
Type: String
AllowedPattern: '(^[a-z][a-z0-9-]{0,31}$|^$)'
Conditions:
AllowBypassNlb: !Equals [ !Ref AllowBypassNlb, 'true' ]
EnableHttps: !Not [ !Equals [ !Ref DomainName, '' ] ]
EnableHttp: !Equals [ !Ref EnableHttp, 'true' ]
AllowBypassNlbHttps: !And
- !Condition AllowBypassNlb
- !Condition EnableHttps
AllowBypassNlbHttp: !And
- !Condition AllowBypassNlb
- !Condition EnableHttp
EnableEipAllocIdNlb1: !Not [ !Equals [ !Ref EipAllocIdNlb1, '' ] ]
EnableEipAllocIdNlb2: !Not [ !Equals [ !Ref EipAllocIdNlb2, '' ] ]
DeployR53Records: !And
- !Condition EnableHttps
- !Not [ !Equals [ !Ref HostedZoneId, '' ] ]
IsAlbNameSpecified: !Not [ !Equals [ !Ref AlbName, '' ] ]
IsNlbNameSpecified: !Not [ !Equals [ !Ref NlbName, '' ] ]
Resources:
# https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-update-security-groups.html
SgAlb:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub ${AWS::StackName}-sg-alb
VpcId: !Ref VpcId
SgAlbIngressIcmp:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SgAlb
IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: !Ref AllowedCidr
SgAlbIngressNlbHttps: # listener & forwarding health check
Type: AWS::EC2::SecurityGroupIngress
Condition: EnableHttps
Properties:
GroupId: !Ref SgAlb
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref SgNlb
SgAlbIngressNlbHttp: # listener & forwarding health check
Type: AWS::EC2::SecurityGroupIngress
Condition: EnableHttp
Properties:
GroupId: !Ref SgAlb
IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref SgNlb
SgAlbIngressBypassNlbHttps:
Type: AWS::EC2::SecurityGroupIngress
Condition: AllowBypassNlbHttps
Properties:
GroupId: !Ref SgAlb
IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !Ref AllowedCidr
SgAlbIngressBypassNlbHttp:
Type: AWS::EC2::SecurityGroupIngress
Condition: AllowBypassNlbHttp
Properties:
GroupId: !Ref SgAlb
IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: !Ref AllowedCidr
SgAlbEgressToVpceExecuteApiAz1: # forwarding & health check
Type: AWS::EC2::SecurityGroupEgress
Properties:
GroupId: !Ref SgAlb
IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !Sub ${VpceExecuteApiIPv4AddressAz1}/32
SgAlbEgressToVpceExecuteApiAz2: # forwarding & health check
Type: AWS::EC2::SecurityGroupEgress
Properties:
GroupId: !Ref SgAlb
IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !Sub ${VpceExecuteApiIPv4AddressAz2}/32
# https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/network/load-balancer-security-groups.html
SgNlb:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub ${AWS::StackName}-sg-nlb
VpcId: !Ref VpcId
SgNlbIngressIcmp:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SgNlb
IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: !Ref AllowedCidr
'Fn::ForEach::SgNlbIngressHttpsNlbPortsHttps':
- ForEachNlbPortsHttps
- !Ref NlbPortsHttps
- 'SgNlbIngressHttps${ForEachNlbPortsHttps}':
Type: AWS::EC2::SecurityGroupIngress
Condition: EnableHttps
Properties:
GroupId: !Ref SgNlb
IpProtocol: tcp
FromPort: !Ref ForEachNlbPortsHttps
ToPort: !Ref ForEachNlbPortsHttps
CidrIp: !Ref AllowedCidr
'Fn::ForEach::SgNlbIngressHttpNlbPortsHttp':
- ForEachNlbPortsHttp
- !Ref NlbPortsHttp
- 'SgNlbIngressHttp${ForEachNlbPortsHttp}':
Type: AWS::EC2::SecurityGroupIngress
Condition: EnableHttp
Properties:
GroupId: !Ref SgNlb
IpProtocol: tcp
FromPort: !Ref ForEachNlbPortsHttp
ToPort: !Ref ForEachNlbPortsHttp
CidrIp: !Ref AllowedCidr
SgNlbEgressToAlbListnerHttps: # forwarding & health check
Type: AWS::EC2::SecurityGroupEgress
Condition: EnableHttps
Properties:
GroupId: !Ref SgNlb
IpProtocol: tcp
FromPort: 443
ToPort: 443
DestinationSecurityGroupId: !Ref SgAlb
SgNlbEgressToAlbListnerHttp: # forwarding & health check
Type: AWS::EC2::SecurityGroupEgress
Condition: EnableHttp
Properties:
GroupId: !Ref SgNlb
IpProtocol: tcp
FromPort: 80
ToPort: 80
DestinationSecurityGroupId: !Ref SgAlb
TgVpceExecuteApi:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
TargetType: ip
Protocol: HTTPS
Port: 443
VpcId: !Ref VpcId
ProtocolVersion: HTTP1 # https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-private-apis.html
HealthCheckEnabled: true
HealthCheckProtocol: HTTPS # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-group-routing-configuration
HealthCheckPath: /ping # https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-known-issues.html#api-gateway-known-issues-rest-and-websocket-apis
HealthCheckPort: 443
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
HealthCheckTimeoutSeconds: 29
HealthCheckIntervalSeconds: 30
Matcher:
HttpCode: '200'
Targets:
-
Id: !Ref VpceExecuteApiIPv4AddressAz1
Port: 443
AvailabilityZone: !Ref VpceExecuteApiAzName1
-
Id: !Ref VpceExecuteApiIPv4AddressAz2
Port: 443
AvailabilityZone: !Ref VpceExecuteApiAzName2
Alb:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Type: application
Name: !If [ IsAlbNameSpecified, !Ref AlbName, !Ref AWS::NoValue ]
Scheme: internet-facing
IpAddressType: ipv4
Subnets: [ !Ref ElbSubnetId1, !Ref ElbSubnetId2 ]
SecurityGroups:
- !Ref SgAlb
CertificateALB:
Type: AWS::CertificateManager::Certificate
Condition: EnableHttps
Properties:
DomainName: !Ref DomainName
SubjectAlternativeNames:
- !Sub "*.${DomainName}"
ValidationMethod: DNS
DomainValidationOptions:
!If
- DeployR53Records
-
-
DomainName: !Ref DomainName
HostedZoneId: !Ref HostedZoneId
-
!Ref AWS::NoValue
KeyAlgorithm: EC_secp384r1
CertificateTransparencyLoggingPreference: ENABLED
AlbListenerHttps:
Type: AWS::ElasticLoadBalancingV2::Listener
Condition: EnableHttps
Properties:
LoadBalancerArn: !Ref Alb
Protocol: HTTPS
Port: 443
DefaultActions:
-
Order: 1
Type: fixed-response
FixedResponseConfig:
StatusCode: 404
ContentType: text/plain
MessageBody: ''
Certificates:
- CertificateArn: !Ref CertificateALB
AlbListenerHttpsRuleForwardToVpceExecuteApi:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Condition: EnableHttps
Properties:
ListenerArn: !Ref AlbListenerHttps
Priority: 2
Conditions:
-
Field: path-pattern
Values:
- /*
Transforms:
-
Type: host-header-rewrite
HostHeaderRewriteConfig:
Rewrites:
# https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-private-api-test-invoke-url.html#apigateway-private-api-invoke-without-custom-domain-name
-
Regex: ^(.*)$
Replace: !Sub ${ApiGwRestApiId}.execute-api.${AWS::Region}.amazonaws.com
-
Type: url-rewrite
UrlRewriteConfig:
Rewrites:
-
Regex: ^/(.*)$
Replace: !Sub /${ApiGatewayStageName}/$1
Actions:
-
Type: forward
ForwardConfig:
TargetGroups:
-
TargetGroupArn: !Ref TgVpceExecuteApi
Weight: 100
TargetGroupStickinessConfig:
Enabled: false
AlbListenerHttpsRuleForwardToVpceExecuteApiPing:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Condition: EnableHttps
Properties:
ListenerArn: !Ref AlbListenerHttps
Priority: 1
Conditions:
-
Field: path-pattern
Values:
- /ping
Transforms:
-
Type: host-header-rewrite
HostHeaderRewriteConfig:
Rewrites:
-
Regex: ^(.*)$
Replace: !Sub ${ApiGwRestApiId}.execute-api.${AWS::Region}.amazonaws.com
Actions:
-
Type: forward
ForwardConfig:
TargetGroups:
-
TargetGroupArn: !Ref TgVpceExecuteApi
Weight: 100
TargetGroupStickinessConfig:
Enabled: false
AlbListenerHttp:
Type: AWS::ElasticLoadBalancingV2::Listener
Condition: EnableHttp
Properties:
LoadBalancerArn: !Ref Alb
Protocol: HTTP
Port: 80
DefaultActions:
-
Order: 1
Type: fixed-response
FixedResponseConfig:
StatusCode: 404
ContentType: text/plain
MessageBody: ''
AlbListenerHttpRuleForwardToVpceExecuteApi:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Condition: EnableHttp
Properties:
ListenerArn: !Ref AlbListenerHttp
Priority: 2
Conditions:
-
Field: path-pattern
Values:
- /*
Transforms:
-
Type: host-header-rewrite
HostHeaderRewriteConfig:
Rewrites:
# https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-private-api-test-invoke-url.html#apigateway-private-api-invoke-without-custom-domain-name
-
Regex: ^(.*)$
Replace: !Sub ${ApiGwRestApiId}.execute-api.${AWS::Region}.amazonaws.com
-
Type: url-rewrite
UrlRewriteConfig:
Rewrites:
-
Regex: ^/(.*)$
Replace: !Sub /${ApiGatewayStageName}/$1
Actions:
-
Type: forward
ForwardConfig:
TargetGroups:
-
TargetGroupArn: !Ref TgVpceExecuteApi
Weight: 100
TargetGroupStickinessConfig:
Enabled: false
AlbListenerHttpRuleForwardToVpceExecuteApiPing:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Condition: EnableHttp
Properties:
ListenerArn: !Ref AlbListenerHttp
Priority: 1
Conditions:
-
Field: path-pattern
Values:
- /ping
Transforms:
-
Type: host-header-rewrite
HostHeaderRewriteConfig:
Rewrites:
-
Regex: ^(.*)$
Replace: !Sub ${ApiGwRestApiId}.execute-api.${AWS::Region}.amazonaws.com
Actions:
-
Type: forward
ForwardConfig:
TargetGroups:
-
TargetGroupArn: !Ref TgVpceExecuteApi
Weight: 100
TargetGroupStickinessConfig:
Enabled: false
TgNlbToAlbHttps:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: EnableHttps
DependsOn:
- AlbListenerHttps
Properties:
TargetType: alb
Protocol: TCP
Port: 443
VpcId: !Ref VpcId
HealthCheckEnabled: true
HealthCheckProtocol: HTTPS
HealthCheckPath: /ping
HealthCheckPort: 443
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
HealthCheckTimeoutSeconds: 29
HealthCheckIntervalSeconds: 30
Matcher:
HttpCode: '200'
Targets:
-
Id: !Ref Alb
Port: 443
TgNlbToAlbHttp:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Condition: EnableHttp
DependsOn:
- AlbListenerHttp
Properties:
TargetType: alb
Protocol: TCP
Port: 80
VpcId: !Ref VpcId
HealthCheckEnabled: true
HealthCheckProtocol: HTTP
HealthCheckPath: /ping
HealthCheckPort: 80
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
HealthCheckTimeoutSeconds: 29
HealthCheckIntervalSeconds: 30
Matcher:
HttpCode: '200'
Targets:
-
Id: !Ref Alb
Port: 80
Nlb:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Type: network
Name: !If [ IsNlbNameSpecified, !Ref NlbName, !Ref AWS::NoValue ]
Scheme: internet-facing
IpAddressType: ipv4
SubnetMappings:
-
SubnetId: !Ref ElbSubnetId1
AllocationId: !If [ EnableEipAllocIdNlb1, !Ref EipAllocIdNlb1, !Ref AWS::NoValue ]
-
SubnetId: !Ref ElbSubnetId2
AllocationId: !If [ EnableEipAllocIdNlb2, !Ref EipAllocIdNlb2, !Ref AWS::NoValue ]
SecurityGroups:
- !Ref SgNlb
'Fn::ForEach::NlbListnerHttpsNlbPortsHttps':
- ForEachNlbPortHttps
- !Ref NlbPortsHttps
- 'NlbListnerHttps${ForEachNlbPortHttps}':
Type: AWS::ElasticLoadBalancingV2::Listener
Condition: EnableHttps
Properties:
LoadBalancerArn: !Ref Nlb
Protocol: TCP
Port: !Ref ForEachNlbPortHttps
DefaultActions:
-
Type: forward
TargetGroupArn: !Ref TgNlbToAlbHttps
'Fn::ForEach::NlbListnerHttpNlbPortsHttp':
- ForEachNlbPortHttp
- !Ref NlbPortsHttp
- 'NlbListnerHttp${ForEachNlbPortHttp}':
Type: AWS::ElasticLoadBalancingV2::Listener
Condition: EnableHttp
Properties:
LoadBalancerArn: !Ref Nlb
Protocol: TCP
Port: !Ref ForEachNlbPortHttp
DefaultActions:
-
Type: forward
TargetGroupArn: !Ref TgNlbToAlbHttp
RecordSetGroup:
Type: AWS::Route53::RecordSetGroup
Condition: DeployR53Records
Properties:
HostedZoneId: !Ref HostedZoneId
RecordSets:
-
Name: !Ref DomainName
Type: A
AliasTarget:
HostedZoneId: !GetAtt Alb.CanonicalHostedZoneID
DNSName: !GetAtt Alb.DNSName
EvaluateTargetHealth: false
Weight: 0
SetIdentifier: alb
-
Name: !Ref DomainName
Type: A
AliasTarget:
HostedZoneId: !GetAtt Nlb.CanonicalHostedZoneID
DNSName: !GetAtt Nlb.DNSName
EvaluateTargetHealth: false
Weight: 100
SetIdentifier: nlb
Outputs:
AlbDnsName:
Value: !GetAtt Alb.DNSName
NlbDnsName:
Value: !GetAtt Nlb.DNSName
必要となる ALB と NLB + 付随リソースを展開するだけのシンプルなテンプレートです。
テンプレート管理外である関連リソースを含めたアーキテクチャ図は下記のようになります。(リージョン, Multi-AZ は略)
API Gateway 向けに調整が必要であった箇所をかいつまんで説明します。
- TgVpceExecuteApi のヘルスチェック
- プライベート API の仕様で HTTP/1.1 となるため、
ProtocolVersion: HTTP1で十分です - ターゲット指定が IP なので VPC エンドポイント証明書の検証は失敗しますが、ALB ヘルスチェック時は証明書を検証しません。今後 API Gateway の Lambda 統合でリソースレベルのヘルスチェックを行うためにも
HealthCheckProtocol: HTTPSにしています - API Gateway サービスレベルでのヘルスチェックで十分ならば、
/pingエンドポイントを使用できます。本テンプレートでもそちらを参照しています
- プライベート API の仕様で HTTP/1.1 となるため、
Private API の HTTP 1.1 に関する記載があるドキュメント
ターゲットグループの証明書検証はスキップされる旨について
/ping について
- AlbListenerHttpsRuleForwardToVpceExecuteApi: の Transforms:
- リクエストを execute-api で要求されるホスト名に書き換える必要があるため、
HostHeaderRewriteConfig:で転送したいホストパターンを${ApiGwRestApiId}.execute-api.${AWS::Region}.amazonaws.comに置き換えています。検証なので、マッチング条件は Any です - HTTP API の
$defaultステージや、カスタムドメイン名での API Gateway リクエストのように、リクエスタへはステージ名を透過的にしたいため、UrlRewriteConfig:の正規表現で全体をキャプチャさせたあと、/${ApiGatewayStageName}/$1でステージ名を付与しています
- リクエストを execute-api で要求されるホスト名に書き換える必要があるため、
プライベートタイプ REST API 本来の用途は API Gateway の AWS 閉域網化であることを十分承知した上て、他の手段ではどうしようもないときに使いましょう、ご利用は計画的に......
