こんにちは。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を通じて自動的にセキュリティアップデートを適用する仕組みを入れる必要があるかもしれません。
以上