EC2 Instance Connect EndpointをCDKで作成してみた

2023.06.16

こんにちは。CX事業本部Delivery部のakkyです。

EC2 Instance Connect Endpoint(EIC)がにわかに話題になっていますね! プライベートサブネットへセッションマネージャーを使わずに接続できて、EC2以外にもトンネル可能、しかもなんと無料!ということで、これを使わない手はありません!

DevelopersIOにも、さっそくたくさんの記事が投稿されています。

EICエンドポイントは最新のリソースなので、6月16日現在、作成をするにはマネジメントコンソール、CLI、SDKからの操作が必要です。セキュリティグループの作成も必要なので、毎回手で作るのは少々面倒ですね。

今回はCDKのカスタムリソース機能を利用し、いちはやくEICエンドポイントを作成できるようにしてみましたのでご紹介します。

(2023/10/26追記)L1コンストラクトがサポートされましたので、現在はこの記事の方法で実装する必要はありません: class CfnInstanceConnectEndpoint (construct)

コード

CDKにはカスタムリソースとして、コンストラクトもCloudFormationも対応していないリソースを作成できる機能があります。

カスタムリソースの作成には、通常はLambdaを書く必要があるのですが、EICエンドポイントはAPIリクエストすれば作成、削除が可能なので、AwsCustomResourceを利用してリソースを作成してみました。

詳しくは以下の記事で解説されています。

完成したのが以下のスタックです。

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export class EicCdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, "vpc", {
      maxAzs: 1,
      subnetConfiguration: [
        {
          name: 'Protected',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        },
      ],
    });

    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));

    const bastion = new ec2.Instance(this, 'bastion', {
      vpc: vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
      machineImage: ec2.MachineImage.latestAmazonLinux2023({ cpuType: ec2.AmazonLinuxCpuType.X86_64 }),
      securityGroup: ec2sg
    });

    const eic = new cr.AwsCustomResource(this, 'eic', {
      installLatestAwsSdk: true,
      onUpdate: {
        service: 'EC2',
        action: 'createInstanceConnectEndpoint',
        parameters: {
          SubnetId: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }).subnetIds[0],
          DryRun: false,
          PreserveClientIp: true,
          SecurityGroupIds: [
            eicsg.securityGroupId
          ]
        },
        physicalResourceId: cr.PhysicalResourceId.fromResponse("InstanceConnectEndpoint.InstanceConnectEndpointId")
      },
      onDelete: {
        service: 'EC2',
        action: 'deleteInstanceConnectEndpoint',
        parameters: {
          DryRun: false,
          InstanceConnectEndpointId: new cr.PhysicalResourceIdReference()
        },
      },
      policy: cr.AwsCustomResourcePolicy.fromStatements([
        new iam.PolicyStatement({
          actions: [
            "ec2:CreateInstanceConnectEndpoint",
            "ec2:CreateNetworkInterface",
            "ec2:CreateTags",
            "ec2:DeleteInstanceConnectEndpoint"],
          resources: ["*"],
        }),
      ]),
    });
  }
}

プライベートサブネットのみ存在するVPCと接続確認用のEC2インスタンスを一つ作りました。

必要なセキュリティグループは先にご紹介した記事をご参照ください。

AwsCustomResourceのポイント

AwsCustomResourceを使うと、AWS SDK for JavaScriptの対応するメソッドを呼び出すLambdaを自動的に作ってくれます。

私は初めてAwsCustomResourceを使ってみたのですが、ハマるポイントも多かったので以下に記します。

エンドポイント

EICエンドポイントの作成には、EC2.createInstanceConnectEndpoint、削除にはEC2.deleteInstanceConnectEndpointを呼び出す必要があります。

parametersに指定するパラメータがそのまま渡されるようです。

なお、最新のSDKが必要なので、installLatestAwsSdktrueとしました。

physicalResourceId

リソース名はcreateInstanceConnectEndpointの応答にInstanceConnectEndpointIdとして渡されますので、これを使います。 cr.PhysicalResourceId.fromResponseを使うとうまく取り込んでくれました。

削除時にはこの名前を使いますので、new cr.PhysicalResourceIdReference()を指定してください。

ポリシー

ここが一番ハマったポイントでした。

ドキュメントのサンプルコードにはcr.AwsCustomResourcePolicy.fromSdkCallsが使われています。しかし、このメソッドはドキュメントにもありますが、呼び出しメソッド名とIAMポリシー名が同じ時にしか使えません。

EICエンドポイントの作成には、ec2:CreateInstanceConnectEndpointec2:CreateNetworkInterfaceec2:CreateTagsが必要で、削除にはec2:DeleteInstanceConnectEndpointが必要なので、手動で設定しました。

マネジメントコンソールでも他のVPCエンドポイントとは別の扱いになっていますが、必要な権限も新設されているのですね。

ポリシーが足りないと、Received response status [FAILED] from custom resource. Message returned: You are not authorized to perform this operation. Encoded authorization failure messageとして暗号化されたエラーメッセージが表示されますが以下の記事を参考にデコードできました。

まとめ

CDKでEICエンドポイントを作成してみました。カスタムリソースの作成は初めてだったのですが、APIの呼び出しで済むのであれば大きな手間がなく作れて便利ですね。

EICエンドポイントはとても便利で有用なサービスだと思っていますので、CloudFormation/CDKで手軽に作れるようになることを期待しています。