こんにちは。 ディベロップメントサービス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 円
めちゃめちゃ高い。
コスト削減方法
使用しない時間帯(定時外や休日)に
NAT Gateway を削除することでコスト削減をはかります。
以下の処理を自動で行う Lambda 関数を作成します。
NAT Gateway の自動起動
- 始業時に実行(朝 9 時)
- Elastic IP を取得
- NAT Gateway 作成
- ルートテーブルの更新("0.0.0.0/0" を NAT Gateway に向ける)
NAT Gateway の自動削除
- 終業時に実行(夜 18 時)
- NAT Gateway の削除
- Elastic IP の解放
- ルートテーブルの更新("0.0.0.0/0" を 削除)
NAT Gateway を EC2 による NAT インスタンスに置き換えて
コスト削減する方法もあります。
こちらは弊社過去ブログにて紹介しているため、興味持たれた方はご参考ください。
ファイル構成
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 が削除。
ルートテーブルからも削除されてます。
画像では伝えづらいのですが、Elastic IP も解放されております。
まとめ
- NAT Gateway は意外にコストが高い(月 6000円強)
- Lambda を使って、自動起動・削除してコストを削減しよう
さいごに
自動削除後に残業で再起動することの無いように。
本ブログがどなかたのお役に立てれば幸いです。