NLB + API Gateway(プライベート)の構成でAPIを実行してみた

プライベートなAPI Gatewayに対して、NLB経由でアクセスするための構成を検証しました。NLBの特徴としてプライベートIPを指定できることがあるので、プライベートIPを固定して、かつ、ドメインでアクセスできる構成を試してみたいと思い、実際にCloudFormationで作ってみました。ふつうNLBを再作成するとプライベートIPやエンドポイントが変わってしまいますが、プライベートIPを指定してNLBも作成してあげることで、再作成でもアクセスするIPが変わらなくなります。また、セキュリティ上VPCエンドポイントに直接アクセスできないリソースでも、NLBを間にはさむことでアクセスできるようになるかもしれないということで、構成の幅も広がると思います。

![nlb-vpce-apigw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780549/e12ccc62-7783-ff77-a6ca-d40520dd7657.png)

CloudFormationのテンプレートはGitHubにアップロードしてあります。

https://github.com/kiyo27/CFnSnippet/tree/main/nlb-vpce-nlb

## 前提

- プライベートなAPI GatewayとACMは作成済み

## NLB用のターゲットグループを作成

ターゲットに指定するIPはAPI Gateway用のVPCエンドポイントのネットワークインターフェースから確認します。

![スクリーンショット 2025-01-01 160257.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780549/a3933b69-a0c7-adb4-9cbf-cb65ae3622e8.png)

テンプレートは下記になります。

```yml:nlb-vpce-apigw.yml
  TargetGroupForVpcendpointForNlb:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${SysName}-${EnvName}-tg-for-nlb
      Port: 443
      Protocol: TLS
      TargetType: ip
      VpcId: !Ref VpcId
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: traffic-port
      HealthCheckProtocol: TCP
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
      Targets:
        - Id: !Select [ "0", !Ref VpcEndpointIps ]
          Port: 443
        - Id: !Select [ "1", !Ref VpcEndpointIps ]
          Port: 443
      Tags:
        - Key: Name
          Value: !Sub ${SysName}-${EnvName}-nlb-tg
````

## NLBを作成

ターゲットグループを作成したら、そのターゲットグループを指定したNLBとリスナーを作成します。NLBを作成する際にプライベートIPを指定して作成するようにします。

テンプレートは下記になります。

```yaml:nlb-vpce-apigw.yml
  NetworkLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${SysName}-${EnvName}-nlb
      Scheme: internal
      Type: network
      SecurityGroups: !Ref SecurityGroupsForNlb
      SubnetMappings:
        - PrivateIPv4Address: !Select [ "0", !Ref NlbStaticIps ]
          SubnetId: !Select [ "0", !Ref NlbSubnetIds ]
        - PrivateIPv4Address: !Select [ "1", !Ref NlbStaticIps ]
          SubnetId: !Select [ "1", !Ref NlbSubnetIds ]
      Tags:
        - Key: Name
          Value: !Sub ${SysName}-${EnvName}-nlb

  NlbTlsListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref NetworkLoadBalancer
      Port: 443
      Protocol: TLS
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroupForVpcendpointForNlb
      Certificates:
        - CertificateArn: !Ref CertificateArn
```

ここまで設定出来れば、NLBに割り当てたIPでアクセスできるようになります。ただ、証明書の検証でエラーがでます。

```bash
$ curl https://172.31.25.205/api/pets/1
curl: (60) SSL: no alternative certificate subject name matches target host name '172.31.25.205'
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
```

エラーが出ないようにするには証明書の検証を無視してアクセスすることになります。

```bash
$ curl --insecure -H "x-apigw-api-id:<api-id>" https://172.31.25.205/api/pets/1
{
  "id": 1,
  "type": "dog",
  "price": 249.99
}
```

## カスタムドメインの設定

API Gatewayとドメインを対応させます。プライベートのAPI Gatewayの場合はリソースポリシーの設定も必要になります。

```yml:apigw-custom-domainname.yml
Resources:
  CustomDomainName:
    Type: AWS::ApiGateway::DomainNameV2
    Properties:
      CertificateArn: !Ref CertificateArn
      DomainName: !Ref DomainName
      EndpointConfiguration: 
        Types:
          - PRIVATE
      Policy:
        Fn::ToJsonString:
          Version: 2012-10-17
          Statement:
            - Effect: Deny
              Principal: "*"
              Action: execute-api:Invoke
              Resource:
                - "execute-api:/*"
              Condition:
                StringNotEquals:
                  aws:SourceVpce: !Ref VpcEndpointId
            - Effect: Allow
              Principal: "*"
              Action: execute-api:Invoke
              Resource:
                - "execute-api:/*"

  DomainNameAssociation:
      Type: AWS::ApiGateway::DomainNameAccessAssociation
      Properties:
        AccessAssociationSource: !Ref VpcEndpointId
        AccessAssociationSourceType: VPCE
        DomainNameArn: !Ref CustomDomainName

  ApiPathMapping:
    Type: AWS::ApiGateway::BasePathMappingV2
    Properties:
      DomainNameArn: !Ref CustomDomainName
      RestApiId: !Ref RestApiId
      Stage: api
```

## プライベートホステッドゾーンにレコード登録

プライベートホステッドゾーンにAレコードを登録します。Aレコードの値にはNLBに割り当てたプライベートIPを指定します。

```yml:route53-private-hosted-zone.yml
  PrivateHostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: !Ref DomainName
      VPCs: 
        - VPCId: !Ref VpcId
          VPCRegion: !Sub ${AWS::Region}

  DomainRecordSet:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref PrivateHostedZone
      Name: !Ref DomainName
      ResourceRecords: !Ref IpAddresses
      Type: A
      TTL: "300"
```

これでリソースの作成は終わりです。あとは動作確認になります。

## 動作確認

名前解決できているか確認します。

```
$ dig api.hogehoge.com

; <<>> DiG 9.18.28 <<>> api.hogehoge.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57538
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;api.androidpug.com.            IN      A

;; ANSWER SECTION:
api.hogehoge.com.     253     IN      A       172.31.42.17
api.hogehoge.com.     253     IN      A       172.31.25.205

;; Query time: 0 msec
;; SERVER: 172.31.0.2#53(172.31.0.2) (UDP)
;; WHEN: Wed Jan 01 12:52:15 UTC 2025
;; MSG SIZE  rcvd: 79
```

NLBに割り当てたプライベートIPが表示されていますね。次に、ドメインでアクセスできることを確認します。

```bash
$ curl https://api.hogehoge.com/pets/1
{
  "id": 1,
  "type": "dog",
  "price": 249.99
}
```

アクセス出来ているので無事に設定完了しました。

## 参考資料

リソース作成で参考にした情報は↓になります。

https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-private-custom-domains-tutorial.html

https://repost.aws/knowledge-center/invoke-private-api-gateway

https://repost.aws/ja/knowledge-center/api-gateway-troubleshoot-403-forbidden