Step Functionsを利用してNAT Gatewayを自動作成/削除する


## 概要

本記事ではStep Functionsを利用して、Nat Gatewayを自動で作成/削除する方法について記載します。
NAT Gatewayは作成しているだけでコストがかかるリソースであり、開発環境の利用していない時間帯などは停止(削除)することでコスト削減につながります。

同じことを以下の記事でCloudFormationを利用して説明していますが、今回はStep Functionsを利用して実施したいと思います。
https://fu3ak1.hatenablog.com/entry/2020/07/28/135924

以下のことをStep Functionsを利用して実現します。

* 作成時
    * 既に存在するElastic IPとSubnetを指定してNAT Gatewayを作成する。
    * ルートテーブルを更新し、デフォルトゲートウェイ(0.0.0.0/0)のルーティング先をNAT Gatewayにする
* 削除時
    * NAT Gatewayを削除する
    * ルートテーブルからデフォルトゲートウェイ(0.0.0.0/0)のルーティングを削除する


## Nat Gatewayを作成する

Step FunctionsでNat Gatewayを作成するために必要な情報をJSONの入力で受け取ります。

Step Functionsの入力は以下になります。

```json
{
  "subnetId": "subnet-xxxx",
  "allocationId": "eipalloc-xxxx",
  "routeTableId": "rtb-xxxx"
}
```

* subnetId: NatGWを作成したいSubnetを指定する
* allocationId: ElasticIPの割り当てIP(allocationId)を指定する。NatGWで利用するElastic IPは固定IPとして利用することを想定して、既に存在するIPを指定します。
* routeTableId: NatGWをデフォルトのルートとするルートテーブルを指定します。

Step Functionsの定義は以下になります。

![スクリーンショット 2023-12-29 23.08.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526312/9230c1f1-d266-72a5-28e4-a3f2634a160d.png)

* CreateNatGateway SubnetIdとAllocationIdを指定してNatGWを作成します。
* Wait NatGWが利用可能になるまで、ルートテーブルのルートに指定できないため、３０秒待機します。
* DescribeNatGateways 30秒待機した後、NatGWの情報を取得します。
* CheckStatus NatGWがavailableならば、ルートテーブルにルートを追加するステップに遷移します。まだ、利用可能でない場合はWaitに遷移して、30秒待機します。
* CreateRoute ルートテーブルにルートを追加します。

<details><summary>ワークフロー定義</summary><div>

```json

{
  "StartAt": "CreateNatGateway",
  "States": {
    "CreateNatGateway": {
      "Type": "Task",
      "Parameters": {
        "SubnetId.$": "$.subnetId",
        "AllocationId.$": "$.allocationId"
      },
      "Resource": "arn:aws:states:::aws-sdk:ec2:createNatGateway",
      "Next": "DescribeNatGateways",
      "ResultSelector": {
        "NatGatewayId.$": "$.NatGateway.NatGatewayId"
      },
      "ResultPath": "$.NatGateway"
    },
    "Wait": {
      "Type": "Wait",
      "Seconds" : 30,
      "Next": "DescribeNatGateways"
    },
    "DescribeNatGateways": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:ec2:describeNatGateways",
      "Parameters": {
        "Filter": [
          {
            "Name": "nat-gateway-id",
            "Values.$": "States.Array($.NatGateway.NatGatewayId)"
          }
        ]
      },
      "Next": "CheckStatus",
      "ResultSelector": {
        "NatGatewayId.$": "$.NatGateways[0].NatGatewayId",
        "State.$": "$.NatGateways[0].State"
      },
      "ResultPath": "$.NatGateway"
    },
    "CheckStatus": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.NatGateway.State",
          "StringEquals": "available",
          "Next": "CreateRoute"
        }
      ],
      "Default": "Wait"
    },
    "CreateRoute": {
      "Type": "Task",
      "Next": "Succeed",
      "Parameters": {
        "DestinationCidrBlock": "0.0.0.0/0",
        "NatGatewayId.$": "$.NatGateway.NatGatewayId",
        "RouteTableId.$": "$.routeTableId"
      },
      "Resource": "arn:aws:states:::aws-sdk:ec2:createRoute"
    },
    "Succeed": {
      "Type": "Succeed"
    }
  }
}

```
</div></details>

## Nat Gatewayを削除する

Step FunctionsでNat Gatewayを削除するために必要な情報をjsonの「入力」で受け取ります。

Step Functionsの入力は以下になります。

* subnetId: 削除したいNat Gatewayが存在するSubnetを指定します。
* routeTableId: デフォルトルートにNat Gatewayのルートテーブルを指定します。

```json
{
  "subnetId": "subnet-xxxx",
  "routeTableId": "rtb-xxxx"
}
```

Step Functionsの定義は以下になります。

* DescribeNatGateways: 指定したSubnetに存在するNat Gatewayを取得します。後続でそのIdを指定します。作成削除を行う上で、Nat GatewayのIdは固定されていません。そのため、固定のリソースである、SubnetからDescribeしてIdを取得します。
* DeleteNatGateway: DescribeNatGatewaysで取得したNat Gatewayをidを指定して削除します。
* DeleteRoute: ルートテーブルに存在するデフォルトルートを削除します。

![スクリーンショット 2023-12-29 23.18.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526312/1803a7c4-9a59-6261-4eb4-627c9b934650.png)


<details><summary>ワークフロー定義</summary><div>

```json
{
  "StartAt": "DescribeNatGateways",
  "States": {
    "DescribeNatGateways": {
      "Type": "Task",
      "Next": "DeleteNatGateway",
      "Parameters": {
        "Filter": [
          {
            "Name": "subnet-id",
            "Values.$": "States.Array($.subnetId)"
          },
          {
            "Name": "state",
            "Values": ["available"]
          }
        ]
      },
      "Resource": "arn:aws:states:::aws-sdk:ec2:describeNatGateways",
      "ResultPath": "$.DescribeNatGateways",
      "Next": "DeleteNatGateway"
    },
    "DeleteNatGateway": {
      "Type": "Task",
      "Parameters": {
        "NatGatewayId.$": "$.DescribeNatGateways.NatGateways[0].NatGatewayId"
      },
      "Resource": "arn:aws:states:::aws-sdk:ec2:deleteNatGateway",
      "ResultPath": "$.DeleteNatGateway",
      "Next": "DeleteRoute"
    },
    "DeleteRoute": {
      "Type": "Task",
      "Next": "Succeed",
      "Parameters": {
        "DestinationCidrBlock": "0.0.0.0/0",
        "RouteTableId.$": "$.routeTableId"
      },
      "Resource": "arn:aws:states:::aws-sdk:ec2:deleteRoute"
    },
    "Succeed": {
      "Type": "Succeed"
    }
  }
}
```
</div></details>

## 複数のNAT Gatewayを作成する

上記の例では一つのNAT Gatewayを指定するパターンを紹介しましたが、複数指定できる方が望ましいです。
また、Step Functionsの入力はいちいちStep Functions実行時に指定するのではなく、どこかで固定しておける方が良いと思います。
そこで、今回は入力をStep FunctionsのItemReaderで取得し、Mapで複数処理することにします。
ItemReaderによってS3に保存されているJSONから複数の入力を受け取ることができます。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/input-output-itemreader.html

NAT Gatewayを複数作成する例を紹介します。削除する方法も同様の方法で実現できます。

Step Functionsの定義は以下のようになります。createNatGatewayから始まる一連の処理は同じですが、入力をS3から取得して、複数のワークフローを実行できるようにしています。

![スクリーンショット 2023-12-30 0.04.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526312/cf62a462-69a9-ac9c-465d-a721ffe37318.png)

<details><summary>ワークフロー定義</summary><div>

```json
{
  "StartAt": "MapNATGWFromS3",
  "States": {
    "MapNATGWFromS3": {
      "Type": "Map",
      "ItemReader": {
        "Resource": "arn:aws:states:::s3:getObject",
        "ReaderConfig": {
          "InputType": "JSON"
        },
        "Parameters": {
            "Bucket": "{S3バケットを指定}",
            "Key": "create-natgw.json"
        }
      },
      "MaxConcurrency": 1000,
      "Label": "MapNATGWFromS3",
      "End": true,
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "DISTRIBUTED",
          "ExecutionType": "STANDARD"
        },
        "StartAt": "createNatGateway",
        "States": {
          "createNatGateway": {
            "Type": "Task",
            "Parameters": {
              "SubnetId.$": "$.subnetId",
              "AllocationId.$": "$.allocationId"
            },
            "Resource": "arn:aws:states:::aws-sdk:ec2:createNatGateway",
            "Next": "CheckStatus",
            "ResultSelector": {
              "NatGatewayId.$": "$.NatGateway.NatGatewayId"
            },
            "ResultPath": "$.NatGateway"
          },
          "Wait": {
            "Type": "Wait",
            "Seconds" : 30,
            "Next": "CheckStatus"
          },
          "CheckStatus": {
            "Type": "Task",
            "Resource": "arn:aws:states:::aws-sdk:ec2:describeNatGateways",
            "Parameters": {
              "Filter": [
                {
                  "Name": "nat-gateway-id",
                  "Values.$": "States.Array($.NatGateway.NatGatewayId)"
                }
              ]
            },
            "Next": "IsRunning",
            "ResultSelector": {
              "NatGatewayId.$": "$.NatGateways[0].NatGatewayId",
              "State.$": "$.NatGateways[0].State"
            },
            "ResultPath": "$.NatGateway"
          },
          "IsRunning": {
            "Type": "Choice",
            "Choices": [
              {
                "Variable": "$.NatGateway.State",
                "StringEquals": "available",
                "Next": "CreateRoute"
              }
            ],
            "Default": "Wait"
          },
          "CreateRoute": {
            "Type": "Task",
            "Next": "Succeed",
            "Parameters": {
              "DestinationCidrBlock": "0.0.0.0/0",
              "NatGatewayId.$": "$.NatGateway.NatGatewayId",
              "RouteTableId.$": "$.routeTableId"
            },
            "Resource": "arn:aws:states:::aws-sdk:ec2:createRoute"
          },
          "Succeed": {
            "Type": "Succeed"
          }
        }
      }
    }
  }
}

```
</div></details>

ワークフローの最初で指定しているcreate-natgw.jsonは以下のようなJSONになります。
NAT Gatewayを作成したい[subnetId, allocationId, routeTableId]の組を指定します。
これを指定したS3に保存しておくことで、Step Functionsの入力として処理することができます。

```json
[
  {
  "subnetId": "subnet-xxxx",
  "allocationId": "eipalloc-xxxx",
  "routeTableId": "rtb-xxxx"
  },
  {
    "subnetId": "subnet-yyyy",
    "allocationId": "eipalloc-yyyy",
    "routeTableId": "rtb-yyyy"
  }
]

```


## EventBridgeを利用して定期的に実行する

上記で作成したStep Functionを定期的に実行することで、作成/削除を行うことができます。
terraformのコードで例えば9時に作成して、20時に削除する場合は以下のようになります。

```tf
resource "aws_cloudwatch_event_rule" "create" {
  name        = "create-rule"
  description = "create rule"

  is_enabled          = true
  schedule_expression = "cron(0 0 * * ? *)"
}

resource "aws_cloudwatch_event_target" "create" {
  target_id = "create-rule"
  arn       = "{Nat Gatewayを作成するstepfunctionsのarn}"
  rule      = aws_cloudwatch_event_rule.stop.name
  role_arn  = aws_iam_role.eventbridge.arn
}

resource "aws_cloudwatch_event_rule" "delete" {
  name        = "delete-rule"
  description = "delete rule"

  is_enabled          = true
  schedule_expression = "cron(0 11 * * ? *)"
}

resource "aws_cloudwatch_event_target" "delete" {
  target_id = "delete-rule"
  arn       = "{Nat Gatewayを削除するstepfunctionsのarn}"
  rule      = aws_cloudwatch_event_rule.stop.name
  role_arn  = aws_iam_role.eventbridge.arn
}
```

## まとめ

Step Functionsを利用してNAT Gatewayを定期的に作成/削除する方法を紹介しました。
NAT Gatewayは利用しない時間もコストがかかるサービスなので、少なからずコスト削減につながると思います。