CDKでNatInstanceProviderV2を使ってNATインスタンスを構築できるようになりました
こんにちは。CX事業本部製造ビジネステクノロジー部のakkyです。
VPCのプライベートサブネットにあるEC2インスタンスからインターネットにアクセスする際にはNATゲートウェイを使用するのが推奨されていますが、検証用環境など、安定性より費用を抑えることのほうが重要になる場合があります。 この場合にはEC2インスタンスを使ってNATインスタンスを作るのがよいでしょう。(トラフィック量など考慮するべきポイントはほかにもあります)
公式ドキュメントによると、NAT AMIはすでにアップデートされておらず非推奨となっているので、これをこのまま使うのはやめましょう。 とはいえ、構築するのに必要な手順は記載されているので、現行OSを使って構築すれば問題ありません。
ただ、NATインスタンスを使うには、EC2インスタンスの構築以外にVPCのルーティングテーブルの設定も行わないといけないので、作業は自動化したいですよね。 CDKにはNatInstanceProviderというコンストラクトがあってNATインスタンスの構築やVPCの設定をすべて行えて便利なのですが、こちらも上記NAT AMIを元にしているので、コンストラクト自体が非推奨となっています。
…ということで、NATインスタンスを作りたいときには、結局自分で書かないといけないのかと憂鬱になっていたのですが、aws-cdk v2.137.0でNatInstanceProviderV2という新たなコンストラクトが使えるようになりました!(正確には少し前から追加されていましたが、不具合がありそのまま使用できませんでした)
NatInstanceProviderV2は、cloud-initを利用して最新のAmazon Linux 2023にNATを構築するコマンドを実行させるように変更されています。
今回はNatInstanceProviderV2を使ってDual Stack VPCを作ってみましたのでご紹介します。
やってみた
構成
IPv4/v6デュアルスタックのVPCを作り、IPv4はNATインスタンス経由で通信させて、IPv6はEgress-Only インターネットゲートウェイ経由で通信させる構成とします。
構成の概要図
管理用にEC2 Instance Connect Endpointも用意しました。
使用バージョン
aws-cdk-lib 2.137.0が必要です。
スタック
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; export class Natinstanceproviderv2CdkStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // NATインスタンスを作成 const natInstance = ec2.NatProvider.instanceV2({ instanceType: ec2.InstanceType.of( ec2.InstanceClass.T4G, ec2.InstanceSize.NANO ), machineImage: ec2.MachineImage.latestAmazonLinux2023({ cpuType: ec2.AmazonLinuxCpuType.ARM_64, }), defaultAllowedTraffic: ec2.NatTrafficDirection.OUTBOUND_ONLY, }); // デュアルスタックVPCを作成 const vpc = new ec2.Vpc(this, "dualstackvpc", { maxAzs: 1, natGateways: 1, natGatewayProvider: natInstance, ipProtocol: ec2.IpProtocol.DUAL_STACK, subnetConfiguration: [ { cidrMask: 24, name: "Public", subnetType: ec2.SubnetType.PUBLIC, mapPublicIpOnLaunch: true, }, { cidrMask: 24, name: "Private", subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, }, ], }); // NATインスタンスはVPC内からのみアクセス許可 natInstance.securityGroup.addIngressRule( ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.allTraffic() ); // EC2 Instance Connect Endpoint用SGを作成 const ec2sg = new ec2.SecurityGroup(this, "ec2-sg", { vpc, }); const eicsg = new ec2.SecurityGroup(this, "eic-sg", { vpc, allowAllOutbound: false, }); ec2sg.addIngressRule(eicsg, ec2.Port.tcp(22)); eicsg.addEgressRule(ec2sg, ec2.Port.tcp(22)); // EC2 Instance Connect Endpointを作成 new ec2.CfnInstanceConnectEndpoint(this, "eice", { subnetId: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, }).subnetIds[0], securityGroupIds: [eicsg.securityGroupId], }); // 実験に使うプライベートIPのみを持つサーバを作成 const server = new ec2.Instance(this, "testserver", { vpc: vpc, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, instanceType: ec2.InstanceType.of( ec2.InstanceClass.T4G, ec2.InstanceSize.NANO ), machineImage: ec2.MachineImage.latestAmazonLinux2023({ cpuType: ec2.AmazonLinuxCpuType.ARM_64, }), }); server.addSecurityGroup(ec2sg); // IPv6インターネットアクセスを許可 const internetsg = new ec2.SecurityGroup(this, "outband-sg", { vpc, allowAllIpv6Outbound: true, }); server.addSecurityGroup(internetsg); // NATインスタンスへEC2 Instance Connect Endpoint経由でアクセスできるようにする eicsg.addEgressRule(natInstance.securityGroup, ec2.Port.tcp(22)); natInstance.securityGroup.addIngressRule(eicsg, ec2.Port.tcp(22)); } }
NATインスタンスを作成しているのが10-19行目です。ec2.NatProvider.instanceV2
を使うと、25行目にあるnatGatewayProvider
に渡すことができるようになり、ルーティングテーブル等が自動的に設定されます。
なお、NATインスタンスのセキュリティグループは少し注意が必要です。
18行目でdefaultAllowedTraffic: ec2.NatTrafficDirection.OUTBOUND_ONLY
としていますが、これはセキュリティーグループで外向き通信のみ許可する設定です。その後43-46行目でVPC内からのみアクセスを許可する設定をしています。
ec2.NatTrafficDirection.INBOUND_AND_OUTBOUND
という設定をすると、すべてのアドレス(0.0.0.0/0
)からすべてのポートへのアクセスを許可することになりますので注意してください。
動作チェック
デプロイ完了後、testserverにEC2 Instance Connectでログインしてインターネットへの疎通を確認してみます。
IPv4でもきちんと動いてますね
IPv6もOKでした
注意点
NatInstanceProviderV2で作成されるNATインスタンスには現時点ではパッケージの自動アップデート機能はありません。
運用時にはNATゲートウェイを使うことを推奨しますが、どうしてもNATインスタンスを使う場合で、アップデート作業を自動化したい場合には、instanceV2の引数に存在するuserDataプロパティでcloud-initに与えるコマンドが変更できるので、dnfを通じて自動的にセキュリティアップデートを適用する仕組みを入れる必要があるかもしれません。
以上