0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

令和7年最新版 固定 IP アドレスでの Amazon API Gateway

Posted at

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 は略)

static-ip-apigw.png

API Gateway 向けに調整が必要であった箇所をかいつまんで説明します。

  • TgVpceExecuteApi のヘルスチェック
    • プライベート API の仕様で HTTP/1.1 となるため、ProtocolVersion: HTTP1 で十分です
    • ターゲット指定が IP なので VPC エンドポイント証明書の検証は失敗しますが、ALB ヘルスチェック時は証明書を検証しません。今後 API Gateway の Lambda 統合でリソースレベルのヘルスチェックを行うためにも HealthCheckProtocol: HTTPS にしています
    • API Gateway サービスレベルでのヘルスチェックで十分ならば、/ping エンドポイントを使用できます。本テンプレートでもそちらを参照しています

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 でステージ名を付与しています

プライベートタイプ REST API 本来の用途は API Gateway の AWS 閉域網化であることを十分承知した上て、他の手段ではどうしようもないときに使いましょう、ご利用は計画的に......

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?