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

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

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

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

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

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

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.yaml

---
AWSTemplateFormatVersion: "2010-09-09"
Description: "SSM test environment"

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: ssmtest

  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-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

  EC2InstanceType:
    Type: String
    Default: t3.micro

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

  EC2VolumeType:
    Type: String
    Default: gp2

  EC2VolumeSize:
    Type: String
    Default: 8

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/xvda
          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.yaml

---
AWSTemplateFormatVersion: "2010-09-09"
Description: "SSM test environment (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: onpremises

  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-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

  EC2BastionInstanceType:
    Type: String
    Default: t3.micro

  EC2BastionVolumeType:
    Type: String
    Default: gp2

  EC2BastionVolumeSize:
    Type: String
    Default: 8

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

  EC2ClientInstanceType:
    Type: String
    Default: t3.micro

  EC2ClientVolumeType:
    Type: String
    Default: gp2

  EC2ClientVolumeSize:
    Type: String
    Default: 8

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: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: "SSH 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: 22
          ToPort: 22
          SourceSecurityGroupId: !Ref SecurityGroupBastion
          Description: "SSH 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/xvda
          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/xvda
          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」へSSHで接続します。
踏み台サーバー「bastion」を経由して接続するには、以下のようにします。

$ ssh -i ~/.ssh/<pemfile> -o ProxyCommand='ssh -i ~/.ssh/<pemfile> -W %h:%p ec2-user@<bastion_public_ip>' ec2-user@<client_public_ip>

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

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

~/.bashrcファイルに以下の行を追記します。

export HTTP_PROXY=http://<proxy_private_ip>:8080
export HTTPS_PROXY=http://<proxy_private_ip>:8080

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

export NO_PROXY=169.254.169.254

~/.bashrcファイルを編集しましたら、一度ログアウトして再度ログインするか、以下のコマンドを実行して設定を反映させます。

$ source ~/.bashrc

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

$ curl https://checkip.amazonaws.com/
XX.XX.XX.XX

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

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

※ 補足

上記のように~/.bashrcファイルへ記述することで、AWS CLIおよび他のLinuxコマンドがHTTPプロキシを利用するようになるはずです。

しかし、場合によっては設定が反映されない場合もあるようですので、その際は各Linuxコマンドの設定ファイルにプロキシ参照の設定を行ってください。

今回の手順ではcurlコマンドやyumコマンドを利用していますので、~/.curlファイルや/etc/yum.confファイルに以下の行を記述するとプロキシを利用するようになると思います。

proxy=http://<proxy_private_ip>:8080

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のインストール・設定を行う

今回はAmazon Linux 2を利用していますので、最初からAWS CLIがインストールされています。
ただし、バージョンが古いため、バージョンアップするか削除後に新規にインストールした方がよいでしょう。

AWS CLI のインストール - AWS Command Line Interface

最初からインストールされているAWS CLIのアンインストール:

$ sudo yum erase -y awscli

AWS CLIインストーラーのダウンロードとインストール:

$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install

インストールされたことの確認:

$ aws --version
aws-cli/2.0.25 Python/3.7.3 Linux/4.14.177-139.254.amzn2.x86_64 botocore/2.0.0dev29

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

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

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

$ 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のコマンドが実行できることを確認します。

$ 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

Linuxの場合は、以下の手順でインストールできます。

$ curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin.rpm" -o "session-manager-plugin.rpm"
$ sudo yum install -y session-manager-plugin.rpm

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

$ session-manager-plugin

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

接続を試してみる

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

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

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

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

$ aws ssm start-session --target <instance-id>

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

$ aws ssm start-session --target i-05003b21db12d64e1

Starting session with SessionId: remote-access-user-0b1738dc9c8e4510b
sh-4.2$

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

sh-4.2$ uname -a
Linux ip-192-168-128-199.ap-northeast-1.compute.internal 4.14.177-139.254.amzn2.x86_64 #1 SMP Thu May 7 18:48:23 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
sh-4.2$

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

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

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

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

今回は、ポートフォワーディングを使って接続する対象として「postgresql」を使いたいと思います。

対象EC2インスタンスへのpostgresqlのインストールと設定

対象EC2インスタンスへログインします。
(マネジメントコンソールからSSMセッションマネージャーで接続しても、さきほど設定したばかりのAWS CLIからのSSMセッションマネージャー接続でも、どちらでも構いません)

yumを使ってpostgresql (サーバーの方) をインストールします:

$ sudo yum install -y postgresql-server

デフォルトユーザーpostgresのパスワードを設定します:

$ sudo passwd postgres

ユーザーpostgresの権限で、データベースを初期化します:

$ su - postgres
Password:
$ initdb
$ exit

postgresqlを起動します:

$ sudo systemctl enable postgresql.service
$ sudo systemctl start postgresql.service

postgresqlサーバーへ接続できることを確認します:

$ psql -l -U postgres
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(3 rows)

クライアントへのpostgresqlクライアントツールのインストール

クライアントからpostgresqlサーバーへ接続するために、postgresqlクライアントツールをインストールします。

$ sudo yum install -y postgresql

「ポートフォワーディング」でpostgresqlのポートへ接続する

お待たせしました!!
それでは、セッションマネージャーの「ポートフォワーディング」機能を使ってEC2インスタンスへ接続してみたいと思います。

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

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

$ aws ssm start-session \
    --target <instance-id> \
    --document-name AWS-StartPortForwardingSession \
    --parameters '{"portNumber":["5432"], "localPortNumber":["15432"]}'

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

$ aws ssm start-session \
> --target i-05003b21db12d64e1 \
> --document-name AWS-StartPortForwardingSession \
> --parameters '{"portNumber":["5432"], "localPortNumber":["15432"]}'

Starting session with SessionId: remote-access-user-0eb75e03d274256e9
Port 15432 opened for sessionId remote-access-user-0eb75e03d274256e9.

ポートフォワーディングによる接続が確立しましたので、postgresqlサーバーへ接続したいと思います。

別のターミナルでクライアントへ接続して、以下のコマンドを実行します。

$ psql -l -U postgres -h localhost -p 15432

以下のように表示されれば成功です!

$ psql -l -U postgres -h localhost -p 15432
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(3 rows)

いかがでしょうか?

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

おわりに

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

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

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