CDKでNatInstanceProviderV2を使ってNATインスタンスを構築できるようになりました

2024.04.16

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

以上