はじめに
こんにちは、エンジニアの石橋です。 今回はNAT GatewayをLambda(Python)で任意の時間だけ立ち上げ、管理するお話です。
VPC Lambdaにおいて、何もしなければLambdaにパブリックIPアドレスが付与されていないためインターネットへの接続が失敗します。 そのため、下図の構成でNAT Gatewayを通してインターネットへ接続するようにします。*1
しかしながら、NAT Gatewayは2024年8月時点、アジアパシフィック(東京)において、何もしなくてもおいておくだけで0.062USD/時 掛かります*2。これは約46USD/月=6670円/月になります。(1ドル=145円計算) 1日1回しかLambdaを起動しない、もしくは夜間の業務時間外は確実に動かさない等の状況であれば、NAT Gatewayを使わない間は止めておけば節約できるというのが、今回のモチベーションです。 ただし、節約と言ってもその準備に時間が掛かると元も子もないので、IaCを利用したサーバーレスアプリケーションを構築するフレームワークを利用し、今回はAWS SAM*3で管理できるようにしました。この記事ではVPCを0から作りながら、NATGatewayをLambdaでできるようになるまでを紹介していきます。
ちなみにネットワークインターフェースにElastic IP(EIP)をアタッチする方法もありますが*4、今回はLambdaをプライベートサブネット上で動かす前提なので触れません。
下準備
まずはcloudformationでVPC、SecurityGroup, Elastic IPを用意します。 コードは以下に用意してあり、これを活用していきます。
後述のrainコマンドやsamコマンドを利用するにあたり、AWSへの認証手続きについては省略するので適宜設定をお願いします。*5
VPC作成
Cloudformationのテンプレートファイル( cloudformation/network.yaml )とrainコマンドを利用してVPCとサブネットを用意します。
$ rain deploy cloudformation/network.yaml dev-network
dev-network
はスタック名なので、適宜自由に変更しても構いません。
環境毎に分けられるようにEnv
パラメータを設定していますが、今回はデフォルトのdev
のままにしておきます。
rainコマンドの詳細については以下を参考にしてください。
SecurityGroup作成
VPCと同様にセキュリティグループも設定します。このセキュリティグループはNAT Gatewayが立ち上がった後に正しくインターネットに接続できるかどうかを確認するためにLambdaに設定するもので、便宜上用意しています。見ての通り、特にAWSリソースへのトラフィックを特別に制御しているわけではなく、デフォルトのままとなっています。NAT Gatewayを通してインターネットに接続するLambdaにおいて、AWSリソースへのトラフィックを制御したい場合にここを編集するとよいです。
Resources: SecurityGroupLambda: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${Env}-sg-lambda" GroupDescription: security group for lambda VpcId: Fn::ImportValue: !Sub "${Env}VPCID" Tags: - Key: Name Value: !Sub "${Env}-sg-lambda" - Key: Env Value: !Ref Env
cloudformation/sg.yamlを利用して以下のコマンドでセキュリティグループを設定します。
$ rain deploy cloudformation/sg.yaml dev-sg
Env
パラメータはVPCで設定したものと同じものにしておきます。(デフォルトではdev
)
Elastic IP作成
Elastic IPはNAT Gateway立ち上げ毎に用意するのではなく、予め用意しておきます。常時パブリックIPv4アドレスを維持するコストとして、約3.7ドル/月(=536円/月) 掛かりますが、毎回NAT Gatewayが作成される毎にLambdaのパブリックIPv4アドレスが変わることはありません。
cloudformation/eip.yamlを利用して以下のコマンドでElastic IPを用意します。
$ rain deploy cloudformation/eip.yaml dev-eip
Env
パラメータはVPCで設定したものと同じものにしておきます。(デフォルトではdev
)
NAT Gatewayを管理するLambda
Lambdaのコード
↓Nat Gatewayを作成するLambdaのコード
import os import boto3 ec2 = boto3.client('ec2') def lambda_handler(event, context): public_subnet_id = os.environ['PUBLIC_SUBNET_ID'] eip_allocation_id = os.environ['EIP_ALLOCATION_ID'] rtb_id = os.environ['RTB_ID'] response = ec2.create_nat_gateway( AllocationId=eip_allocation_id, SubnetId=public_subnet_id ) nat_id = response['NatGateway']['NatGatewayId'] ec2.get_waiter('nat_gateway_available').wait(NatGatewayIds=[nat_id]) response = ec2.create_route( DestinationCidrBlock = '0.0.0.0/0', NatGatewayId = nat_id, RouteTableId = rtb_id )
↓Nat Gatewayを削除するLambdaのコード
import os import boto3 ec2 = boto3.client('ec2') def lambda_handler(event, context): rtb_id = os.environ['RTB_ID'] response = ec2.delete_route( DestinationCidrBlock = '0.0.0.0/0', RouteTableId = rtb_id ) filters = [{'Name': 'state', 'Values': ['available']}] response = ec2.describe_nat_gateways(Filters=filters) for rec in response['NatGateways']: natgw = rec['NatGatewayId'] ec2.delete_nat_gateway(NatGatewayId=natgw)
Lambdaの実行Role
natgw_scheduler/up.pyとnatgw_scheduler/down.pyを実行する上で必要最低限のものだけを以下のように設定しています。
FunctionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${Env}-natgw-scheduler-execution-role" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: !Sub ${Env}-natgw-scheduler-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - logs:TagResource Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* - Effect: Allow Action: - ec2:CreateRoute - ec2:DeleteRoute - ec2:CreateNatGateway - ec2:DescribeNatGateways - ec2:DeleteNatGateway Resource: "*"
SAMのtemplate.yaml
NAT Gatewayを作成する natgw_scheduler/up.py のSAMの設定は以下のとおりです。
Resources: UpNatGWFunction: Type: AWS::Serverless::Function Properties: Timeout: 180 FunctionName: !Sub "${Env}-nat-up" CodeUri: natgw_scheduler/ Handler: up.lambda_handler Role: !GetAtt FunctionRole.Arn Events: Schedule1: Type: ScheduleV2 Properties: Name: !Sub "${Env}-nat-up-schedule" ScheduleExpression: "cron(50 6 * * ? *)" ScheduleExpressionTimezone: "Asia/Tokyo"
ScheduleExpression "cron(50 6 * * ? *)"
とScheduleExpressionTimezone "Asia/Tokyo"
の設定により毎日、日本時間の6:50にNAT Gatewayが起動するようにLambdaが実行されます。
natgw_scheduler/up.py のLambdaの処理は約2分程度掛かるので、 Timeout: 180
とタイムアウトを3分に設定しています。
NAT Gatewayを削除するnatgw_scheduler/down.pyのSAMの設定は以下のとおりです。
Resources: # (中略) DownNatGWFunction: Type: AWS::Serverless::Function Properties: Timeout: 180 FunctionName: !Sub "${Env}-nat-down" CodeUri: natgw_scheduler/ Handler: down.lambda_handler Role: !GetAtt FunctionRole.Arn Events: Schedule1: Type: ScheduleV2 Properties: Name: !Sub "${Env}-nat-down-schedule" ScheduleExpression: "cron(10 23 * * ? *)" ScheduleExpressionTimezone: "Asia/Tokyo"
こちらは毎日日本時間の23:10にNAT Gatewayが削除するようにLamdbaが実行されます。
今回は1日1回NAT Gatewayを作成するように設定していますが、1日に何度もNAT Gatewayの作成と削除を繰り返すと以下の費用が発生する可能性があるのでご注意ください。*6
USD 0.10: キャリア IP アドレスのリマップ 1 回あたり – 1 か月間で 100 リマップを超える追加分
環境変数は以下にてdev-network
のスタックの出力よりインポートして設定しています。
Globals: Function: Environment: Variables: PUBLIC_SUBNET_ID: Fn::ImportValue: !Sub "${Env}PublicSubnet1" RTB_ID: Fn::ImportValue: !Sub "${Env}PrivateRouteTable" EIP_ALLOCATION_ID: Fn::ImportValue: !Sub "${Env}ElasticIPAllocationId"
SAMでデプロイ
template.yamlがある場所で以下のコマンドを実行します。これでNAT Gatewayを管理するLambdaのデプロイが完了します。
$ sam build --use-container $ sam deploy --guided --capabilities CAPABILITY_NAMED_IAM
デプロイ時に実行ロールを設定しているため--capabilities CAPABILITY_NAMED_IAM
オプション*7が必要になります。
要求されるパラメータについてはデフォルト値で問題ありません。
テスト
実はtemplate.yamlにテスト用のLambda関数(dev-hello-world
)を設定してあるので、インターネットへ接続するdev-hello-world
というLambda関数がすでにデプロイされています。
import json import requests def lambda_handler(event, context): response = requests.get("https://www.industry-one.com/") if response.status_code != 200: return { "statusCode": 500, "body": json.dumps({ "message": "failed to get industry-one.com", }), } return { "statusCode": 200, "body": json.dumps({ "message": "hello world", }), }
Resources: # (中略) HelloWorldFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub "${Env}-hello-world" CodeUri: hello_world/ Handler: app.lambda_handler VpcConfig: SubnetIds: - Fn::ImportValue: !Sub "${Env}PrivateSubnet1" - Fn::ImportValue: !Sub "${Env}PrivateSubnet2" SecurityGroupIds: - Fn::ImportValue: !Sub "${Env}SecurityGroupLambda"
上記Lambda関数でNAT Gatewayがないときとあるときの挙動の確認を行います。
NAT Gatewayがない場合
以下のようにdev-hello-world
を実行してもインターネットへの接続ができず、タイムアウトします。
NAT Gatewayがある場合
以下のように成功します。
Cleanup
NAT Gatewayが作成されている場合は適宜削除してください。その後、以下のコマンドで今回利用した環境を削除することができます。
$ sam delete --stack-name "natgw-scheduler" $ rain rm dev-eip $ rain rm dev-sg $ rain rm dev-network
おわりに
以上で、プライベートサブネットで動作するLambda関数のインターネット接続をするためのNAT GatewayをLambdaで管理できました。プロジェクトの性質に依存しますが、時折Lambdaを使って処理をするケースがあるので、必要があるときのみNAT Gatewayを作成することでNAT Gatewayの費用を抑えることができると思います。
インダストリー・ワンでは一緒に働く仲間を募集しています。エンジニアの採用は以下より、
もし、より弊社について知りたい方は、以下よりカジュアル面談のご応募お待ちしております。
*1:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-vpc-internet.html
*2:https://aws.amazon.com/jp/vpc/pricing/
*3:https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html
*4:https://zenn.dev/nix/articles/7dd29a1e9edc55
*5:https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html
*6:https://aws.amazon.com/jp/ec2/pricing/on-demand/#Elastic_IP_Addresses
*7:https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/APIReference/API_CreateStack.html