AWS Systems Managerセッションマネージャーの「ポートフォワーディング」をHTTPプロキシ経由で利用する (Windows編)

AWS上に構築したEC2インスタンスのWindowsへリモートデスクトップ接続したいけど、インターネットへはHTTPプロキシ経由でしかアクセスできない・・・という方、接続できる方法があるんです!
2020.06.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。

今回は AWS Systems Manager (SSM) の「セッションマネージャー」機能について実験してみました。

同じような内容で「Linux編」と「Windows編」を執筆しています。
こちらのブログエントリは「Windows編」です。

Linux編は下記リンク先を参照してください。

Linux編とWindows編で内容が重複している箇所が多々ありますが、ご了承ください。

はじめに

セッションマネージャーには「プライベートサブネットのEC2に直接接続できる」「IAMでアクセス制御を行うためパスワードや秘密鍵の管理が不要」などのメリットがありますが、「マネジメントコンソールからWebブラウザだけでEC2へシェルログインして操作できる」という隠れた(?)利点もあります。

特に、インターネットへのアクセスがHTTPプロキシ経由となっている企業・学校などに所属されている方は、助かっている方も多いのではないでしょうか。

ところが、セッションマネージャーの便利な機能「ポートフォワーディング」を利用しようとすると、一転して話が変わります。
ポートフォワーディング機能はマネジメントコンソールから利用することができず、AWS CLIを使う必要があるためです。

しかし、AWS CLIも内部ではAWSのWeb APIを呼び出すことで処理を実行しており、プロキシ経由でのHTTP(S)の通信が行えればAWS CLIを利用できるはずです。

そこで、実際にプロキシ経由で利用できるか、試してみました。

SSMセッションマネージャーの「ポートフォワーディング」について

機能や手順については、こちらのブログが分かり易いかと思います。

SSMセッションマネージャーの「ポートフォワーディング」を利用する際、クライアントから接続先EC2インスタンスへの通信経路はこのようになります。

クライアントからSSMまでHTTPSで通信できればよい訳ですから、今回はHTTPプロキシ経由でこのように接続してみようと思います。

検証環境

「さあ、試してみよう」と思ったところ、困ったことに気付きました。

弊社ではHTTPプロキシではなく「SOCKSプロキシ」を利用しており、そもそも、現在は絶賛「テレワーク中」であるため、HTTPプロキシを使って実際に試すことができないのです。

そこで、AWS上に疑似的に「HTTPプロキシを設置しているオンプレミス環境」を構築して、試すことにしました。

図の左側が、SSMセッションマネージャーで接続する対象となるAWS環境です。

図の右側のように、別のAWSアカウントで疑似オンプレミス環境を構築します。
(別のAWSアカウントを用意するのが難しければ、同一AWSアカウントの別のVPCでも構いません)

パブリックサブネットにEC2インスタンスを配置してHTTPプロキシをインストールします。
プライベートサブネットには検証用クライアントとなるEC2インスタンスを配置し、プロキシを参照する設定を行います。

ポイントとしては、パブリックサブネットにNATゲートウェイ/NATインスタンスを設置しないことです。
そうすることで、HTTPプロキシ経由でなければインターネットへアクセスできないオンプレミス環境を再現することができます。

なお、検証用クライアントへのログインのために踏み台サーバー (Bastion) を利用します。
(ここもSSMセッションマネージャーを使うという方法もありますが、あちこちでSSMによる接続が登場すると分かり辛くなると思いますので、今回は踏み台サーバーを採用しました)

検証環境を構築する

AWS環境のプロビジョニング

接続対象のAWS環境、疑似オンプレミス環境を構築するCloudFormationテンプレートを用意しました。

それぞれ、環境を作成したいAWSアカウント・リージョンで実行してください。
(事前にキーペアを準備しておいてください。また、CloudFormationスタック作成時に自分のIPアドレスを聞かれますので、入力してください)


接続対象AWS環境作成CloudFormationテンプレート (クリックすると展開します)

cfn-ssmtest-win.yaml

---
AWSTemplateFormatVersion: "2010-09-09"
Description: "SSM test environment (Windows)"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "General Information"
        Parameters:
          - SystemName
      - Label:
          default: "Network Configuration"
        Parameters:
          - CidrBlockVPC
          - CidrBlockSubnetPublic
          - CidrBlockSubnetPrivate
      - Label:
          default: "EC2 Instance Configuration"
        Parameters:
          - EC2ImageID
          - EC2InstanceType
          - EC2KeyName
          - EC2VolumeType
          - EC2VolumeSize

Parameters:
  SystemName:
    Type: String
    Default: ssmtestwin

  CidrBlockVPC:
    Type: String
    Default: 192.168.0.0/16

  CidrBlockSubnetPublic:
    Type: String
    Default: 192.168.0.0/24

  CidrBlockSubnetPrivate:
    Type: String
    Default: 192.168.128.0/24

  EC2ImageID:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-windows-latest/Windows_Server-2019-Japanese-Full-Base

  EC2InstanceType:
    Type: String
    Default: t3.micro

  EC2KeyName:
    Type: AWS::EC2::KeyPair::KeyName

  EC2VolumeType:
    Type: String
    Default: gp2

  EC2VolumeSize:
    Type: String
    Default: 30

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CidrBlockVPC
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-vpc"
        - Key: System
          Value: !Ref SystemName

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-igw"
        - Key: System
          Value: !Ref SystemName

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  SubnetPublic:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Ref CidrBlockSubnetPublic
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-public-subnet"
        - Key: System
          Value: !Ref SystemName

  SubnetPrivate:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Ref CidrBlockSubnetPrivate
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-private-subnet"
        - Key: System
          Value: !Ref SystemName

  EIPNatGateway:
    DependsOn:
      - VPCGatewayAttachment
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  NatGateway:
    DependsOn:
      - EIPNatGateway
      - SubnetPublic
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt EIPNatGateway.AllocationId
      SubnetId: !Ref SubnetPublic
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-natgateway"
        - Key: System
          Value: !Ref SystemName

  RouteTablePublic:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-public-rtb"
        - Key: System
          Value: !Ref SystemName

  RouteTablePrivate:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-private-rtb"
        - Key: System
          Value: !Ref SystemName

  RouteIGW:
    DependsOn:
      - VPCGatewayAttachment
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTablePublic
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  RouteNatGateway:
    DependsOn:
      - VPCGatewayAttachment
      - NatGateway
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTablePrivate
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  RouteTableAssociationPublic:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPublic
      RouteTableId: !Ref RouteTablePublic

  RouteTableAssociationPrivate:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate
      RouteTableId: !Ref RouteTablePrivate

  SecurityGroupServer:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${SystemName}-server-sg"
      GroupDescription: "Security group for server"
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-server-sg"
        - Key: System
          Value: !Ref SystemName

  IAMRoleServer:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-server-role"
      AssumeRolePolicyDocument: |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": "ec2.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        }
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Path: /

  IAMInstanceProfileServer:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub "${SystemName}-server-role"
      Roles: 
        - !Ref IAMRoleServer
      Path: /

  EC2InstanceServer:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref EC2ImageID
      InstanceType: !Ref EC2InstanceType
      KeyName: !Ref EC2KeyName
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: !Ref EC2VolumeType
            VolumeSize: !Ref EC2VolumeSize
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref SubnetPrivate
          GroupSet:
            - !Ref SecurityGroupServer
      IamInstanceProfile: !Ref IAMInstanceProfileServer
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-server"
        - Key: System
          Value: !Ref SystemName

Outputs:
  VPC:
    Value: !Ref VPC
    Export:
      Name: !Sub "${AWS::StackName}::VPC"

  SubnetPublic:
    Value: !Ref SubnetPublic
    Export:
      Name: !Sub "${AWS::StackName}::SubnetPublic"

  SubnetPrivate:
    Value: !Ref SubnetPrivate
    Export:
      Name: !Sub "${AWS::StackName}::SubnetPrivate"

  SecurityGroupServer:
    Value: !Ref SecurityGroupServer
    Export:
      Name: !Sub "${AWS::StackName}::SecurityGroupServer"

  IAMRoleServer:
    Value: !Ref IAMRoleServer
    Export:
      Name: !Sub "${AWS::StackName}::IAMRoleServer"

  IAMInstanceProfileServer:
    Value: !Ref IAMInstanceProfileServer
    Export:
      Name: !Sub "${AWS::StackName}::IAMInstanceProfileServer"

  EC2InstanceServer:
    Value: !Ref EC2InstanceServer
    Export:
      Name: !Sub "${AWS::StackName}::EC2InstanceServer"
疑似オンプレミス環境作成CloudFormationテンプレート (クリックすると展開します)

cfn-onpremises-win.yaml

---
AWSTemplateFormatVersion: "2010-09-09"
Description: "SSM test environment (Windows) (virtual 'on-premises' environment)"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "General Information"
        Parameters:
          - SystemName
      - Label:
          default: "Network Configuration"
        Parameters:
          - CidrBlockVPC
          - CidrBlockSubnetPublic
          - CidrBlockSubnetPrivate
          - MyIP
      - Label:
          default: "EC2 Instance Configuration (Common)"
        Parameters:
          - EC2KeyName
      - Label:
          default: "EC2 Instance Configuration (Proxy)"
        Parameters:
          - EC2ProxyImageID
          - EC2ProxyInstanceType
          - EC2ProxyVolumeType
          - EC2ProxyVolumeSize
      - Label:
          default: "EC2 Instance Configuration (Bastion)"
        Parameters:
          - EC2BastionImageID
          - EC2BastionInstanceType
          - EC2BastionVolumeType
          - EC2BastionVolumeSize
      - Label:
          default: "EC2 Instance Configuration (Client)"
        Parameters:
          - EC2ClientImageID
          - EC2ClientInstanceType
          - EC2ClientVolumeType
          - EC2ClientVolumeSize
  
Parameters:
  SystemName:
    Type: String
    Default: onpremiseswin

  CidrBlockVPC:
    Type: String
    Default: 10.0.0.0/16

  CidrBlockSubnetPublic:
    Type: String
    Default: 10.0.0.0/24

  CidrBlockSubnetPrivate:
    Type: String
    Default: 10.0.128.0/24

  MyIP:
    Type: String

  EC2KeyName:
    Type: AWS::EC2::KeyPair::KeyName

  EC2ProxyImageID:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

  EC2ProxyInstanceType:
    Type: String
    Default: t3.micro

  EC2ProxyVolumeType:
    Type: String
    Default: gp2

  EC2ProxyVolumeSize:
    Type: String
    Default: 8

  EC2BastionImageID:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-windows-latest/Windows_Server-2019-Japanese-Full-Base

  EC2BastionInstanceType:
    Type: String
    Default: t3.micro

  EC2BastionVolumeType:
    Type: String
    Default: gp2

  EC2BastionVolumeSize:
    Type: String
    Default: 30

  EC2ClientImageID:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /aws/service/ami-windows-latest/Windows_Server-2019-Japanese-Full-Base

  EC2ClientInstanceType:
    Type: String
    Default: t3.micro

  EC2ClientVolumeType:
    Type: String
    Default: gp2

  EC2ClientVolumeSize:
    Type: String
    Default: 30

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CidrBlockVPC
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-vpc"
        - Key: System
          Value: !Ref SystemName

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-igw"
        - Key: System
          Value: !Ref SystemName

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  SubnetPublic:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Ref CidrBlockSubnetPublic
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-public-subnet"
        - Key: System
          Value: !Ref SystemName

  SubnetPrivate:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref AWS::Region
      CidrBlock: !Ref CidrBlockSubnetPrivate
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-private-subnet"
        - Key: System
          Value: !Ref SystemName

  RouteTablePublic:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-public-rtb"
        - Key: System
          Value: !Ref SystemName

  RouteTablePrivate:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-private-rtb"
        - Key: System
          Value: !Ref SystemName

  RouteIGW:
    DependsOn:
      - VPCGatewayAttachment
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTablePublic
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  RouteTableAssociationPublic:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPublic
      RouteTableId: !Ref RouteTablePublic

  RouteTableAssociationPrivate:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetPrivate
      RouteTableId: !Ref RouteTablePrivate

  SecurityGroupProxy:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${SystemName}-proxy-sg"
      GroupDescription: "Security group for Proxy"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: "SSH access from MyIP"
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: !Ref CidrBlockVPC
          Description: "Access from this VPC to Proxy"
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-proxy-sg"
        - Key: System
          Value: !Ref SystemName

  SecurityGroupBastion:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${SystemName}-bastion-sg"
      GroupDescription: "Security group for Bastion"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3389
          ToPort: 3389
          CidrIp: !Ref MyIP
          Description: "RDP access from MyIP"
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-bastion-sg"
        - Key: System
          Value: !Ref SystemName

  SecurityGroupClient:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${SystemName}-client-sg"
      GroupDescription: "Security group for Client"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3389
          ToPort: 3389
          SourceSecurityGroupId: !Ref SecurityGroupBastion
          Description: "RDP access from Bastion"
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-client-sg"
        - Key: System
          Value: !Ref SystemName

  EC2InstanceProxy:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref EC2ProxyImageID
      InstanceType: !Ref EC2ProxyInstanceType
      KeyName: !Ref EC2KeyName
      SourceDestCheck: false
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: !Ref EC2ProxyVolumeType
            VolumeSize: !Ref EC2ProxyVolumeSize
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref SubnetPublic
          GroupSet:
            - !Ref SecurityGroupProxy
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-proxy"
        - Key: System
          Value: !Ref SystemName

  EC2InstanceBastion:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref EC2BastionImageID
      InstanceType: !Ref EC2BastionInstanceType
      KeyName: !Ref EC2KeyName
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: !Ref EC2BastionVolumeType
            VolumeSize: !Ref EC2BastionVolumeSize
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref SubnetPublic
          GroupSet:
            - !Ref SecurityGroupBastion
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-bastion"
        - Key: System
          Value: !Ref SystemName

  EC2InstanceClient:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref EC2ClientImageID
      InstanceType: !Ref EC2ClientInstanceType
      KeyName: !Ref EC2KeyName
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: !Ref EC2ClientVolumeType
            VolumeSize: !Ref EC2ClientVolumeSize
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref SubnetPrivate
          GroupSet:
            - !Ref SecurityGroupClient
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-client"
        - Key: System
          Value: !Ref SystemName

Outputs:
  VPC:
    Value: !Ref VPC
    Export:
      Name: !Sub "${AWS::StackName}::VPC"

  SubnetPublic:
    Value: !Ref SubnetPublic
    Export:
      Name: !Sub "${AWS::StackName}::SubnetPublic"

  SubnetPrivate:
    Value: !Ref SubnetPrivate
    Export:
      Name: !Sub "${AWS::StackName}::SubnetPrivate"

  SecurityGroupProxy:
    Value: !Ref SecurityGroupProxy
    Export:
      Name: !Sub "${AWS::StackName}::SecurityGroupProxy"

  SecurityGroupBastion:
    Value: !Ref SecurityGroupBastion
    Export:
      Name: !Sub "${AWS::StackName}::SecurityGroupBastion"

  SecurityGroupClient:
    Value: !Ref SecurityGroupClient
    Export:
      Name: !Sub "${AWS::StackName}::SecurityGroupClient"

  EC2InstanceProxy:
    Value: !Ref EC2InstanceProxy
    Export:
      Name: !Sub "${AWS::StackName}::EC2InstanceProxy"

  EC2InstanceBastion:
    Value: !Ref EC2InstanceBastion
    Export:
      Name: !Sub "${AWS::StackName}::EC2InstanceBastion"

  EC2InstanceClient:
    Value: !Ref EC2InstanceClient
    Export:
      Name: !Sub "${AWS::StackName}::EC2InstanceClient"

SSMセッションマネージャーで接続できることの確認

AWS環境を作成しましたら、接続対象EC2インスタンスへSSMセッションマネージャーで接続できることを確認しましょう。

対象EC2インスタンスを右クリックして「接続」を選択し、「セッションマネージャー」を選択して「接続」をクリックします。

接続できることを確認できましたら、次へ進みましょう。

HTTPプロキシを構築する

今度は疑似オンプレミス環境の方に移って、まずはHTTPプロキシを構築します。
HTTPプロキシのソフトウェアとしてSquidを使用します。

EC2インスタンス「proxy」へSSHで接続します:

$ ssh -i ~/.ssh/<pemfile> ec2-user@<proxy_public_ip>

yumを使ってSquidをインストールします:

$ sudo yum install -y squid

Squidの設定ファイルを編集します:

$ sudo vi /etc/squid/squid.conf

プロキシの待ち受けポートを「8080」に変更します:

  :
# Squid normally listens to port 3128
http_port 3128 → 8080 へ変更する
  :

設定ファイルを保存して、Squidを起動します:

$ sudo systemctl enable squid.service
$ sudo systemctl start squid.service

Squidが正常に起動すれば準備はOKです。

クライアントでプロキシ参照の設定を行う

EC2インスタンス「client」へリモートデスクトップ接続で接続します。
踏み台サーバー「bastion」へリモートデスクトップ接続でいったん接続し、「bastion」上で更にリモートデスクトップ接続を起動して「client」へ接続します。

HTTPプロキシ参照の設定を行います。
設定方法は、AWSドキュメントの「AWS CLI」の下記ページの記述に従います。

HTTP プロキシを使用する - AWS Command Line Interface

コマンドプロンプトを起動して、以下のコマンドを実行します。

C:\> setx HTTP_PROXY http://<proxy_private_ip>:8080
C:\> setx HTTPS_PROXY http://<proxy_private_ip>:8080

なお、今回は疑似的なクライアントとしてEC2インスタンスを利用していますので、以下のコマンドも実行する必要があります。
(これは、EC2インスタンスメタデータへアクセスする際にプロキシを使用しないようにする設定です。物理のクライアントPCを利用している際は設定不要です)

C:\> setx NO_PROXY 169.254.169.254

プロキシ参照の設定を行いましたので、実際にプロキシを使ってインターネットへアクセスできることを確認しましょう。
AWSが提供しているアクセス元IPアドレス確認サイトcheckip.amazonaws.comを利用します。

PowerShellコンソールを起動して、以下のコマンドレットを実行します。

PS C:\> Invoke-WebRequest -Uri https://checkip.amazonaws.com/

ただし、PowerShellのバージョンが7.0未満の場合は、環境変数HTTP_PROXYHTTPS_PROXYの設定を残念ながら参照してくれないようです。
その場合は、以下のようにプロキシサーバーのアドレスを直接指定して実行します。

PS C:\> Invoke-WebRequest -Uri https://checkip.amazonaws.com/ -Proxy http://<proxy_private_ip>:8080

実行結果は以下のようになりました。

StatusCode        : 200
StatusDescription : OK
Content           : {49, 56, 46, 50...}
RawContent        : HTTP/1.1 200 OK
                    Connection: keep-alive
                    Content-Length: 15
                    Date: Wed, 24 Jun 2020 11:30:20 GMT
                    Server: lighttpd/1.4.53

                    XX.XX.XX.XX

Headers           : {[Connection, keep-alive], [Content-Length, 15], [Date, Wed, 24 Jun 2020 11:30:20 GMT], [Server, lighttpd/1.4.53]}
RawContentLength  : 15

ここまで上手くいっていれば、アクセス元のIPアドレスが表示されるはずです。

表示されているIPアドレスが「EC2インスタンス『proxy』のパブリックIPアドレス」であることを確認してください。

SSMセッションマネージャーを利用するためのIAMユーザーの作成

冒頭でチラッと書きましたように、SSMセッションマネージャーはIAMユーザーによるアクセス制御が可能です。

クライアントでSSMセッションマネージャーを使うためのIAMユーザーを作成しましょう。

まず、アクセス権限を定義するIAMポリシーを作成します。
マネジメントコンソールで「IAM」から「ポリシー」を選択して、「ポリシーの作成」をクリックします。

アクセス権限を編集する画面になりますので、「JSON」タブを選択します。
テキストボックスに以下の内容を貼り付けて、「ポリシーの確認」をクリックします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:StartSession"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:instance/*",
                "arn:aws:ssm:*:*:document/AWS-StartPortForwardingSession"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:TerminateSession"
            ],
            "Resource": [
                "arn:aws:ssm:*:*:session/${aws:username}-*"
            ]
        }
    ]
}

ポリシーの名前を入力して、「ポリシーの作成」をクリックします。
(名前は任意ですが、ここではssm-session-manager-policyとしました)

これでIAMポリシーが作成されました。

続けて、IAMユーザーを作成します。
マネジメントコンソールで「IAM」から「ユーザー」を選択して、「ユーザーを追加」をクリックします。

ユーザーの名前を入力します。
(名前は任意ですが、ここではremote-access-userとしました)

アクセスの種類のうち「プログラムによるアクセス」にのみチェックを入れます。

「次のステップ: アクセス権限」をクリックします。

「既存のポリシーを直接アタッチ」を選択します。

ポリシーの一覧から、さきほど作成したIAMポリシーを探して、ポリシー名の左側のチェックボックスにチェックを入れます。
(検索窓にポリシー名を入力すると探すのが楽です)

「次のステップ: タグ」をクリックします。

次の画面「タグの追加 (オプション)」は何も入力せずスキップします。

確認画面になりますので、内容を確認して「ユーザーの作成」をクリックします。

AWS CLIを使ってSSMセッションマネージャーを使うために必要となる「アクセスキーID」「シークレットアクセスキー」が表示されます。
この画面を閉じてしまうと「シークレットアクセスキー」は二度と確認することができなくなりますので、「.csvのダウンロード」をクリックして保存しましょう。
(保存したcsvファイルやメモしたシークレットアクセスキーは、安全な場所に保管してください)

これでIAMユーザーも作成されました。
次の手順へ進みます。

クライアントでAWS CLIのインストール・設定を行う

以下のページを参照して、AWS CLIをインストールします。

Windows への AWS CLI バージョン 2 のインストール - AWS Command Line Interface

インストーラーを下記リンク先からダウンロードします。
https://awscli.amazonaws.com/AWSCLIV2.msi

ダウンロードしたインストーラー (MSIファイル) をダブルクリックして実行します。
設定は全てデフォルト値で構いません。

インストールが終わりましたら、コマンドが実行できることを確認しましょう。

C:\> aws --version
aws-cli/2.0.25 Python/3.7.7 Windows/10 botocore/2.0.0dev29

AWS CLIをインストールしましたら、AWS CLIを実行するユーザーの認証情報設定を行います。

設定ファイルと認証情報ファイルの設定 - AWS Command Line Interface

aws configureコマンドを実行して、さきほどIAMユーザーを作成した際に取得した「アクセスキーID」「シークレットアクセスキー」を入力します。

C:\> aws configure
AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXX
AWS Secret Access Key [None]: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]:

これで認証情報が保存されました。

指定したIAMユーザーによってAWS CLIのコマンドが実行できることを確認します。

C:\> aws sts get-caller-identity
{
    "UserId": "XXXXXXXXXXXXXXXXXXXXX",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/remote-access-user"
}

このようにArn欄にIAMユーザー名remote-access-userが表示されていればOKです。
(エラーとなった場合は、ここまでの手順を確認してください)

※ 補足

「EC2インスタンスに対してIAMによるアクセス権限を与える場合は『IAMロール』と『インスタンスプロファイル』を使うのがベストプラクティスじゃないの?」と思われる方もいらっしゃると思います。

その通りなのですが、今回はあくまで「オンプレミス環境のクライアント」を疑似的に再現しているため、アクセスキーIDとシークレットアクセスキーによる認証情報の設定手順を採用しています。

Session Manager Pluginのインストール

これでAWS CLIを実行することができるようになりましたが、AWS CLIでSSMセッションマネージャーを使うためには「Session Manager Plugin」を追加でインストールする必要があります。

(オプション) AWS CLI 用の Session Manager Plugin をインストールする - AWS Systems Manager

インストーラーを下記リンク先からダウンロードします。
https://s3.amazonaws.com/session-manager-downloads/plugin/latest/windows/SessionManagerPluginSetup.exe

ダウンロードしたインストーラー (EXEファイル) をダブルクリックして実行します。
設定は全てデフォルト値で構いません。

インストールが終わりましたら、以下のコマンドを実行してインストール結果を確認しましょう。

C:\> session-manager-plugin

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.

接続を試してみる

SSMセッションマネージャーを使ってEC2インスタンスへ接続してみる

さて、全ての準備が整いましたので、いよいよセッションマネージャーを使ってEC2インスタンスへ接続してみたいと思います。

セッションを開始する - AWS Systems Manager

以下のようにコマンドを実行します。

C:\> aws ssm start-session --target <instance-id>

実際に接続してみると、以下のようになりました。
セッションIDを見るとIAMユーザーremote-access-userでセッションが開始されたことが分かります。
セッションが開始されると、マネジメントコンソールでSSMセッションマネージャーを利用する時と同様にプロンプトPS C:\Windows\system32>が表示されます。

C:\> aws ssm start-session --target i-0c8ab910de5d74cd8

Starting session with SessionId: remote-access-user-090615001a0b9ce6c
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

PS C:\Windows\system32>

接続先でコマンドを実行してみましょう。

PS C:\Windows\system32> Get-ComputerInfo

WindowsBuildLabEx                                       : 17763.1.amd64fre.rs5_release.180914-1434
WindowsCurrentVersion                                   : 6.3
WindowsEditionId                                        : ServerDatacenter
WindowsInstallationType                                 : Server
WindowsInstallDateFromRegistry                          : 2020/06/23 6:55:14
WindowsProductId                                        : XXXXX-XXXXX-XXXXX-XXXXX
WindowsProductName                                      : Windows Server 2019 Datacenter
WindowsRegisteredOrganization                           : Amazon.com
WindowsRegisteredOwner                                  : EC2
WindowsSystemRoot                                       : C:\Windows
WindowsVersion                                          : 1809
(以下省略)

PS C:\Windows\system32>

ちゃんと対象のEC2インスタンスへ接続できていることが確認できました。

確認が終わりましたら、exitコマンドで接続を終了してください。

SSMセッションマネージャーの「ポートフォワーディング」を使ってみる

最後に、今回の最終目標である「HTTPプロキシ経由でSSMセッションマネージャーの『ポートフォワーディング』を利用する」を試してみたいと思います。

セッションの開始 (ポート転送)

今回は、ポートフォワーディングを使って接続する対象として「リモートデスクトップ接続 (RDP)」を使いたいと思います。

以下のようにコマンドを実行します。

C:\> aws ssm start-session ^
       --target <instance-id> ^
       --document-name AWS-StartPortForwardingSession ^
       --parameters portNumber="3389",localPortNumber="13389"

実際に接続してみると、以下のようになります。
IAMユーザーremote-access-userでセッションが開始された後、セッション内でポート13389がオープンされたことが分かります。

C:\> aws ssm start-session ^
More? --target i-0c8ab910de5d74cd8 ^
More? --document-name AWS-StartPortForwardingSession ^
More? --parameters portNumber="3389",localPortNumber="13389"

Starting session with SessionId: remote-access-user-0e20f92a19c70c08d
Port 13389 opened for sessionId remote-access-user-0e20f92a19c70c08d.

ポートフォワーディングによる接続が確立しましたので、リモートデスクトップ接続で対象EC2インスタンスへ接続したいと思います。

「リモートデスクトップ接続」を起動して、接続先アドレスにlocalhost:13389を指定して接続します。

資格情報はAdministratorおよびパスワードを入力してください。
(Administratorのパスワードは、マネジメントコンソールから「Windowsパスワードの取得」で確認してください)

対象のEC2インスタンスへリモートデスクトップ接続できれば成功です!

いかがでしょうか?

各サーバーを行ったり来たりしながら設定を行うので分かり辛いところがあるかもしれませんが、ここまでの手順を1ステップずつ実施すれば、無事に接続できたのではないかと思います。

おわりに

HTTPプロキシ経由でしかインターネットへアクセスできない環境で、SSMセッションマネージャーの「ポートフォワーディング」機能が利用できることが確認できました。

今回は疑似的にオンプレミス環境をAWS上に構築して検証を行いましたが、実際のオンプレミス環境の場合でも今回の手順に沿って実施して頂ければ (一部読み替えが必要ですが) 利用できるのではないかと思います。

なお、今回はHTTPプロキシとしてSquidを利用しましたが、Squidの設定によっては、あるいは他のプロキシソフトウェアを利用している場合は、今回のように上手く接続できるとは限らない可能性があります。
その場合はご容赦ください。