【AWS CDK】EC2 Instance Connect Endpoint 経由で EC2 インスタンスの Windows Server に RDP 接続してみた

【AWS CDK】EC2 Instance Connect Endpoint 経由で EC2 インスタンスの Windows Server に RDP 接続してみた

はじめに

テントの中から失礼します。製造ビジネステクノロジー部の「てんとタカハシ」です!

EC2 Instance Connect Endpoint(EIC Endpoint)を使用することで、パブリック IP を持たないプライベートサブネット上の EC2 インスタンスに対してセキュアな接続が可能です。わざわざ踏み台サーバーを用意せずに、シンプルな構成で EC2 インスタンスに接続できる素敵な機能です。

この EIC Endpoint では、EC2 インスタンス上で稼働する Windows Server に対して RDP で接続することが可能です。

https://dev.classmethod.jp/articles/how-to-connect-windows-server-instance-via-eic-endpoint/

https://dev.classmethod.jp/articles/rdp-connection-to-windows-server-using-ec2-instance-connect-endpoint-eic/

本記事では、EIC Endpoint を経由して EC2 インスタンスの Windows Server に RDP で接続する構成を AWS CDK で実装します。

開発環境

開発環境は下記の通りです。

% sw_vers
ProductName:		macOS
ProductVersion:		14.6.1
BuildVersion:		23G93

% aws --version
aws-cli/2.22.7 Python/3.12.6 Darwin/23.6.0 exe/x86_64

% cdk --version
2.177.0 (build b396961)

実装のポイント

EIC Endpoint

EIC Endpoint を作成するための実装内容は下記の通りです。

  • EIC Endpoint 用のセキュリティグループを作成(allowAllOutbound: false)
  • EIC Endpoint → EC2 インスタンスの RDP 通信(TCP:3389)を許可するルールを追加
    • EIC Endpoint から EC2 インスタンスへの Egress(送信)を許可
    • EC2 インスタンスに対して EIC Endpoint からの Ingress(受信)を許可
  • L1 コンストラクトで EIC Endpoint を作成(L2 は非対応のため)
// EIC Endpoint
const endpointSecurityGroup = new ec2.SecurityGroup(
  this,
  'Ec2InstanceConnectEndpointSecurityGroup',
  {
    vpc,
    securityGroupName: `${projectName}-instance-connect-endpoint-security-group`,
    allowAllOutbound: false,
  }
);

// EIC Endpoint → EC2 インスタンスの RDP アクセスを許可(送信側)
endpointSecurityGroup.addEgressRule(
  instanceSecurityGroup,
  ec2.Port.tcp(3389)
);

// EIC Endpoint → EC2 インスタンスの RDP アクセスを許可(受信側)
instanceSecurityGroup.addIngressRule(
  endpointSecurityGroup,
  ec2.Port.tcp(3389)
);

new ec2.CfnInstanceConnectEndpoint(this, 'Ec2InstanceConnectEndpoint', {
  subnetId: vpc.selectSubnets({ subnetGroupName: 'private' }).subnetIds[0],
  securityGroupIds: [endpointSecurityGroup.securityGroupId],
});

EIC Endpoint を CDK で実装する内容は下記の記事を参考にさせていただきました。

https://zenn.dev/thyt_lab/articles/7fc528985dce9c

AMI のバージョン固定

AMI のバージョンを固定化することで、デプロイ時に意図しない EC2 インスタンスの再作成を防ぎます。

const machineImage = new ec2.LookupMachineImage({
  name: `${ec2.WindowsVersion.WINDOWS_SERVER_2019_JAPANESE_FULL_BASE}-*`,
  filters: {
    'image-type': ['machine'],
    state: ['available'],
  },
  owners: ['amazon'],
  windows: true,
});
// こちらだと AMI のバージョンが更新されるたびに EC2 インスタンスが再作成されてしまう
// const machineImage = new ec2.WindowsImage(
//   ec2.WindowsVersion.WINDOWS_SERVER_2019_JAPANESE_FULL_BASE
// );

LookupMachineImage で使用する AMI を指定すると cdk.context.json に AMI のバージョンがキャッシュされます。
その結果、以降のデプロイでは同じ AMI の ID が使用され、意図しない差分の発生を防ぐことができます。

cdk.context.json
{
  ...
  "ami:account=xxxxxxxxxxxx:filters.image-type.0=machine:filters.name.0=Windows_Server-2019-Japanese-Full-Base-*:filters.platform.0=windows:filters.state.0=available:owners.0=amazon:region=ap-northeast-1": "ami-028a21172be6f0723"
  ...
}

AMI のバージョン固定化は下記の記事を参考にさせていただきました。

https://zenn.dev/hiren/scraps/4f768929fcfe25

ソースコード

VPC

下記の記事に記載している VPC の実装と同じです。

https://dev.classmethod.jp/articles/running-windows-container-on-fargate-with-cdk/#vpc

EC2

ec2.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';

type Ec2StackProps = cdk.StackProps & {
  projectName: string;
};

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

    const { projectName } = props;

    // VPC
    const vpcName = `${projectName}-vpc`;
    const vpc = ec2.Vpc.fromLookup(this, 'Vpc', {
      vpcName,
    });

    // https://docs.aws.amazon.com/ja_jp/prescriptive-guidance/latest/optimize-costs-microsoft-workloads/right-size-selection.html#right-size-selection-next-steps
    const instanceType = ec2.InstanceType.of(
      ec2.InstanceClass.T3,
      ec2.InstanceSize.MEDIUM
    );

    const machineImage = new ec2.LookupMachineImage({
      name: `${ec2.WindowsVersion.WINDOWS_SERVER_2019_JAPANESE_FULL_BASE}-*`,
      filters: {
        'image-type': ['machine'],
        state: ['available'],
      },
      owners: ['amazon'],
      windows: true,
    });
    // こちらだと AMI のバージョンが更新されるたびに EC2 インスタンスが再作成されてしまう
    // const machineImage = new ec2.WindowsImage(
    //   ec2.WindowsVersion.WINDOWS_SERVER_2019_JAPANESE_FULL_BASE
    // );

    const instanceSecurityGroup = new ec2.SecurityGroup(
      this,
      'Ec2InstanceSecurityGroup',
      {
        vpc: vpc,
        securityGroupName: `${projectName}-instance-security-group`,
      }
    );

    const instanceRole = new iam.Role(this, 'EC2InstanceRole', {
      roleName: `${projectName}-instance-role`,
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
    });

    const keyPairName = `${projectName}-ec2-key`;
    const keyPair = new ec2.KeyPair(this, 'EC2KeyPair', {
      keyPairName,
    });

    const volumeSize = 100;
    const vpcSubnets = vpc.selectSubnets({ subnetGroupName: 'private' });
    const blockDevices = [
      {
        // ルートボリューム上書き
        deviceName: '/dev/sda1',
        volume: ec2.BlockDeviceVolume.ebs(volumeSize, {
          volumeType: ec2.EbsDeviceVolumeType.GP3,
        }),
      },
    ];

    new ec2.Instance(this, 'EC2Instance', {
      vpc,
      vpcSubnets,
      instanceName: `${projectName}-instance`,
      instanceType,
      machineImage,
      securityGroup: instanceSecurityGroup,
      role: instanceRole,
      keyPair,
      blockDevices,
      ebsOptimized: true,
      requireImdsv2: true,
    });

    // EIC Endpoint
    const endpointSecurityGroup = new ec2.SecurityGroup(
      this,
      'Ec2InstanceConnectEndpointSecurityGroup',
      {
        vpc,
        securityGroupName: `${projectName}-instance-connect-endpoint-security-group`,
        allowAllOutbound: false,
      }
    );

    // EIC Endpoint → EC2 インスタンスの RDP アクセスを許可(送信側)
    endpointSecurityGroup.addEgressRule(
      instanceSecurityGroup,
      ec2.Port.tcp(3389)
    );

    // EIC Endpoint → EC2 インスタンスの RDP アクセスを許可(受信側)
    instanceSecurityGroup.addIngressRule(
      endpointSecurityGroup,
      ec2.Port.tcp(3389)
    );

    new ec2.CfnInstanceConnectEndpoint(this, 'Ec2InstanceConnectEndpoint', {
      subnetId: vpc.selectSubnets({ subnetGroupName: 'private' }).subnetIds[0],
      securityGroupIds: [endpointSecurityGroup.securityGroupId],
    });
  }
}

RDP で接続する手順

RDP 接続用のパスワードを取得

CDK で EC2 インスタンスのキーペアを作成した場合、秘密鍵の内容は SSM パラメータストアで確認できます。秘密鍵の内容をファイルに保存してください。

スクリーンショット 2025-04-06 22.18.43

対象の EC2 インスタンスを選択して「接続」に進みます。

スクリーンショット 2025-04-06 22.24.08

「RDP クライアント」から「パスワードを取得」に進みます。

スクリーンショット 2025-04-06 22.24.18

「プライベートキーファイルのアップロード」で秘密鍵をアップロードした後、「パスワードを復号化」します。

スクリーンショット 2025-04-06 22.24.32

RDP 接続用のパスワードが表示されますので手元にコピーします。

スクリーンショット 2025-04-06 22.24.39

WebSocket トンネルを作成

下記を実行して WebSocket トンネルを作成します。

% aws ec2-instance-connect open-tunnel \
  --instance-id <対象の EC2 インスタンス ID> \
  --remote-port 3389 \
  --local-port 13389

Windows App をインストール

Microsoft が提供しているリモート接続アプリ「Windows App」を MacBook Pro にインストールします。このアプリは、従来の Microsoft Remote Desktop の後継にあたるもので、RDP 接続に対応しています。

https://apps.apple.com/jp/app/windows-app/id1295203466?mt=12

Windows App から RDP 接続

Windows App の画面右上「+」から「Add PC」を選択します。

スクリーンショット 2025-04-06 22.32.10

「PC name」に localhost:13389 を入力した後、「Add」します。

スクリーンショット 2025-04-06 22.33.56

localhost:13389 を選択します。

スクリーンショット 2025-04-06 22.34.00

下記を入力した後、「Continue」します。

  • Username: Administrator
  • Password: 先ほどコピーしたパスワード

スクリーンショット 2025-04-06 22.34.28

「Continue」します。

スクリーンショット 2025-04-06 22.34.32

Windows Server への接続が完了します!

スクリーンショット 2025-04-06 22.35.39

おわりに

本記事では、EC2 Instance Connect Endpoint(EIC Endpoint)を経由して EC2 インスタンスの Windows Server に RDP で接続する構成を AWS CDK で実装してみました。EIC Endpoint を使用することで、踏み台サーバーを用意せずにセキュアかつシンプルな構成を比較的簡単に構築できるのは非常に便利です。

個人的には、Windows Server 上で稼働しているシステムを AWS にマイグレーションするためのサービスや機能を検証する際にも活用しています。この環境で検証した内容を別の記事で公開できるように様々な検証を試していこうと考えています。

最後までお読みいただきありがとうございました!この記事が少しでも参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.