空っぽのAWSにWindows Serverの検証環境を爆速構築&SSMでセキュアに接続する
アノテーションのwataboです。
入社後数日が経ち、おっかなびっくりながらチケットを任されるようになってきました。
そろそろブログを書いていこうと思います。
3行で説明
- 何もないところからセキュアにEC2に接続する環境を作るためには、構築するリソースが多い
- すぐに環境を作りたければ、リソースは AWS CDK / CloudFormation を使って構築するとラク
- Win環境を作りたい人向けに CFn テンプレートを掲載しますので、お使いください
想定読者
- Windows Server の手頃な環境をAWSでサクッと作りたい方
- パブリックIPを使ってリモート接続するのがセキュリティ的にNGな方
- すぐEC2に接続したいけど VPC とか SSM とか色々作ったり繋げたりするのが面倒な方
- CloudFormation による IaC の恩恵を手軽に受けてみたい方
背景
テクニカルサポート担当者をはじめ、クラスメソッドグループのエンジニアの方には、検証用のAWSアカウントが支給されます。
IAMなどの権限周りの制約を除けば、何もリソースはありません。
的確なサポートを行うためには、お客様の環境になるべく近い環境を、実際に構築して検証するのが基本です。
ですので実際に構築してみるのですが、ゼロスタートだと EC2 インスタンス1つを構築して接続するという要件だけでも、実は結構多くのリソースを作る必要があることがわかります。
というわけで、今回は CloudFormation の機能を使ってジェットスタートする試みを行いました。
要件の整理
必須のリソース
まずは必要となるAWSリソースを整理します。
必須のものだけでも以下が挙げられるでしょう。
- VPC
- サブネット
- インターネットゲートウェイ (igw)
- (プライベートサブネットでは、NATゲートウェイ)
- ルートテーブル
- セキュリティグループ
- キーペア
セキュリティを考慮した接続
さらに、パブリックIPを 使用せず に繋ぐ場合を考えます。
もう少し要件が厳しくなり、以下のリソースが追加で必要となります。
- SSM Agent 接続用 IAM ロール
- VPC エンドポイント
セキュアな接続には SSM Agent (Systems Manager) を使う方法がありますが、VPCエンドポイントが2つ必要です。
(SSM Version 3.3.40 以上の場合)
- com.amazonaws.<region>.ssm
- com.amazonaws.<region>.ssmmessages
- EC2 Instance Connect Endpoint (EIC エンドポイント)
また、Systems Manager の利用に制約があるパターンも考慮して、EICエンドポイントも作成することにしました。
そもそもなぜCloudFormation?
- 必要な時にすぐ作れるから
- 要らなくなったらすぐ消せるから
他にも利点は沢山あるのですが、今回はこの2つの理由にフォーカスします。
ここまで列挙したリソースを毎回コンソールで作ったり消したりすると、なかなか大変なのです。
さらに、VPCエンドポイントなどは存在するだけで料金が掛かります。
なるべく使わない時はリソースは削除してAWS利用料を節約したいという声もあるかと思いますので、その点も考慮しました。
CFnテンプレート
もともと AWS CDK + Python を利用して作成していましたが、わかりやすさ優先でyamlとして掲載することにしました。
長くなりますので、テンプレートは文末に掲載します。
事前に用意が必要なもの
- キーペア
既存のものをご用意ください。
このテンプレートで作成されるもの
- VPC
- プライベートサブネット
- セキュリティグループ(HTTPS送信のみ許可)
- EC2用
- EICエンドポイント用
- Windows Server EC2インスタンス(t3.medium)
- EBS ボリューム (gp3, 30GiB)
- EC2インスタンス用IAMロール(インスタンスプロファイル)
- VPCエンドポイント
- SSM
- SSM Messages
- EICエンドポイント
なお、今回のこだわりポイントとして、Windows Server のバージョンと言語を Parameters で選択可能としています。デフォルトでは Windows Server 2022 日本語版が起動します。
このテンプレートで作成されないもの
- NATゲートウェイ
金額面・セキュリティ面を考慮し、今回は外しました。
インターネット接続が必要な場合は、別途作成する必要がありますのでご注意ください。
CloudFormation で構築
いくつか手段はあるのですが、今回は CloudFormation コンソールからyamlファイルを読み込ませる方式を取ります。
スタックの作成 > 新しいリソースを使用(標準) を選択。
テンプレートファイルをアップロードします。
「スタックの詳細を指定」で任意のスタック名とパラメータを選択します。
ここでキーペアとWindows Serverの言語とバージョンを選択できます。
パラメータに応じてSSM Parameter Store に保存されている値を取得する仕組みになっています。
今回は Windows Server 2019 英語版を選択しました。
オプションがいくつか指定できるのですが、特に指定しなくても構築可能です。
正常に作成できると、CloudFormation コンソールから状態を確認できます。
作成中は CREATE_IN_PROGRESS
と表示されます。
右ペインには、それぞれのリソースの構築状況が表示されます。
無事に作成できると CREATE_COMPLETE
となります。
ここまで大体3〜4分程度かかりました。
接続してみる
無事に作成できたか確認します。
SSM Agent 経由
EC2コンソール > 「接続」 から接続できます。
「セッションマネージャー」を選択して「接続」します。
Powershellが立ち上がれば、成功です。
起動直後はコンソールがうまく表示されないことがあるので、その際はEnterキーを押下してください。
フリートマネージャー経由 (RDP)
オンプレとの接続がなくても、フリートマネージャーを使えばRDP(リモートデスクトップ)接続も可能です。
今回は、キーペアを選択して接続します。
無事にRDP接続できました。
EICエンドポイント経由
今回は触れませんが、EC2 Instance Connect Endpoint での接続も可能です。
別途 CloudShell または ローカル AWS CLI からコマンドを実行する必要があります。
以下の記事にわかりやすくまとまっていますので、参考になるかと思います。
付録: CFnテンプレート (yaml)
コーディングには Claude Code を使っています。
AWSTemplateFormatVersion: '2010-09-09'
# SSM / EICE経由でリモートデスクトップ接続可能なWindows EC2環境
Description: 'Windows EC2 with SSM and EIC Endpoint'
Parameters:
WindowsVersion:
Type: String
Default: '2022'
AllowedValues:
- '2016'
- '2019'
- '2022'
- '2025'
Description: 'Windows Server Version'
WindowsLanguage:
Type: String
Default: 'Japanese'
AllowedValues:
- 'English'
- 'Japanese'
Description: 'Windows Server Language'
KeyPairName:
Type: String
Description: 'Please specify an existing EC2 key pair name.'
ConstraintDescription: '既存のキーペア名を入力してください'
Mappings:
WindowsAmiParameters:
'2016':
English: '/aws/service/ami-windows-latest/Windows_Server-2016-English-Full-Base'
Japanese: '/aws/service/ami-windows-latest/Windows_Server-2016-Japanese-Full-Base'
'2019':
English: '/aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base'
Japanese: '/aws/service/ami-windows-latest/Windows_Server-2019-Japanese-Full-Base'
'2022':
English: '/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base'
Japanese: '/aws/service/ami-windows-latest/Windows_Server-2022-Japanese-Full-Base'
'2025':
English: '/aws/service/ami-windows-latest/Windows_Server-2025-English-Full-Base'
Japanese: '/aws/service/ami-windows-latest/Windows_Server-2025-Japanese-Full-Base'
Resources:
# VPC関連
SsmEc2RdpVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc
# パブリックサブネット1
SsmEc2RdpVpcPublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [0, !GetAZs '']
CidrBlock: 10.0.0.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: aws-cdk:subnet-name
Value: Public
- Key: aws-cdk:subnet-type
Value: Public
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc/PublicSubnet1
VpcId: !Ref SsmEc2RdpVpc
# パブリックサブネット1のルートテーブル
SsmEc2RdpVpcPublicSubnet1RouteTable:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc/PublicSubnet1
VpcId: !Ref SsmEc2RdpVpc
# パブリックサブネット1のルートテーブル関連付け
SsmEc2RdpVpcPublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref SsmEc2RdpVpcPublicSubnet1RouteTable
SubnetId: !Ref SsmEc2RdpVpcPublicSubnet1
# パブリックサブネット1のデフォルトルート
SsmEc2RdpVpcPublicSubnet1DefaultRoute:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref SsmEc2RdpVpcIGW
RouteTableId: !Ref SsmEc2RdpVpcPublicSubnet1RouteTable
DependsOn: SsmEc2RdpVpcVPCGW
# プライベートサブネット1
SsmEc2RdpVpcPrivateIsolatedSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [0, !GetAZs '']
CidrBlock: 10.0.2.0/24
MapPublicIpOnLaunch: false
Tags:
- Key: aws-cdk:subnet-name
Value: PrivateIsolated
- Key: aws-cdk:subnet-type
Value: Isolated
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc/PrivateIsolatedSubnet1
VpcId: !Ref SsmEc2RdpVpc
# プライベートサブネット1のルートテーブル
SsmEc2RdpVpcPrivateIsolatedSubnet1RouteTable:
Type: AWS::EC2::RouteTable
Properties:
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc/PrivateIsolatedSubnet1
VpcId: !Ref SsmEc2RdpVpc
# プライベートサブネット1のルートテーブル関連付け
SsmEc2RdpVpcPrivateIsolatedSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref SsmEc2RdpVpcPrivateIsolatedSubnet1RouteTable
SubnetId: !Ref SsmEc2RdpVpcPrivateIsolatedSubnet1
# インターネットゲートウェイ
SsmEc2RdpVpcIGW:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc
# VPCゲートウェイアタッチメント
SsmEc2RdpVpcVPCGW:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref SsmEc2RdpVpcIGW
VpcId: !Ref SsmEc2RdpVpc
# SSM VPCエンドポイント用セキュリティグループ
SsmVpcEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for SSM VPC endpoint
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: '-1'
SecurityGroupIngress:
- CidrIp: !GetAtt SsmEc2RdpVpc.CidrBlock
Description: !Sub 'from ${SsmEc2RdpVpc.CidrBlock}:443'
FromPort: 443
IpProtocol: tcp
ToPort: 443
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc/SsmVpcEndpoint
VpcId: !Ref SsmEc2RdpVpc
# SSM VPCエンドポイント
SsmVpcEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !GetAtt SsmVpcEndpointSecurityGroup.GroupId
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssm'
SubnetIds:
- !Ref SsmEc2RdpVpcPrivateIsolatedSubnet1
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc/SsmVpcEndpoint
VpcEndpointType: Interface
VpcId: !Ref SsmEc2RdpVpc
# SSM Messages VPCエンドポイント用セキュリティグループ
SsmMessagesVpcEndpointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for SSM Messages VPC endpoint
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: '-1'
SecurityGroupIngress:
- CidrIp: !GetAtt SsmEc2RdpVpc.CidrBlock
Description: !Sub 'from ${SsmEc2RdpVpc.CidrBlock}:443'
FromPort: 443
IpProtocol: tcp
ToPort: 443
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc/SsmMessagesVpcEndpoint
VpcId: !Ref SsmEc2RdpVpc
# SSM Messages VPCエンドポイント
SsmMessagesVpcEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
SecurityGroupIds:
- !GetAtt SsmMessagesVpcEndpointSecurityGroup.GroupId
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssmmessages'
SubnetIds:
- !Ref SsmEc2RdpVpcPrivateIsolatedSubnet1
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpVpc/SsmMessagesVpcEndpoint
VpcEndpointType: Interface
VpcId: !Ref SsmEc2RdpVpc
# EC2インスタンス用セキュリティグループ
SsmEc2RdpSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for SSM EC2 RDP access
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: '-1'
SecurityGroupIngress:
- Description: RDP access from EC2 Instance Connect Endpoint
FromPort: 3389
IpProtocol: tcp
SourceSecurityGroupId: !GetAtt EiceSecurityGroup.GroupId
ToPort: 3389
VpcId: !Ref SsmEc2RdpVpc
# EC2インスタンス用IAMロール
SsmEc2RdpRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: ec2.amazonaws.com
Version: '2012-10-17'
Description: IAM role for SSM EC2 RDP instance
ManagedPolicyArns:
- !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore'
# EC2インスタンス用インスタンスプロファイル
SsmEc2RdpInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref SsmEc2RdpRole
# Windows EC2インスタンス
SsmEc2RdpInstance:
Type: AWS::EC2::Instance
Properties:
AvailabilityZone: !Select [0, !GetAZs '']
IamInstanceProfile: !Ref SsmEc2RdpInstanceProfile
ImageId: !Sub
- '{{resolve:ssm:${ParameterPath}:1}}'
- ParameterPath: !FindInMap [WindowsAmiParameters, !Ref WindowsVersion, !Ref WindowsLanguage]
InstanceType: t3.medium
KeyName: !Ref KeyPairName
SecurityGroupIds:
- !GetAtt SsmEc2RdpSecurityGroup.GroupId
SubnetId: !Ref SsmEc2RdpVpcPrivateIsolatedSubnet1
Tags:
- Key: Name
Value: SsmEc2RdpDynamicStack/SsmEc2RdpInstance
UserData:
Fn::Base64: !Sub |
<powershell>
# リモートデスクトップを有効化
Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections" -Value 0
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
# SSM Agentが正常に動作するためのレジストリ設定
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "LocalAccountTokenFilterPolicy" -Value 1
# タイムゾーンを東京時間に変更
tzutil /s "Tokyo Standard Time"
</powershell>
DependsOn: SsmEc2RdpRole
# EC2 Instance Connect Endpoint用セキュリティグループ
EiceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for EC2 Instance Connect Endpoint
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: '-1'
VpcId: !Ref SsmEc2RdpVpc
# EC2 Instance Connect Endpoint
InstanceConnectEndpoint:
Type: AWS::EC2::InstanceConnectEndpoint
Properties:
PreserveClientIp: false
SecurityGroupIds:
- !GetAtt EiceSecurityGroup.GroupId
SubnetId: !Ref SsmEc2RdpVpcPrivateIsolatedSubnet1
Tags:
- Key: Description
Value: EC2 Instance Connect Endpoint for RDP access
- Key: Name
Value: EICE-for-RDP
Outputs:
VpcId:
Description: VPC ID
Value: !Ref SsmEc2RdpVpc
Export:
Name: !Sub '${AWS::StackName}-VpcId'
InstanceId:
Description: EC2 Instance ID
Value: !Ref SsmEc2RdpInstance
Export:
Name: !Sub '${AWS::StackName}-InstanceId'
InstanceConnectEndpointId:
Description: EC2 Instance Connect Endpoint ID
Value: !Ref InstanceConnectEndpoint
Export:
Name: !Sub '${AWS::StackName}-InstanceConnectEndpointId'
ConnectionInstructions:
Description: How to Connect via EICE
Value: !Sub |
1. インスタンスIDを確認: ${SsmEc2RdpInstance}
2. ポートフォワーディング: aws ec2-instance-connect open-tunnel --instance-id ${SsmEc2RdpInstance} --remote-port 3389 --local-port 13389
3. リモートデスクトップクライアントで localhost:13389 に接続
以上です。
本エントリがどなたかの助けになれば幸いです。
参考
ユーザはビルトインの Administrator を使用しましたが、ドメインユーザでもRDP可能です。
アノテーション株式会社について
アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。