【AWS】 NAT GatewayをCloudFormationでスケーリングする

[NTTテクノクロス Advent Calendar 2024 ](https://qiita.com/advent-calendar/2024/ntt-tx) シリーズ2 "7日目"の記事です。

こんにちは。NTTテクノクロスの増田です。
現在2年目のエンジニアで、AWSを用いた業務を行っています。

## はじめに
本記事では、AWSのNAT Gatewayにおけるコスト削減のための作業を行ったので、その実施内容や方法などについてご紹介します。

## NAT Gatewayとは
NAT Gatewayとは、VPC内に構成したプライベートサブネットからインターネットに接続するためのもので、IPアドレスとポート変換（NAPT）を行ってくれます。

## NAT Gatewayのコスト
NAT Gatewayには時間単位料金とデータ処理料金があります。

時間単位料金：1時間あたり0.062USD
データ処理料金：1GBデータあたり0.062USD

NAT Gatewayを配置しているだけで時間単位料金が発生するため、不要なNAT Gatewayを削減する構成の見直しを行い、料金を抑えることにしました。

## 構成
NAT Gatewayの構成見直し前後の構成を以下に示します。
### 対処前
![Untitled-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626189/031063b0-a8c4-47d6-c29b-cf342dda1610.png)

対処前は可用性を考慮して異なるAZにNAT Gatewayを3つ配置していましたが、開発環境や検証環境は状況によってはマルチAZ構成が不要であることもあり、コスト節約のため不要時は削除できるように改善しました。

### 対処後
![Untitled3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626189/f0639a17-9749-95ff-a42d-493de7d26751.png)

対処後は、CloudFormationを使用して上図のように構成を変更できるようにします。なお、CloudFormationはAWSリソースをコードとして定義し、構築するためのIaC(Infrastructure as Code)ツールです。

なお、ここでは対処後の構成を以下のように定義します。

<table border="1">
  <tr>
    <th>構成</th><th>定義内容</th>
  </tr>
  <tr>
    <td>Single構成</td><td>全てのAZにおけるルーティングを1つのNAT Gatewayに集約 <br> (NAT Gateway：計1台)</td>
  </tr>
  <tr>
    <td>Multipl構成</td><td>全てのAZにおいて、NAT Gatewayを配置 <br> (NAT Gateway：計3台)</td>
  </tr>
</table>

 これにより、通常時はSingle構成の運用で、緊急時にはMultiple構成への切り替えを行うことが可能です。

## CloudFormationテンプレート
構成を変更するCloudFormationテンプレートを作成します。

※テンプレート内の "VPC", "Public_Subnet00X", "Private_Subnet00X" の設定値はRefやImportValueなどで補完してください。

1\. 構成選択用の環境変数を定義します
　デフォルトではSingle構成にしています
```yaml
Parameters:
# NAT Gatewayの構成選択用の環境変数を定義
   NATGatewayType:
     Description: "Type of NAT Gateway configuration (single or multiple)"
     Type: String
     AllowedValues:
       - single
       - multiple
	 Default: single
```

2\. Multiple構成の条件を定義します

```yaml
Conditions:
  UseMultipleNatGateways: !Equals [!Ref "NATGatewayType", "multiple"]
```

3\. NAT GatewayとElastic IPを作成します
　UseMultipleNatGatewaysの条件によって、NAT Gatewayの作成数が変わります

```yaml      
Resources:
  # NATGateway1a Create
  NATGateway1a:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGateway1aEIP.AllocationId
      SubnetId: !Ref SubnetPublic1a
      Tags:
        - Key: Name
          Value: !Sub NATGateway1a_Name
  # NATGateway1a For EIP Create
  NATGateway1aEIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
  # NATGateway1c Create
  NATGateway1c:
    Type: AWS::EC2::NatGateway
    # UseMultipleNatGatewaysが true の場合のみ作成
    Condition: "UseMultipleNatGateways"
    Properties:
      AllocationId: !GetAtt NATGateway1cEIP.AllocationId
      SubnetId: !Ref SubnetPublic1c
      Tags:
        - Key: Name
          Value: !Sub NATGateway1c_Name
  # NATGateway1c For EIP Create
  NATGateway1cEIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
  # NATGateway1d Create
  NATGateway1d:
    Type: AWS::EC2::NatGateway
    # UseMultipleNatGatewaysが true の場合のみ作成
    Condition: "UseMultipleNatGateways"
    Properties:
      AllocationId: !GetAtt NATGateway1dEIP.AllocationId
      SubnetId: !Ref SubnetPublic1d
      Tags:
        - Key: Name
          Value: !Sub NATGateway1d_Name
  # NATGateway1d For EIP Create
  NATGateway1dEIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
```

4\. ルートテーブルを作成します

```yaml
  # PrivateRouteTable1a Create
  PrivateRouteTable1a:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub PrivateRouteTable1a_Name
  # PrivateRouteTable1c Create
  PrivateRouteTable1c:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub PrivateRouteTable1c_Name
  # PrivateRouteTable1d Create
  PrivateRouteTable1d:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub PrivateRouteTable1d_Name
```

5\. NAT Gatewayをルートテーブルへ関連付けます
　UseMultipleNatGatewaysの条件によって、関連付ける対象が変わります

```yaml
  # Route to NATGateway1a for PrivateRouteTable1a
  Private1aRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1a
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway1a
  # Route to NATGateway1c for PrivateRouteTable1c
  Private1cRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1c
      DestinationCidrBlock: 0.0.0.0/0
      ### UseMultipleNatGatewaysが true の場合 NATGateway1c に、false の場合 NATGateway1a に接続
      NatGatewayId: !If 
        - UseMultipleNatGateways
        - !Ref NATGateway1c
        - !Ref NATGateway1a
  # Route to NATGateway1d for PrivateRouteTable1d
  Private1dRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1d
      DestinationCidrBlock: 0.0.0.0/0
      ### UseMultipleNatGatewaysが true の場合 NATGateway1d に、false の場合 NATGateway1a に接続
      NatGatewayId: !If 
        - UseMultipleNatGateways
        - !Ref NATGateway1d
        - !Ref NATGateway1a
```

6\. プライベートサブネットをルートテーブルへ関連付けます

```yaml
  # Associate SubnetPrivate1a with PrivateRouteTable1a
  Private1aSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate1a
      RouteTableId: !Ref PrivateRouteTable1a
  # Associate SubnetPrivate1c with PrivateRouteTable1c
  Private1cSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate1c
      RouteTableId: !Ref PrivateRouteTable1c
  # Associate SubnetPrivate1d with PrivateRouteTable1d
  Private1dSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate1d
      RouteTableId: !Ref PrivateRouteTable1d
```

7\. 構築したNAT Gatewayの構成を出力します

```yaml
Outputs:
  NatGatewayType:
    Description: "The type of NAT Gateway configuration used"
    Value: !Ref "NATGatewayType"
```

## リソースマップ

作成したNat Gatewayを含むVPCのリソースマップを以下に示します。
リソースマップだとSingle構成のルーティングも分かりやすいですね。

### Single構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626189/1a6c4cc5-d2ea-2e11-aad0-644785bd1b39.png)

### Multiple構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626189/0d1d63ab-32d3-b270-69df-e126b4e4788c.png)

- - -

最後にNo.1~7までを繋げて、VPCやサブネットなどを追加したテンプレート全体を以下に示します。

```yaml:sample.yaml
AWSTemplateFormatVersion: '2010-09-09'

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  VPCCIDR:
    Description: VPC Cider Block for VPC
    Type: String
  SubnetPublic1aCIDR:
    Description: Public Subnet 1a Cider Block for SubnetPublic1a
    Type: String
  SubnetPublic1cCIDR:
    Description: Public Subnet 1c Cider Block for SubnetPublic1c
    Type: String
  SubnetPublic1dCIDR:
    Description: Public Subnet 1d Cider Block for SubnetPublic1d
    Type: String
  SubnetPrivate1aCIDR:
    Description: Private Subnet 1a Cider Block for SubnetPrivate1a
    Type: String
  SubnetPrivate1cCIDR:
    Description: Private Subnet 1c Cider Block for SubnetPrivate1c
    Type: String
  SubnetPrivate1dCIDR:
    Description: Private Subnet 1d Cider Block for SubnetPrivate1d
    Type: String
  # NAT-GWの構成選択用の環境変数を定義
  NATGatewayType:
    Description: "Type of NAT Gateway configuration (single or multiple)"
    Type: String
    AllowedValues:
      - single
      - multiple
    Default: single
    
# ------------------------------------------------------------#
# Conditions
# ------------------------------------------------------------# 
Conditions:
  UseMultipleNatGateways: !Equals [!Ref "NATGatewayType", "multiple"]

Resources:
# ------------------------------------------------------------#
# VPC, Subnet, IGW
# ------------------------------------------------------------# 
  # VPC Create
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub VPC_Name
  # SubnetPublic1a Create
  SubnetPublic1a:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref SubnetPublic1aCIDR
      Tags:
        - Key: Name
          Value: !Sub Public_Subnet1a_Name
  # SubnetPublic1c Create
  SubnetPublic1c:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref SubnetPublic1cCIDR
      Tags:
        - Key: Name
          Value: !Sub Public_Subnet1c_Name
  # SubnetPublic1d Create
  SubnetPublic1d:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ap-northeast-1d
      CidrBlock: !Ref SubnetPublic1dCIDR
      Tags:
        - Key: Name
          Value: !Sub Public_Subnet1d_Name
  # SubnetPrivate1a Create
  SubnetPrivate1a:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref SubnetPrivate1aCIDR
      Tags:
        - Key: Name
          Value: !Sub Private_Subnet1a_Name
  # SubnetPrivate1c Create
  SubnetPrivate1c:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref SubnetPrivate1cCIDR
      Tags:
        - Key: Name
          Value: !Sub Private_Subnet1c_Name
  # SubnetPrivate1d Create
  SubnetPrivate1d:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: ap-northeast-1d
      CidrBlock: !Ref SubnetPrivate1dCIDR
      Tags:
        - Key: Name
          Value: !Sub Private_Subnet1d_Name
  # InternetGateway Create
  IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub IGW
  # InternetGateway Attachment to VPC
  IGWAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref IGW
      VpcId: !Ref VPC

# ------------------------------------------------------------#
#  NAT Gateway, EIP
# ------------------------------------------------------------#
  # NATGateway1a Create
  NATGateway1a:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGateway1aEIP.AllocationId
      SubnetId: !Ref SubnetPublic1a
      Tags:
        - Key: Name
          Value: !Sub NATGateway1a_Name
  # NATGateway1a For EIP Create
  NATGateway1aEIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
  # NATGateway1c Create
  NATGateway1c:
    Type: AWS::EC2::NatGateway
    # UseMultipleNatGatewaysが true の場合のみ作成
    Condition: "UseMultipleNatGateways"
    Properties:
      AllocationId: !GetAtt NATGateway1cEIP.AllocationId
      SubnetId: !Ref SubnetPublic1c
      Tags:
        - Key: Name
          Value: !Sub NATGateway1c_Name
  # NATGateway1c For EIP Create
  NATGateway1cEIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
  # NATGateway1d Create
  NATGateway1d:
    Type: AWS::EC2::NatGateway
    # UseMultipleNatGatewaysが true の場合のみ作成
    Condition: "UseMultipleNatGateways"
    Properties:
      AllocationId: !GetAtt NATGateway1dEIP.AllocationId
      SubnetId: !Ref SubnetPublic1d
      Tags:
        - Key: Name
          Value: !Sub NATGateway1d_Name
  # NATGateway1d For EIP Create
  NATGateway1dEIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc

# ------------------------------------------------------------#
#  RouteTable
# ------------------------------------------------------------#
  # PublicRouteTable Create
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub PublicRouteTable_Name
  # PrivateRouteTable1a Create
  PrivateRouteTable1a:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub PrivateRouteTable1a_Name
  # PrivateRouteTable1c Create
  PrivateRouteTable1c:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub PrivateRouteTable1c_Name
  # PrivateRouteTable1d Create
  PrivateRouteTable1d:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub PrivateRouteTable1d_Name
  # PublicRoute Create
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref IGW
  # Route to NATGateway1a for PrivateRouteTable1a
  Private1aRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1a
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway1a
  # Route to NATGateway1c for PrivateRouteTable1c
  Private1cRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1c
      DestinationCidrBlock: 0.0.0.0/0
      ### UseMultipleNatGatewaysが true の場合 NATGateway1c に、false の場合 NATGateway1a に接続
      NatGatewayId: !If 
        - UseMultipleNatGateways
        - !Ref NATGateway1c
        - !Ref NATGateway1a
  # Route to NATGateway1d for PrivateRouteTable1d
  Private1dRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1d
      DestinationCidrBlock: 0.0.0.0/0
      ### UseMultipleNatGatewaysが true の場合 NATGateway1d に、false の場合 NATGateway1a に接続
      NatGatewayId: !If 
        - UseMultipleNatGateways
        - !Ref NATGateway1d
        - !Ref NATGateway1a
  # Associate SubnetPublic1a with PublicRouteTable
  Public1aSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPublic1a
      RouteTableId: !Ref PublicRouteTable
  # Associate SubnetPublic1c with PublicRouteTable
  Public1cSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPublic1c
      RouteTableId: !Ref PublicRouteTable
  # Associate SubnetPublic1d with PublicRouteTable
  Public1dSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPublic1d
      RouteTableId: !Ref PublicRouteTable
  # Associate SubnetPrivate1a with PrivateRouteTable1a
  Private1aSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate1a
      RouteTableId: !Ref PrivateRouteTable1a
  # Associate SubnetPrivate1c with PrivateRouteTable1c
  Private1cSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate1c
      RouteTableId: !Ref PrivateRouteTable1c
  # Associate SubnetPrivate1d with PrivateRouteTable1d
  Private1dSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate1d
      RouteTableId: !Ref PrivateRouteTable1d
      
# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#    
Outputs:
  NatGatewayType:
    Description: "The type of NAT Gateway configuration used"
    Value: !Ref "NATGatewayType"
```

## まとめ
以上、NAT Gatewayに関する内容をお届けしました。今回ご紹介した運用方法以外にも、未使用時はNAT Gatewayをすべて削除し、必要な時に再配置するなど、さまざまな運用方法が考えられますね。

AWS環境の構築においては、コスト削減は重要なポイントです。今後も対策に取り組みながら、参考になる情報を投稿していければと思います。

明日は [**@yukimuraki**](https://qiita.com/yukimuraki) さんの 「**綺麗なIAMポリシーを書きたくて**」です。明日以降も [NTTテクノクロスアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/ntt-tx) をお楽しみに！