AWSで遊ばない時間はNATゲートウェイを削除したい

# 背景
[AWSではじめるインフラ構築入門](https://www.seshop.com/product/detail/23455)を読んでAWSの再入門に励んでいるのですが、
NATゲートウェイのコストは馬鹿になりません。1円2円を求めてポイ活をしている私にとっては耐え難いレベルです。

使っている時間に課金されてしまうのは仕方ないとして、使っていない時間はNATゲートウェイを削除しておきたいものです。
ところがNATゲートウェイはルートテーブルなどの周辺機能にも影響を及ぼしており、手動で削除/起動するのは結構手間です。

そこで**AWS Lambda**を使ってNATゲートウェイの削除/起動を自動化してみます。

# 要件
要件は以下の通りです。
- NATゲートウェイの削除/起動や周辺機能の設定変更を自動化したい。
- 特定の時刻や曜日に自動起動するのではなく、オンデマンドで実行したい。手動でボタンをポチッとするイメージ。

# 前提
Lambda関数のランタイムは「Python 3.9」を選んでいますが、バージョンは大きな問題ではないでしょう。
CloudFormationなどを使うと上手に配布できるみたいですが、よくわからないのでコードをベタ書きしちゃいます。

VPCにパブリック/プライベートのサブネットがそれぞれ2つあり、両方のパブリックサブネットにNATゲートウェイがある構成を対象としています。要は[AWSではじめるインフラ構築入門](https://www.seshop.com/product/detail/23455)の構成を前提としています。

関数を使用するにはまずはサブネットのIDやルートテーブルのIDなどが必要になりますので、
まずは書籍にしたがってNATゲートウェイの作成とルートテーブルの設定まで手動で完了させてパラメータを確定させてください。

:::note warn
エラーハンドリングなどはまともにしておりませんので、設定ミスなどで関数が途中で止まってしまった場合、
リソースの状態が中途半端になってしまう可能性があります。そのときはコンソールを手動でポチポチするなりしてください。
:::

# 準備
- 以下のポリシーを付与したIAMロールを作成します。
    - CloudWatchLogsFullAccess
    - AmazonVPCFullAccess
    - AWSLambdaBasicExecutionRole

# 削除用関数
- Lambdaの画面の[**関数を作成**]→[**一から作成**]から作成します。
- 「準備」で作成したIAMロールを実行ロールとして設定してください。
- 関数名は`disable-nat-gateway`にしていますがお好きに名前を付けてください。
- NATゲートウェイの削除にはそれなりに時間がかかるのでタイムアウト時間は6分にしました。

## コード
``` py:disable-nat-gateway
import os
import boto3
import logging
from time import sleep

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

client = boto3.client('ec2')

def delete_elastic_ip(elastic_ip_id):
    logger.info('Deleting Elastic IP...')
    response = client.release_address(AllocationId=elastic_ip_id)
    logger.info(response)
    
def delete_nat_gateway(subnet_id):
    logger.info('Deleting NAT Gateway...')
    response = client.describe_nat_gateways(
        Filters = [
            {
                'Name': 'subnet-id',
                'Values': [subnet_id]
            },
            {
                'Name': 'state',
                'Values': ['available']
            }
        ]
    )
    logger.info(response)
    nat_gateway_id = response['NatGateways'][0]['NatGatewayId']
    elastic_ip_id = response['NatGateways'][0]['NatGatewayAddresses'][0]['AllocationId'];
    client.delete_nat_gateway(NatGatewayId=nat_gateway_id)
    sleep(120) # 必要に応じて変更せよ
    
    return(elastic_ip_id)
    
def delete_route_to_nat_gateway(route_table_id):
    logger.info('Deleting route to NAT Gateway...')
    
    response = client.delete_route(
        DestinationCidrBlock = '0.0.0.0/0',
        RouteTableId = route_table_id
    )
    logger.info(response)

def lambda_handler(event, context):
    logger.info('Started to Stopping NAT Gateway...')
    
    subnet_id = os.environ['NatGatewayTargetSubnetId1']
    route_table_id = os.environ['PrivateSubnetRouteTableId1']
    delete_route_to_nat_gateway(route_table_id)
    eip_id = delete_nat_gateway(subnet_id)
    delete_elastic_ip(eip_id)
    
    subnet_id = os.environ['NatGatewayTargetSubnetId2']
    route_table_id = os.environ['PrivateSubnetRouteTableId2']
    delete_route_to_nat_gateway(route_table_id)
    eip_id = delete_nat_gateway(subnet_id)
    delete_elastic_ip(eip_id)
    
    logger.info('Finished to Stopping NAT Gateway...')
```

:::note info
`delete_nat_gateway()`の中に謎の`sleep`があります。
これはNATゲートウェイが削除されないとElastic IPの削除に失敗することがあるため待ち時間を入れています。

[ドキュメントによると](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Waiter.NatGatewayDeleted)下のような削除されるまで上手に待ってくれる`waiter`があるはずなのですが、
実行時にエラーになってしまったのでエイヤッで待ちを入れるようにしてしまいました。いずれ使えるようになるのかな？
``` py: これがしたかった
client.get_waiter('nat_gateway_deleted').wait(NatGatewayIds=[nat_gateway_id])
```
:::

## 環境変数
[設定]→[環境変数]より以下の環境変数を設定します。
NATゲートウェイが2つあるので変数もそれぞれ1つずつです。ダサいです。
| キー | 値の例 | 説明 |
|:-:|:-:|:-:|
| NatGatewayTargetSubnetId1  | subnet-***  | 削除したい1つめのNATゲートウェイがあるサブネットID  |
| NatGatewayTargetSubnetId2  | subnet-***  | 削除したい2つめのNATゲートウェイがあるサブネットID  |
| PrivateSubnetRouteTableId1  | rtb-***  | 1つめのNATゲートウェイへのデフォルトルートを削除したいルートテーブルID  |
| PrivateSubnetRouteTableId2  | rtb-***  | 2つめのNATゲートウェイへのデフォルトルートを削除したいルートテーブルID  |

## 実行
NATゲートウェイを葬り去りたくなったら、このLambda関数の「Test」ボタンを押下します。
テスト用データは空っぽ`{}`でかまいません。必要なデータは環境変数から取得します。

# 起動用関数
- Lambdaの画面の[**関数を作成**]→[**一から作成**]から作成します。
- 「準備」で作成したIAMロールを実行ロールとして設定してください。
- 関数名は`enable-nat-gateway`にしていますがお好きに名前を付けてください。
- タイムアウト時間は削除時と同様に6分にしました。

## コード
``` py:enable-nat-gateway
import os
import boto3
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

client = boto3.client('ec2')

def allocate_elastic_ip():
    logger.info('Allocating Elastic IP...')
    response = client.allocate_address(Domain='vpc')
    logger.info(response)
    
    return response['AllocationId']
    
def create_nat_gateway(eip_id, subnet_id, nat_gateway_name):
    logger.info('Creating NAT Gateway...')
    response = client.create_nat_gateway(
        AllocationId = eip_id,
        SubnetId = subnet_id,
        TagSpecifications=[
            {
                "ResourceType": "natgateway",
                "Tags": [
                    {"Key": "Name", "Value": nat_gateway_name},
                ]
            }
        ]
    )
    logger.info(response)
    
    nat_gateway_id = response['NatGateway']['NatGatewayId']
    client.get_waiter('nat_gateway_available').wait(NatGatewayIds=[nat_gateway_id])
    
    return(nat_gateway_id)
    
def create_route_to_nat_gateway(nat_gateway_id, route_table_id):
    logger.info('Creating route to NAT Gateway...')
    response = client.create_route(
        DestinationCidrBlock = '0.0.0.0/0',
        NatGatewayId = nat_gateway_id,
        RouteTableId = route_table_id
    )
    logger.info(response)

def lambda_handler(event, context):
    logger.info('Started to Setting up NAT Gateway...')
    
    subnet_id = os.environ['NatGatewayTargetSubnetId1']
    route_id = os.environ['PrivateSubnetRouteTableId1']
    nat_name = os.environ['NatGatewayName1']
    eip_id = allocate_elastic_ip()
    nat_id = create_nat_gateway(eip_id, subnet_id, nat_name)
    create_route_to_nat_gateway(nat_id, route_id)
    
    subnet_id = os.environ['NatGatewayTargetSubnetId2']
    route_id = os.environ['PrivateSubnetRouteTableId2']
    nat_name = os.environ['NatGatewayName2']
    eip_id = allocate_elastic_ip()
    nat_id = create_nat_gateway(eip_id, subnet_id, nat_name)
    create_route_to_nat_gateway(nat_id, route_id)
    
    logger.info('Finished to Setting up NAT Gateway...')
```

## 環境変数
[設定]→[環境変数]より以下の環境変数を設定します。
| キー | 値の例 | 説明 |
|:-:|:-:|:-:|
| NatGatewayName1  | sample-ngw-01  | 1つめのNATゲートウェイの名前  |
| NatGatewayName2  | sample-ngw-02  | 2つめのNATゲートウェイの名前  |
| NatGatewayTargetSubnetId1  | subnet-***  | 1つめのNATゲートウェイを配置したいサブネットのID  |
| NatGatewayTargetSubnetId2  | subnet-***  | 2つめのNATゲートウェイを配置したいサブネットのID  |
| PrivateSubnetRouteTableId1  | rtb-***  | 1つめのNATゲートウェイへのデフォルトルートを追加したいルートテーブルID  |
| PrivateSubnetRouteTableId2  | rtb-***  | 2つめのNATゲートウェイへのデフォルトルートを追加したいルートテーブルID  |

## 実行
NATゲートウェイを起動したくなったら、このLambda関数の「Test」ボタンを押下します。
削除時と同様にテスト用データは空っぽ`{}`でかまいません。

# 参考文献
- [【AWS】Lambda用のNAT Gatewayの自動起動・終了](https://qiita.com/s_Pure/items/ab66bebfc2f744b81c61)