AWS CloudFormation: 07. EC2インスタンスの構築とアクセス方法 (Session Manager / EC2 Instance Connect Endpoint)

# 本記事について

- AWS CloudFormation (yamlテンプレート) を用いて、色々なアーキテクチャを構築していきます。テンプレートのコピペ元としてご活用いただければ幸いです
  - [01. 仮想ネットワークの構築](https://qiita.com/iwatake2222/items/d19bd983391a292345af)
  - [02. アプリケーションサーバーの構築と踏み台サーバー経由でのアクセス方法](https://qiita.com/iwatake2222/items/45822e5ef9b56df42069)
  - [03. S3バケットの作成とポリシー・アクセス許可の設定](https://qiita.com/iwatake2222/items/d9c977e740ec1ee16b9c)
  - [04. S3 (+ CloudFront + OAC) による静的ウェブサイトのホスティング](https://qiita.com/iwatake2222/items/ac4392c11f83af2f320a)
  - [05. S3 + CloudFront + Cognito + Lambda@Edge による認証機能付き静的ウェブサイトのホスティング](https://qiita.com/iwatake2222/items/998d77951b7044e9bbbf)
  - [06. Lambda関数を作成して、S3/EventBridge/SQS から呼び出す](https://qiita.com/iwatake2222/items/e6eed5301e807e1a685d)
  - [07. EC2インスタンスの構築とアクセス方法 (Session Manager / EC2 Instance Connect Endpoint)]()
- [最新テンプレートはGitHubに配置しています](https://github.com/iwatake2222/aws_cloudformation_template)
- 以前の[02. アプリケーションサーバーの構築と踏み台サーバー経由でのアクセス方法](https://qiita.com/iwatake2222/items/45822e5ef9b56df42069) の記事では踏み台サーバー経由でPrivate SubnetにあるEC2 Instanceにアクセスする方法を説明しました。しかしこの方法だと、わざわざ踏み台サーバーを用意したりポート開放をする必要がありました。また、一般的にはキーペアの管理も必要となってしまいます（この記事ではEC2 Instance Connect (EIC) を使って、キーペア管理をしないで済むようにしています）
- また、多くの記事ではサーバーをPrivate Subnetに配置する構成が取られています。実際に会社で構築する環境等ではセキュリティを担保するためにはそうすべきだと思います。しかし、Private Subnetに配置されたEC2 Instanceをインターネットに接続するためには通常はNAT Gatewayが必要となります。NAT Gatewayは個人で使うには少々コストが高いという問題もあります
- 本記事では、EC2 Instanceへの接続を以下の2パターンで紹介します。どちらも外部に対してのポート開放は不要です
  - AWS System Manager (SSM) Session Manager を使う
  - EC2 Instance Connect (EIC) Endpoint を使う
- また、key pairのやり取りのために `ec2-instance-connect send-ssh-public-key` を使っています。これによってローカルPCの公開鍵をEC2 Instanceに一時的に登録します
  - Session ManagerもEIC Endpointも、あくまでもSSH Sessionを開始したりトンネルを作成するものになります。SSHのためにkey pairは結局必要となります （自分は最初なぜこれが必要なのか理解していなかったです）
- また、EC2 InstanceをPrivate Subnetだけなく、Public Subnetに配置するパターンも挙げて個人でもお金の面で使いやすくしています
  - サーバーをPublic Subnetに配置する際はセキュリティリスク等について各自の責任で十分にご確認をお願いいたします
  - Public Subnetに配置してもポートは閉じているし問題ないと思うかもしれません。が、ミドルウェアやOSのセキュリティホールを突いた攻撃を受けるリスクはあります
- おまけでリモートデスクトップ環境の構築方法も記載します

- 本記事で紹介するEC2インスタンスの構築とアクセス方法のパターン

|           | key pair 管理                            | 接続方法        | 配置場所 | 
| --------- | ---------------------------------------- | --------------- | -------- | 
| パターン1 | ec2-instance-connect send-ssh-public-key | Session Manager | Public   | 
| パターン2 | ec2-instance-connect send-ssh-public-key | Session Manager | Private  | 
| パターン3 | ec2-instance-connect send-ssh-public-key | EIC Endpoint    | Public   | 
| パターン4 | ec2-instance-connect send-ssh-public-key | EIC Endpoint    | Private  | 

# AWS System Manager (SSM) Session Manager を使ってEC2 Instance にアクセスする

## EC2 InstanceをPublic Subnetに配置する場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/1190b089-f125-9ba2-2e18-dfd9b67f1db8.png)

- Network
  - VPCとPublic Subnetを作成します
- EC2
  - Public Subnetに配置します
  - OSはUbuntu Server 22.04を使います。これにはssm-agentがデフォルトでインストールされています。ssm-agentがインストールされていないAMIを使う場合は自分でインストールする必要があります
  - Session Managerを使えるように `arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore` ポリシーを割り当てます
  - 常時起動するような使い方はしないため、Elastic IP アドレス (静的 IPv4 アドレス) は割り当てずにパブリックIPを自動割当しています
    - 設定は、 `AssociatePublicIpAddress: true`
    - パブリックIPv4アドレスが割り当てられると、0.005USD/hourの料金がかかります (24/2/1以降)
    - 一方、EIPを使うとインスタンスが起動しているときは無料ですが、インスタンスが起動していない場合は料金がかかります
    - インスタンスの使い方によってどちらにするか決めると良いかと思います
  - Security Groupはデフォルトのままで大丈夫です
    - ポート開放は不要です

- テンプレート
  - https://github.com/iwatake2222/aws_cloudformation_template/blob/master/10_ec2_ssm_eic/ec2_ssm_eic_public.yaml

<details>
<summary>ec2_ssm_eic_public.yaml</summary>

```yaml:ec2_ssm_eic_public.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: |
  Create an EC2 and connect using SSM and EC2 Instance Connect (EIC)

Parameters:
  SystemName:
    Description: System Name
    Type: String
  AvailabilityZone:
    Description: Availability Zone
    Type: AWS::EC2::AvailabilityZone::Name

Resources:
  #-----------------------------------------------------------------------------
  # VPC
  #-----------------------------------------------------------------------------
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-vpc

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

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

  #-----------------------------------------------------------------------------
  # Public Subnet
  #-----------------------------------------------------------------------------
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/20
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-public-subnet

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

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetToInternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  #-----------------------------------------------------------------------------
  # EC2
  #-----------------------------------------------------------------------------
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'Security Group for an EC2 server'
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ec2-sg

  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0595d6e81396a9efb   # Ubuntu Server 22.04
      InstanceType: t2.micro
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: gp3
            VolumeSize: 64
      IamInstanceProfile:
        !Ref EC2InstanceProfile
      NetworkInterfaces: 
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet:
            - !Ref EC2SecurityGroup
          SubnetId: !Ref PublicSubnet
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ec2

  #-----------------------------------------------------------------------------
  # IAM Role for EC2 server
  #-----------------------------------------------------------------------------
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SystemName}-ec2-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore   # To connect to an instance using Session Manager

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles: 
        - !Ref EC2Role
```

</details>

## EC2 InstanceをPrivate Subnetに配置する場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/74a45b94-1332-9aff-1b13-211bbd1a3ff5.png)

- Network
  - VPCとPublic Subnet、Private Subnetを作成します
- NAT Gateway
  - Public Subnetに配置します
- EC2
  - Private Subnetに配置します。Private SubnetのRouteTableではdefaultの宛先をNAT Gatewayに設定しています
  - OSはUbuntu Server 22.04を使います。これにはssm-agentがデフォルトでインストールされています。ssm-agentがインストールされていないAMIを使う場合は自分でインストールする必要があります
  - Session Managerを使えるように `arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore` ポリシーを割り当てます
  - Security Groupはデフォルトのままで大丈夫です
    - ポート開放は不要です
- ノート
  - 今回、EC2 InstanceはNAT Gateway経由でインターネットに接続されています。インターネット接続がないVPCの場合にはSession Manager接続用にVPC Endpointが必要となります

- テンプレート
  - https://github.com/iwatake2222/aws_cloudformation_template/blob/master/10_ec2_ssm_eic/ec2_ssm_eic_private.yaml


<details>
<summary>ec2_ssm_eic_private.yaml</summary>

```yaml:ec2_ssm_eic_private.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: |
  Create an EC2 and connect using SSM and EC2 Instance Connect (EIC)

Parameters:
  SystemName:
    Description: System Name
    Type: String
  AvailabilityZone:
    Description: Availability Zone
    Type: AWS::EC2::AvailabilityZone::Name

Resources:
  #-----------------------------------------------------------------------------
  # VPC
  #-----------------------------------------------------------------------------
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-vpc

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

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

  #-----------------------------------------------------------------------------
  # Public Subnet
  #-----------------------------------------------------------------------------
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/20
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-public-subnet

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

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetToInternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  #-----------------------------------------------------------------------------
  # Private Subnet
  #-----------------------------------------------------------------------------
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref VPC
      CidrBlock: 10.0.16.0/20
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-private-subnet

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

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetToInternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  #-----------------------------------------------------------------------------
  # Nat Gateway
  #-----------------------------------------------------------------------------
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ngw-eip

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId: !Ref PublicSubnet
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ngw

  #-----------------------------------------------------------------------------
  # EC2
  #-----------------------------------------------------------------------------
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'Security Group for an EC2 server'
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ec2-sg

  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0595d6e81396a9efb   # Ubuntu Server 22.04
      InstanceType: t2.micro
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: gp3
            VolumeSize: 64
      IamInstanceProfile:
        !Ref EC2InstanceProfile
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      SubnetId: !Ref PrivateSubnet
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ec2

  #-----------------------------------------------------------------------------
  # IAM Role for EC2 server
  #-----------------------------------------------------------------------------
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SystemName}-ec2-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore   # To connect to an instance using Session Manager

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles: 
        - !Ref EC2Role
```

</details>

## デプロイ方法

EC2 Instanceをどちらに配置したいかに応じて、以下コマンド内で使うテンプレートファイルを切り替えてください

```sh
Region=ap-northeast-1
AvailabilityZone=ap-northeast-1a

SystemName=sample-ec2-ssm-eic-public
TemplateFileName=./ec2_ssm_eic_public.yaml

# SystemName=sample-ec2-ssm-eic-private
# TemplateFileName=./ec2_ssm_eic_private.yaml

aws cloudformation deploy \
--region "${Region}" \
--stack-name "${SystemName}" \
--template-file ${TemplateFileName} \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
SystemName="${SystemName}" \
AvailabilityZone="${AvailabilityZone}"
```

## EC2へのアクセス方法

EC2 Instanceの配置場所に関わらずアクセス方法は同じです

### 準備

- ローカルPC (Windows/Linux/Macなど) で、以下を用意します
  - sshコマンド
  - AWS CLI
  - AWS CLI 用の Session Manager プラグイン
    - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
- AWSコンソール -> EC2 -> Isntanceから、作成したインスタンス名 (i-00000000000000000) を確認しておきます
  - 以後、記事内の i-00000000000000000 は適宜置き換えをしてください

### 簡単な接続確認

- 下記コマンドでインスタンスにアクセスできることを確認します

```sh
aws ssm start-session --target i-00000000000000000
```

### sshの設定

- `~/.ssh/config` を開き、以下を追記します
  - Windowsの場合
    - 場所は、 `C:\Users\ooo\.ssh`
    - `sh -c` を `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe` に置き換えます
    - 改行区切りを && から ;(セミコロン) に変えます。Linuxでのエラー処理を気にしなければどちらもセミコロンで大丈夫です

- 設定の説明
  - `i-00000000000000000` というホスト名にsshアクセスしようとしたら記載したコマンドが動きます
  - まず、ローカルPCの公開鍵をEC2 Instanceに一時的に登録します
  - その後、SSH sessionを開始しています
- ssh接続のたびに毎回インスタンス名を入力するのが面倒な場合は、configにあらかじめ記載して、普段は `ec2-server` という名前のホストを使うと便利です (名前は任意)

```txt:Linuxのconfig
Host i-* mi-*
    ProxyCommand sh -c "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' && aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"

# (Optional) To specify host id
Host ec2-server
    HostName i-00000000000000000
    User ubuntu
    ProxyCommand sh -c "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' && aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
```

```txt:Windowsのconfig
Host i-* mi-*
    ProxyCommand C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' ; aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"

# (Optional) To specify host id
Host ec2-server
    HostName i-0b723aba7cd8e8183
    User ubuntu
    ProxyCommand C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' ; aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"

```

### sshで接続する

- 以下のいずれかのコマンドで接続できるはずです
- また、~/.ssh/config に設定があるため、VSCodeからもRemote Explorerで接続できるはずです

```sh
ssh ubuntu@i-00000000000000000

ssh ec2-server
```

- もしも以下のようなエラーが発生したら、known_hosts ファイルを削除するか、sshに `-o 'StrictHostKeyChecking no'` オプションをつけてください
  - `kex_exchange_identification: Connection closed by remote host`

# EC2 Instance Connect Endpoint (EIC Endpoint) を使ってEC2 Instance にアクセスする

## EC2 InstanceをPublic Subnetに配置する場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/7c508c86-3bd3-e870-6d78-4f33d109c427.png)

- Network
  - VPCとPublic Subnetを作成します
- EIC Endpoint
  - Public Subnetに配置します
  - Securiy Groupはデフォルトのままとしました
- EC2
  - Public Subnetに配置します
  - OSはUbuntu Server 22.04を使います
  - 常時起動するような使い方はしないため、Elastic IP アドレス (静的 IPv4 アドレス) は割り当てずにパブリックIPを自動割当しています
    - 設定は、 `AssociatePublicIpAddress: true`
    - パブリックIPv4アドレスが割り当てられると、0.005USD/hourの料金がかかります (24/2/1以降)
    - 一方、EIPを使うとインスタンスが起動しているときは無料ですが、インスタンスが起動していない場合は料金がかかります
    - インスタンスの使い方によってどちらにするか決めると良いかと思います
  - Security Group
    - EIC Endpointからのsshのみ受け付けるように設定します

- テンプレート
  - https://github.com/iwatake2222/aws_cloudformation_template/blob/master/11_ec2_eic/ec2_eic_public.yaml

<details>
<summary>ec2_eic_public.yaml</summary>

```yaml:ec2_eic_public.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: |
  Create an EC2 and connect using SSM and EC2 Instance Connect (EIC) Endpoint

Parameters:
  SystemName:
    Description: System Name
    Type: String
  AvailabilityZone:
    Description: Availability Zone
    Type: AWS::EC2::AvailabilityZone::Name

Resources:
  #-----------------------------------------------------------------------------
  # VPC
  #-----------------------------------------------------------------------------
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-vpc

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

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

  #-----------------------------------------------------------------------------
  # Public Subnet
  #-----------------------------------------------------------------------------
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/20
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-public-subnet

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

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetToInternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  #-----------------------------------------------------------------------------
  # EC2 Instance Connect Endpoint
  #-----------------------------------------------------------------------------
  EICSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'Security Group for an EC2 Instance Connect Endpoint'
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-eic-sg

  EICEndpoint:
    Type: AWS::EC2::InstanceConnectEndpoint
    Properties: 
      SecurityGroupIds: 
        - !Ref EICSecurityGroup
      SubnetId: !Ref PublicSubnet

  #-----------------------------------------------------------------------------
  # EC2
  #-----------------------------------------------------------------------------
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'Security Group for an EC2 server'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          SourceSecurityGroupId: !Ref EICSecurityGroup
        # - IpProtocol: tcp
        #   FromPort: 3389
        #   ToPort: 3389
        #   SourceSecurityGroupId: !Ref EICSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ec2-sg

  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0595d6e81396a9efb   # Ubuntu Server 22.04
      InstanceType: t2.micro
      # InstanceType: m7i.xlarge
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: gp3
            VolumeSize: 64
      IamInstanceProfile:
        !Ref EC2InstanceProfile
      NetworkInterfaces: 
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet:
            - !Ref EC2SecurityGroup
          SubnetId: !Ref PublicSubnet
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ec2

  #-----------------------------------------------------------------------------
  # IAM Role for EC2 server
  #-----------------------------------------------------------------------------
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SystemName}-ec2-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles: 
        - !Ref EC2Role
```

</details>

## EC2 InstanceをPrivate Subnetに配置する場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/7dc63395-5587-f1b8-c02c-b5e8c61afbc5.png)

- Network
  - VPCとPublic Subnet、Private Subnetを作成します
- NAT Gateway
  - Public Subnetに配置します
- EIC Endpoint
  - Public Subnetに配置します
  - Securiy Groupはデフォルトのままとしました
- EC2
  - Private Subnetに配置します。Private SubnetのRouteTableではdefaultの宛先をNAT Gatewayに設定しています
  - OSはUbuntu Server 22.04を使います
  - Security Group
    - EIC Endpointからのsshのみ受け付けるように設定します

- テンプレート
  - https://github.com/iwatake2222/aws_cloudformation_template/blob/master/11_ec2_eic/ec2_eic_private.yaml


<details>
<summary>ec2_eic_private.yaml</summary>

```yaml:ec2_eic_private.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: |
  Create an EC2 and connect using SSM and EC2 Instance Connect (EIC) Endpoint

Parameters:
  SystemName:
    Description: System Name
    Type: String
  AvailabilityZone:
    Description: Availability Zone
    Type: AWS::EC2::AvailabilityZone::Name

Resources:
  #-----------------------------------------------------------------------------
  # VPC
  #-----------------------------------------------------------------------------
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-vpc

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

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

  #-----------------------------------------------------------------------------
  # Public Subnet
  #-----------------------------------------------------------------------------
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/20
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-public-subnet

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

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetToInternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  #-----------------------------------------------------------------------------
  # Private Subnet
  #-----------------------------------------------------------------------------
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref VPC
      CidrBlock: 10.0.16.0/20
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-private-subnet

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

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetToInternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  #-----------------------------------------------------------------------------
  # Nat Gateway
  #-----------------------------------------------------------------------------
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ngw-eip

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId: !Ref PublicSubnet
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ngw

  #-----------------------------------------------------------------------------
  # EC2 Instance Connect Endpoint
  #-----------------------------------------------------------------------------
  EICSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'Security Group for an EC2 Instance Connect Endpoint'
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-eic-sg

  EICEndpoint:
    Type: AWS::EC2::InstanceConnectEndpoint
    Properties: 
      SecurityGroupIds: 
        - !Ref EICSecurityGroup
      SubnetId: !Ref PublicSubnet

  #-----------------------------------------------------------------------------
  # EC2
  #-----------------------------------------------------------------------------
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'Security Group for an EC2 server'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          SourceSecurityGroupId: !Ref EICSecurityGroup
        # - IpProtocol: tcp
        #   FromPort: 3389
        #   ToPort: 3389
        #   SourceSecurityGroupId: !Ref EICSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ec2-sg

  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0595d6e81396a9efb   # Ubuntu Server 22.04
      # ImageId: ami-09b74ffed30c40197   # Deep Learning Base OSS Nvidia Driver GPU AMI (Ubuntu 22.04) 20240610
      InstanceType: t2.micro
      # InstanceType: m7i.xlarge
      # InstanceType: g5.4xlarge
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeType: gp3
            VolumeSize: 64
      IamInstanceProfile:
        !Ref EC2InstanceProfile
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      SubnetId: !Ref PrivateSubnet
      Tags:
        - Key: Name
          Value: !Sub ${SystemName}-ec2

  #-----------------------------------------------------------------------------
  # IAM Role for EC2 server
  #-----------------------------------------------------------------------------
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SystemName}-ec2-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles: 
        - !Ref EC2Role
```

</details>

## デプロイ方法

EC2 Instanceをどちらに配置したいかに応じて、以下コマンド内で使うテンプレートファイルを切り替えてください

```sh
Region=ap-northeast-1
AvailabilityZone=ap-northeast-1a

SystemName=sample-ec2-eic-public
TemplateFileName=./ec2_eic_public.yaml

# SystemName=sample-ec2-eic-private
# TemplateFileName=./ec2_eic_private.yaml

aws cloudformation deploy \
--region "${Region}" \
--stack-name "${SystemName}" \
--template-file ${TemplateFileName} \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
SystemName="${SystemName}" \
AvailabilityZone="${AvailabilityZone}"
```

## EC2へのアクセス方法

EC2 Instanceの配置場所に関わらずアクセス方法は同じです

### 準備

- ローカルPC (Windows/Linux/Macなど) で、以下を用意します
  - sshコマンド
  - AWS CLI
- AWSコンソール -> EC2 -> Isntanceから、作成したインスタンス名 (i-00000000000000000) を確認しておきます
  - 以後、記事内の i-00000000000000000 は適宜置き換えをしてください

### 簡単な接続確認

- 下記コマンドでインスタンスにアクセスできることを確認します

```sh
aws ec2-instance-connect ssh --instance-id i-00000000000000000 --os-user ubuntu --connection-type eice
```

### sshの設定

- `~/.ssh/config` を開き、以下を追記します
  - Windowsの場合
    - 場所は、 `C:\Users\ooo\.ssh`
    - `sh -c` を `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe` に置き換えます
    - 改行区切りを && から ;(セミコロン) に変えます。Linuxでのエラー処理を気にしなければどちらもセミコロンで大丈夫です
- 設定の説明
  - `i-00000000000000000` というホスト名にsshアクセスしようとしたら記載したコマンドが動きます
  - まず、ローカルPCの公開鍵をEC2 Instanceに一時的に登録します
  - その後、open-tunnelコマンドによってインスタンスへのプライベートトンネルを作成します
- ssh接続のたびに毎回インスタンス名を入力するのが面倒な場合は、configにあらかじめ記載して、普段は `ec2-server` という名前のホストを使うと便利です
- 参考としてトンネル作成だけを行うコマンドも記載しています。 `ec2-server-tunnel`
- 後述するリモートデスクトップ接続のための設定もここで記載します。 `ec2-server-tunnel-rdp`

```txt:Linuxのconfig
Host i-* mi-*
    ProxyCommand sh -c "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' && aws ec2-instance-connect open-tunnel --instance-id %h"

# (Optional) To specify host id
Host ec2-server
    HostName i-00000000000000000
    User ubuntu
    ProxyCommand sh -c "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' && aws ec2-instance-connect open-tunnel --instance-id %h"

# (Optional) Sample to create tunnel
Host ec2-server-tunnel
    HostName i-00000000000000000
    User ubuntu
    ProxyCommand sh -c "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' && aws ec2-instance-connect open-tunnel --instance-id %h --remote-port 22 --local-port 2222"

# (Optional) Sample to create tunnel for rdp
Host ec2-server-tunnel-rdp
    HostName i-00000000000000000
    User ubuntu
    ProxyCommand sh -c "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' && aws ec2-instance-connect open-tunnel --instance-id %h --remote-port 3389 --local-port 3389"
```

```txt:Windowsのconfig
Host i-* mi-*
    ProxyCommand C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' ; aws ec2-instance-connect open-tunnel --instance-id %h"

# (Optional) To specify host id
Host ec2-server
    HostName i-00000000000000000
    User ubuntu
    ProxyCommand C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' ; aws ec2-instance-connect open-tunnel --instance-id %h"

# (Optional) Sample to create tunnel
Host ec2-server-tunnel
    HostName i-00000000000000000
    User ubuntu
    ProxyCommand C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' ; aws ec2-instance-connect open-tunnel --instance-id %h --remote-port 22 --local-port 2222"

# (Optional) Sample to create tunnel for rdp
Host ec2-server-tunnel-rdp
    HostName i-00000000000000000
    User ubuntu
    ProxyCommand C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe "aws ec2-instance-connect send-ssh-public-key --instance-id %h --instance-os-user %r --ssh-public-key 'file://~/.ssh/id_rsa.pub' ; aws ec2-instance-connect open-tunnel --instance-id %h --remote-port 3389 --local-port 3389"
```

### sshで接続する

- 以下のいずれかのコマンドで接続できるはずです
- また、~/.ssh/config に設定があるため、VSCodeからもRemote Explorerで接続できるはずです

```sh
ssh ubuntu@i-00000000000000000
ssh ec2-server
```

- トンネル作成とssh接続をあえて分けると以下のようになります

```
ssh ec2-server-tunnel
ssh ubuntu@localhost -p 2222
```


# リモートデスクトップ (xrdp) 環境を構築する

- 最後に構築したパターン4 (以下の環境) に対して、リモートデスクトップ (xrdp) 追加します
  - key pairはec2-instance-connect send-ssh-public-key で管理
  - 接続にはEC2 Instance Connect Endpoint (EIC Endpoint) を使用
  - EC2 InstanceはPrivate Subnetに配置
 - ここまで使用したテンプレートだと、インスタンスタイプは `t2.micro` とデスクトップ環境には貧弱なものとなっています。適当に `m7i.xlarge` あたりに変更しておいてください

## xrdp用のポートを開放する

- EIC Endpoint -> EC2 Instance に対してRDP用のポート (3389) を開放します
  - 外部に対するポート開放ではないです。送信元はEIC Endpointに限定します
  - 以下のいずれかの方法で開放します
- なお、EIC Endpointにおいて使用可能なポートはSSH用の22とRDP用の3389のみになります。もしも他のポートを使おうとしたら以下のようなエラーが発生します
  - `awscli.customizations.ec2instanceconnect.websocket - ERROR - {"ErrorCode":"InvalidParameter","Message":"The specified RemotePort is not valid. Specify either 22 or 3389 as the RemotePort and retry your request."}`
- 手段1: テンプレートにInboundルールを追加する
  - テンプレート内の `EC2SecurityGroup` に以下を追加します
    ```yaml
    SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 3389
        ToPort: 3389
        SourceSecurityGroupId: !Ref EICSecurityGroup
    ```
- 手段2: AWSコンソール上で手動でInboundルールを追加する
  - Type: RDP (3389)
  - Source: Custom (EICSecurityGroup)

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/57527312-e921-5fd2-572c-078813fa56c2.png)

## EC2の設定を行う

- EC2 Instance内にユーザーを追加します
  - ここでは `user0` というユーザーを作りsudo権限を付与しています
  - デフォルトの `ubuntu` ユーザーをそのまま使用する場合はパスワード設定だけ行います
- デスクトップ環境とxrdpをインストールします
- 上記を行うために、EC2にSSHでログイン後下記コマンドを実行します
  - デスクトップ環境はお好みで

```sh
# sudo passwd ubuntu
sudo adduser user0
sudo usermod -aG sudo user0

sudo apt update
sudo apt install -y ubuntu-desktop xrdp
sudo systemctl enable xrdp
sudo reboot
```


## リモートデスクトップ接続する

- まず、ローカルPCから下記コマンドでRDP用のトンネルを作成します。 (設定は前述のconfig参照)

```sh
ssh ec2-server-tunnel-rdp
```

- 以下のホストに対してリモートデスクトップ接続します
  - `localhost:3389`
  - Windowsだと標準の「リモートデスクトップ接続」が使えます

![xrdp_host_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/7c8a1550-7324-ec53-220f-9637148f06ea.png)

![xrdp_host_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/489c15ca-a62b-8376-d5bc-1dabec45ef35.png)

![xrdp_host_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/cc49794b-5b4e-ebb4-701b-cc7ef094bdd2.png)

## (おまけ) VNCを使いたい場合

- リモートデスクトップだと、キーボード入力など局所的な画面変化への応答性は高いのですが、画面全体が変わるようなシーンで遅延が大きいように見えます
- 負荷は高くなりますが、VNCを使いたい場合もあるかと思います
- 一応以下で出来ました
  - 適当にtigervncあたりをインストールする
  - 3389ポートを使うように設定する