【AWSコスト削減の道 for 開発環境】 NATゲートウェイ編

# 【AWSコスト削減の道 for 開発環境】NATゲートウェイ編

## 結論
* NATゲートウェイを利用していない時間に対して、自動作成及び自動削除を行うことで、料金の**約62%**を低下

## 前提
* 以前ページにて紹介しているTerraoformを用いて環境を構築している
    * <a href="https://qiita.com/H_broth/items/3cbcfb7fa55b242acf36" target="_blank">Terraformを用いた開発環境について</a>
* 本記事内で記載している料金については、2021/12/21時点の料金を表示している
* 料金は「東京リージョン」の料金を表示している

# 目次
[前提](#前提)
[背景](#背景)
[本記事内の利用技術について](#本記事内の利用技術について)
[利用料金について](#利用料金について)
[料金停止効果について](#料金停止効果について)
[Lambda内の処理について](#lambda内の処理について)
[NATゲートウェイのライフサイクル](#natゲートウェイのライフサイクル)
[参考URL](#参考url)

## 概要
* 開発環境に使用しているNATを業務時間以外は削除する
* NATゲートウェイの作成及び削除は自動的に行う
    * NATゲートウェイはEC2のように**停止**という概念がないため毎回、作成、削除を行う必要がある

## 背景
* 業務時間外にも課金状態となっているAWSリソースに対してコストを削減しようとしたため

## 本記事内の利用技術について
* NATゲートウェイ
    * プライベートIPアドレスをパブリックIPアドレスに変更するサービス

* Lambda
    * 特定のイベントに応じて実行される関数 ※ サーバなどを考えずに処理内容だけ考えればいいので便利!

* EventBrige
    * AWSリソースからのイベント通知を受け取り、特定の処理をさせることができるサービス
    * 本記事では、特定の時間にLambdaを実行させるようにする設定をしている

## 利用料金について
### 前提
* データ処理料金については加味していない ※ 利用していない時間の為、データ処理は「0」と判断したため

### 詳細
* NAT ゲートウェイあたりの料金 (USD/時)
    * **0.062USD**

## 停止時間について
* 停止曜日は以下としている
    * **月 ~ 金** ※ 祝日は考慮していない
* 各曜日の停止時間は以下としている
    * **PM10:00 ~ AM8:00**

## 料金停止効果について

### 結論
#### **約62%の節約!！**
* **0.61...** = 27.28USD(停止後の料金) / 44.64USD(停止前の料金)

### 停止前
* 720時間(1か月の料金) * 0.062USD = **44.64USD**

### 停止後
* 440時間(1か月の料金) * 0.062USD = **27.28USD**
    * 1日の起動時間: **14時間** = 24時間 - 10時間(一日あたりの停止時間)
    * 1週間の起動時間: **70時間** = 168時間 - (10 * 5)時間(1週間内の平日停止時間) - 48時間(1週間内の土、日停止時間)
    * 1か月の起動時間: **440時間** = 720時間 - (70 * 4)時間

## 停止方法について
* EventBrigeを用いて特定の時間にLambdaを実行するようにしている

### 考慮した点について
* NATゲートウェイは停止という概念がないため、使用していない時間はNATゲートウェイを削除している。  
削除後は設定情報も削除されるので、設定情報を保持するために、Lambdaの環境変数にあらかじめNATゲートウェイを作成する際の設定情報を保持させている

## Lambda内の処理について

```python:nat_handler.py
import calendar
import os
import json
import urllib.request
from datetime import datetime
from enum import Enum
from dataclasses import dataclass

import boto3

ec2 = boto3.client('ec2', region_name='ap-northeast-1')
# 処理対象
processing_objects = {
	# 環境変数にElasticIPやSubnetIDをあらかじめ保存して置き必要なときに呼び出せるようにしている
	'NAT_INFOMATION': os.environ['NAT_INFOMATION'],
}
destination_cidr_block = '0.0.0.0/0'


class LaunchType(Enum):
	CREATE = 'create'
	DESTROY = 'destroy'


@dataclass
class LambdaResponse:
	processed: dict
	launch_type: LaunchType

	def successful_format(self) -> dict:
		return {'statusCode': 200, 'processed {}'.format(self.launch_type.value): self.processed}

	def failed_format(self) -> dict:
		return {'statusCode': 500, 'processed {}'.format(self.launch_type.value): self.processed}


def is_holiday() -> bool:
	class HolidayName(Enum):
		SATURDAY = 'Saturday'
		SUNDAY = 'Sunday'

	# 実行時の曜日を文字列で取得(JST)
	today_str = calendar.day_name[datetime.today().weekday()]

	return today_str == HolidayName.SATURDAY.value or today_str == HolidayName.SUNDAY.value


def get_launch_type(event: dict) -> LaunchType:
	key_name = 'status'
	if event[key_name] is None:
		raise Exception('control tag name the None: variable name is {}'.format(key_name))

	launch_type = event[key_name]
	if launch_type == LaunchType.CREATE.value:
		return LaunchType.CREATE
	elif launch_type == LaunchType.DESTROY.value:
		return LaunchType.DESTROY
	else:
		raise Exception('control tag name illegal value: variable name is {}'.format(key_name))


def nat_handler(launch_type: LaunchType) -> LambdaResponse:
	processed_result = {}
	if launch_type == LaunchType.CREATE:
		for value in processing_objects.values():
			info = json.loads(value)
			response = ec2.create_nat_gateway(
				AllocationId=info['EipAllocationId'],
				SubnetId=info['SubnetId'],
				TagSpecifications=[
					{
						'ResourceType': 'natgateway',
						'Tags': [
							{
								'Key': 'Name',
								'Value': info['NatName']
							}
						]
					}
				])
			natgw_id = response['NatGateway']['NatGatewayId']
			ec2.get_waiter('nat_gateway_available').wait(NatGatewayIds=[natgw_id])
			ec2.create_route(DestinationCidrBlock=destination_cidr_block, NatGatewayId=natgw_id,
							 RouteTableId=info['PrivateRouteTableId'])

			processed_result[info['NatName']] = value
	elif launch_type == LaunchType.DESTROY:
		for value in processing_objects.values():
			info = json.loads(value)
			filters = [
				{
					'Name': 'subnet-id',
					'Values': [info['SubnetId']]
				},
				{
					'Name': 'state',
					'Values': ['available']
				},
				{
					'Name': 'tag:Name',
					'Values': [info['NatName']]
				}
			]
			response = ec2.describe_nat_gateways(Filters=filters)
			if len(response['NatGateways']) == 0:
				continue

			natgw = response['NatGateways'][0]['NatGatewayId']
			ec2.delete_nat_gateway(NatGatewayId=natgw)
			ec2.delete_route(DestinationCidrBlock=destination_cidr_block, RouteTableId=info['PrivateRouteTableId'])

			processed_result[info['NatName']] = value
	else:
		raise Exception('control tag name illegal value: variable name is launch_type')

	return LambdaResponse(processed_result, launch_type)


def lambda_handler(event, context):

	# 休日の場合は処理を実行しない
	if is_holiday():
		return {'statusCode': 300, 'message': 'today is holiday! Take a rest now!!'}

	launch_type = get_launch_type(event)
	response = nat_handler(launch_type)

	return response.successful_format()
```

### NATゲートウェイのライフサイクル

![nat_life_cycle.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2209596/f0b44a2f-d732-6323-2027-9cc5db766624.png)

### 参考URL
* https://www.ntt-tx.co.jp/column/dojo_aws_blog/20180411/
* https://dev.classmethod.jp/articles/create_nat_gateway/