AWS

CDKで作る!プライベートEC2インスタンス + EC2 Instance Connect Endpoint

create new reources
hiropy

はじめに

お疲れ様です!hiropyです!

今回は、EC2 Instance Connect Endpoint(以降EICEと記載)で接続するプライベートなEC2をCDKでつくってみる!という記事になります。
EICEは、プライベートなEC2インスタンスに対してSSH接続できるようにするもので、SSHキーを自身で管理せずに簡単にインスタンスに接続することができます!(参照
一時的にテスト用のインスタンスを建てたい!でもプライベートにしたいし、AWS Systems Manager用のエンドポイントを作るのは面倒くさい!という方にはぴったりかもしれません🙌

前提

本記事では、以下の内容にフォーカスしています。

  • EICE + EC2をCDKで作成する

本記事では、以下の内容には触れませんのでご了承ください。

  • CDKとは?セットアップ方法
  • EICEの詳細な説明

本記事では、以下の環境を想定しています。

  • デプロイ先のアカウントに対してcdk bootstrap の実行が完了していること

本記事では、CDKをTypeScriptで実装します。

テンプレートの概要

今回は以下のリソースをCDKで構築していきます!

  • VPC
  • プライベートサブネット
  • EC2インスタンス
  • セキュリティグループ
  • EICE

このテンプレートのポイントは以下の通りです!

  1. EC2インスタンスをプライベートサブネットに作成する
  2. EICEを作成し、プライベートサブネットのEC2にSSH接続できるようにする
  3. EICE を利用するために必要なIAMロールとセキュリティグループを作成する

コードの解説

では早速コードを見ていきましょう!

Q
CDKテンプレートの全体
import * as cdk from "aws-cdk-lib";
import { RemovalPolicy } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";

interface TestEc2StackProps extends cdk.StackProps {
  vpcConfig: {
    cidr: string;
  };
}

export class TestEc2Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: TestEc2StackProps) {
    super(scope, id, props);

    const keyPair = new ec2.KeyPair(this, `KeyPair`, {
      keyPairName: `test-key-pair`,
    });
    keyPair.applyRemovalPolicy(RemovalPolicy.DESTROY);

    const ec2Role = new iam.Role(this, `TestEC2Role`, {
      roleName: `TestEc2Role`,
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      description: "IAM role for EC2 for testing",
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
      ],
    });

    // Create a VPC with a single private subnet
    const vpc = new ec2.Vpc(this, "Vpc", {
      ipAddresses: ec2.IpAddresses.cidr(props.vpcConfig.cidr),
      maxAzs: 1,
      subnetConfiguration: [
        {
          cidrMask: 28,
          name: "Private",
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
    });

    // Get the private subnet
    const privateSubnetList = vpc.selectSubnets().subnets;

    const sgEic = new ec2.SecurityGroup(this, `EicSecurityGroup`, {
      vpc: vpc,
      description: "Security group for EC2 instance connect endpoint",
      allowAllOutbound: true,
    });

    const eic = new ec2.CfnInstanceConnectEndpoint(this, `Eic`, {
      subnetId: privateSubnetList[0].subnetId,
      clientToken: sgEic.securityGroupId,
      preserveClientIp: false,
      securityGroupIds: [sgEic.securityGroupId],
      tags: [
        {
          key: "Name",
          value: `test-eic-${vpc.vpcId}`,
        },
      ],
    });

    const sgTestEc2 = new ec2.SecurityGroup(this, `SgTestEc2`, {
      vpc: vpc,
      description: "Allow ICMP and SSH",
      allowAllOutbound: true,
      securityGroupName: `test-sg-ec2`,
    });

    sgTestEc2.addIngressRule(
      ec2.Peer.ipv4(vpc.vpcCidrBlock),
      ec2.Port.icmpPing(),
      "Allow ICMP from VPC"
    );

    sgTestEc2.addIngressRule(
      ec2.Peer.securityGroupId(sgEic.securityGroupId),
      ec2.Port.tcp(22),
      "Allow SSH from EIC Security Group"
    );

    const instanceName = `ec2-test`;
    const instance = new ec2.Instance(this, `Ec2Test`, {
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023,
      }),
      vpc: vpc,
      vpcSubnets: { subnets: privateSubnetList },
      securityGroup: sgTestEc2,
      keyPair: keyPair,
      instanceName: instanceName,
      role: ec2Role,
      blockDevices: [
        {
          deviceName: "/dev/xvda",
          volume: ec2.BlockDeviceVolume.ebs(8, {
            volumeType: ec2.EbsDeviceVolumeType.GP3,
            encrypted: true,
          }),
        },
      ],
    });
    instance.node.addDependency(eic);
  }
}

このテンプレートでは、単一のVPC, サブネット, EC2を作成し、かつEICEとインスタンスでEICEを使用するために必要な周辺リソースを作成しています。

今回は、特に重要なEICEとその設定についてのみ解説します。EICEを使用することで、インターネットに直接公開されていないプライベートサブネット内のEC2インスタンスにもSSH接続が可能になります。以下にEICEの設定に関する主要な部分を抜粋し、解説します。

const sgEic = new ec2.SecurityGroup(
  this,
  `EicSecurityGroup`,
  {
    vpc: vpc,
    description: "Security group for EC2 instance connect endpoint",
    allowAllOutbound: true,
  }
);

const eic = new ec2.CfnInstanceConnectEndpoint(this, `Eic`, {
  subnetId: privateSubnetList[0].subnetId,
  clientToken: sgEic.securityGroupId,
  preserveClientIp: false,
  securityGroupIds: [sgEic.securityGroupId],
  tags: [
    {
      key: "Name",
      value: `test-eic-${vpc.vpcId}`,
    },
  ],
});

// EC2インスタンス用のセキュリティグループにEICEからのSSH接続を許可
const sgTestEc2 = new ec2.SecurityGroup(this, `SgTestEc2`, {
  vpc: vpc,
  description: "Allow ICMP and SSH",
  allowAllOutbound: true,
  securityGroupName: `test-sg-ec2`,
});

sgTestEc2.addIngressRule(
  ec2.Peer.securityGroupId(sgEic.securityGroupId),
  ec2.Port.tcp(22),
  "Allow SSH from EIC Security Group"
);

解説

  1. EICE用セキュリティグループの作成:

    まず、EICEに専用のセキュリティグループ(sgEic)を作成します。このセキュリティグループは、すべてのアウトバウンド通信を許可する設定にします。

  2. EICE の作成:
    ec2.CfnInstanceConnectEndpoint を使用してEICEを作成します。各パラメータで設定している内容は以下のとおりです:

    • subnetId: EICEを配置するプライベートサブネットのID
    • securityGroupIds: EICEに適用するセキュリティグループのID
    • preserveClientIp: クライアントIPを保持するかどうか(ここではfalse)
  1. EC2インスタンス用セキュリティグループの設定:

    EC2インスタンス用のセキュリティグループ(sgTestEc2)を作成し、EICEからのSSH接続(ポート22)を許可するルールを追加します。これにより、EICEを経由したSSH接続が可能になります。

この設定により、プライベートサブネット内のEC2インスタンスに対して、EICEを介したセキュアなSSH接続が可能になります。直接インターネットに公開することなく、管理タスクを実行できるため、セキュリティが向上します。

カスタマイズのヒント

今回は単一のVPC, サブネットに対しEC2インスタンスを1つだけ作成しましたが、ユースケースに応じて以下のようにカスタマイズすることが可能です。

  • 複数のサブネットにEC2インスタンスを作成するようロジックを修正
  • インスタンスタイプの変更
  • 複数のサブネットやAZの追加
  • セキュリティグループルールの調整

まとめ

EICEは作成するリソースが比較的少なく、設定も簡単なので、かなり便利な機能だなと思いました!
どなたかのお役に立てていれば幸いです。

参考

AUTHOR
hiropy
hiropy
記事URLをコピーしました