空っぽのAWSにWindows Serverの検証環境を爆速構築&SSMでセキュアに接続する

空っぽの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

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/setup-create-vpc.html#create-vpc-endpoints

https://dev.classmethod.jp/articles/vpcendpoint-ec2messages-not-required/

  • 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ファイルを読み込ませる方式を取ります。

スタックの作成 > 新しいリソースを使用(標準) を選択。
名称未設定2

テンプレートファイルをアップロードします。

名称未設定

「スタックの詳細を指定」で任意のスタック名とパラメータを選択します。
ここでキーペアとWindows Serverの言語とバージョンを選択できます。
パラメータに応じてSSM Parameter Store に保存されている値を取得する仕組みになっています。

今回は Windows Server 2019 英語版を選択しました。

WinSvr-RDP

オプションがいくつか指定できるのですが、特に指定しなくても構築可能です。
正常に作成できると、CloudFormation コンソールから状態を確認できます。

作成中は CREATE_IN_PROGRESS と表示されます。
右ペインには、それぞれのリソースの構築状況が表示されます。

CREATE_IN_PROGRESS

無事に作成できると CREATE_COMPLETE となります。
ここまで大体3〜4分程度かかりました。

CREATE_COMPLETE

接続してみる

無事に作成できたか確認します。

SSM Agent 経由

EC2コンソール > 「接続」 から接続できます。

EC2_SSM1

「セッションマネージャー」を選択して「接続」します。

EC2_SSM2

Powershellが立ち上がれば、成功です。
起動直後はコンソールがうまく表示されないことがあるので、その際はEnterキーを押下してください。

EC2_SSM3

フリートマネージャー経由 (RDP)

オンプレとの接続がなくても、フリートマネージャーを使えばRDP(リモートデスクトップ)接続も可能です。

FleetManager1

今回は、キーペアを選択して接続します。

FleetManager2

無事にRDP接続できました。

FleetManager3

EICエンドポイント経由

今回は触れませんが、EC2 Instance Connect Endpoint での接続も可能です。
別途 CloudShell または ローカル AWS CLI からコマンドを実行する必要があります。
以下の記事にわかりやすくまとまっていますので、参考になるかと思います。

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

付録: 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可能です。
https://dev.classmethod.jp/articles/rdp-connection-from-aws-systems-manager-fleet-manager-as-a-domain-user/

https://dev.classmethod.jp/articles/aws-at-home-systems-manager/

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.