【NAT Gateway】簡単節約術!Lambda を使って自動起動・削除してみた

記事タイトルとURLをコピーする

こんにちは。 ディベロップメントサービス1課の山本です。

今回はコスト削減のため
AWS Lambda (以降、Lambda) を使って NAT Gateway を自動起動・削除してみます。

この記事の対象者は?

  • NAT Gateway を利用されている方
  • NAT Gateway のコストを抑えたい方

NAT Gateway のコストについて

NAT Gateway のコストは以下の通りです。(2024年8月27日 時点 東京リージョン)

NAT ゲートウェイあたりの料金 (USD/時) 処理データ 1 GB あたりの料金 (USD)
USD 0.062 USD 0.062

処理データ当たりの料金は一旦無視しても
1ヶ月起動しているだけで以下の金額がかかります。

  • 0.062 (USD/時) * 24 (時) * 30(日) * 144 (円/USD) = 約 6428 円

めちゃめちゃ高い。

料金 - Amazon VPC | AWS

コスト削減方法

使用しない時間帯(定時外や休日)に
NAT Gateway を削除することでコスト削減をはかります。

以下の処理を自動で行う Lambda 関数を作成します。

NAT Gateway の自動起動

  1. 始業時に実行(朝 9 時)
  2. Elastic IP を取得
  3. NAT Gateway 作成
  4. ルートテーブルの更新("0.0.0.0/0" を NAT Gateway に向ける)

NAT Gateway の自動削除

  1. 終業時に実行(夜 18 時)
  2. NAT Gateway の削除
  3. Elastic IP の解放
  4. ルートテーブルの更新("0.0.0.0/0" を 削除)

NAT Gateway を EC2 による NAT インスタンスに置き換えて
コスト削減する方法もあります。

こちらは弊社過去ブログにて紹介しているため、興味持たれた方はご参考ください。

blog.serverworks.co.jp

ファイル構成

AWS Serverless Application Model (以降、SAM) を利用して
Lambda 関数をデプロイします。

.
├── samconfig.toml
├── src
│   └── app.py
└── template.yaml

samconfig.toml

SAM の設定ファイルとなります。

version = 0.1

[default.deploy.parameters]
stack_name = "auto-nat-gateway-stack"
capabilities = "CAPABILITY_NAMED_IAM"
region = "ap-northeast-1"
s3_bucket = "{SAM リソースを保管する S3 バケット名}"
parameter_overrides = [
    "SystemName=dev",  # システム名
    "SubnetId=subnet-xxxxxxxxxxxxxx",  # NAT Gateway を設置するサブネット
    "RouteTableId=rtb-xxxxxxxxxxxxxx",  # NAT Gateway に向けるルートテーブル
    'StartCronExpression="cron(0 9 ? * MON-FRI *)"',  # NAT Gateway 起動時間(JST)
    'StopCronExpression="cron(0 18 ? * MON-FRI *)"'  # NAT Gateway 終了時間(JST)
]

parameter_overrides 内の値は各環境によって、変更ください。

template.yaml

SAM のテンプレートファイルとなります。

以下のリソースを作成します。

  • Lambda * 1
  • EventBridge スケジュール * 2
  • IAM Role * 1
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Auto Start/Stop NAT Gateway

Parameters:
  SystemName:
    Type: String
    Default: "dev"
    Description: "The name of the system"
  SubnetId:
    Type: String
    Description: "The Subnet ID where the NAT Gateway will be created"
  RouteTableId:
    Type: String
    Description: "The Route Table ID to update with the NAT Gateway"
  StartCronExpression:
    Type: String
    Default: "cron(0 9 ? * MON-FRI *)"
    Description: "Cron expression for starting the NAT Gateway"
  StopCronExpression:
    Type: String
    Default: "cron(0 18 ? * MON-FRI *)"
    Description: "Cron expression for stopping the NAT Gateway"
  
Resources:
  NatGatewayFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${SystemName}-nat-gateway-handler
      Description: "NAT Gateway 自動起動/停止 Lambda"
      Handler: app.lambda_handler
      Runtime: python3.12
      CodeUri: src/
      Environment:
        Variables:
          SubnetId: !Ref SubnetId
          RouteTableId: !Ref RouteTableId
      Policies:
        - AmazonEC2FullAccess
      Timeout: 900
  
  StartScheduler:
    Type: AWS::Scheduler::Schedule
    Properties:
      Name: !Sub ${SystemName}-start-nat-gateway
      Description: NAT Gateway 起動スケジュール
      ScheduleExpression: !Ref StartCronExpression
      ScheduleExpressionTimezone: "Asia/Tokyo"
      FlexibleTimeWindow:
        Mode: "OFF"
      Target:
          Arn: !GetAtt NatGatewayFunction.Arn
          Input: '{"Action": "Start"}'
          RoleArn: !GetAtt LambdaInvokeRole.Arn
  
  StopScheduler:
    Type: AWS::Scheduler::Schedule
    Properties:
      Name: !Sub ${SystemName}-stop-nat-gateway
      Description: NAT Gateway 終了スケジュール
      ScheduleExpression: !Ref StopCronExpression
      ScheduleExpressionTimezone: "Asia/Tokyo"
      FlexibleTimeWindow:
        Mode: "OFF"
      Target:
          Arn: !GetAtt NatGatewayFunction.Arn
          Input: '{"Action": "Stop"}'
          RoleArn: !GetAtt LambdaInvokeRole.Arn
  
  LambdaInvokeRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SystemName}-nat-gateway-invoke-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "scheduler.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: !Sub ${SystemName}-nat-gateway-invoke-policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "lambda:InvokeFunction"
                Resource: !GetAtt NatGatewayFunction.Arn

app.py

Lambda 関数で実行するプログラムファイルとなります。

なるべく簡単な構成で作成したかったため、起動と削除の処理は内部で分岐させてます。

import os
  
import boto3
  
ec2 = boto3.client('ec2')
  
  
def lambda_handler(event, context):
    action = event.get('Action')
    subnet_id = os.environ['SubnetId']
    route_table_id = os.environ['RouteTableId']
  
    if action == 'Start':
        start_nat_gateway(subnet_id, route_table_id)
    elif action == 'Stop':
        stop_nat_gateway(subnet_id, route_table_id)
  
  
def start_nat_gateway(subnet_id, route_table_id):
    # Elastic IP の取得
    response = ec2.allocate_address(Domain='vpc')
    allocation_id = response['AllocationId']
  
    # NAT Gateway の作成
    response = ec2.create_nat_gateway(SubnetId=subnet_id, AllocationId=allocation_id)
    nat_gateway_id = response['NatGateway']['NatGatewayId']
  
    # NAT Gateway の作成完了を待つ
    waiter = ec2.get_waiter('nat_gateway_available')
    waiter.wait(NatGatewayIds=[nat_gateway_id])
  
    # ルートの'0.0.0.0/0'存在チェック
    route_tables = ec2.describe_route_tables(RouteTableIds=[route_table_id])
    routes = route_tables['RouteTables'][0]['Routes']
    route_exists = any(route.get('DestinationCidrBlock') == '0.0.0.0/0' for route in routes)
  
    # ある場合は更新、ない場合は作成
    if route_exists:
        ec2.replace_route(
            RouteTableId=route_table_id,
            DestinationCidrBlock='0.0.0.0/0',
            NatGatewayId=nat_gateway_id
        )
    else:
        ec2.create_route(
            RouteTableId=route_table_id,
            DestinationCidrBlock='0.0.0.0/0',
            NatGatewayId=nat_gateway_id
            )
  
  
def stop_nat_gateway(subnet_id, route_table_id):
    # 指定されたサブネットに属する NAT Gateway の取得
    nat_gateways = ec2.describe_nat_gateways(Filters=[
        {'Name': 'state', 'Values': ['available']},
        {'Name': 'subnet-id', 'Values': [subnet_id]}
    ])
  
    for nat_gateway in nat_gateways['NatGateways']:
        # NAT Gateway の削除
        ec2.delete_nat_gateway(NatGatewayId=nat_gateway['NatGatewayId'])
  
        # NAT Gateway の削除完了を待つ
        waiter = ec2.get_waiter('nat_gateway_deleted')
        waiter.wait(NatGatewayIds=[nat_gateway['NatGatewayId']])
  
        # Elastic IP のリリース
        ec2.release_address(AllocationId=nat_gateway['NatGatewayAddresses'][0]['AllocationId'])
  
        # ルートテーブルのルート削除
        ec2.delete_route(
            RouteTableId=route_table_id,
            DestinationCidrBlock='0.0.0.0/0'
        )

実行コマンド

以下コマンドでデプロイします。

sam build
sam deploy

実行結果

自動起動

NAT Gateway が起動 & サブネットに紐付けられてます。

NAT Gateway 起動

ルートテーブルにも向き先が追加されてます。

ルートテーブルに追加

自動削除

NAT Gateway が削除。

NAT Gateway 削除

ルートテーブルからも削除されてます。

ルートテーブルから削除

画像では伝えづらいのですが、Elastic IP も解放されております。

まとめ

  • NAT Gateway は意外にコストが高い(月 6000円強)
  • Lambda を使って、自動起動・削除してコストを削減しよう

さいごに

自動削除後に残業で再起動することの無いように。

本ブログがどなかたのお役に立てれば幸いです。

山本 真大(執筆記事の一覧)

アプリケーションサービス部 ディベロップメントサービス1課

2023年8月入社。カピバラさんが好き。