【Amazon Linux 2023】プライベートサブネット内EC2にNAT Gateway無しでパッケージをインストールする方法

# はじめに
セキュリティ向上のため、EC2インスタンスをプライベートサブネットに配置することがあります。
この場合、インターネット通信が制限されるため、通常の手段では外部リソースに接続してパッケージをインストールしたり更新したりできません。


一般的には、パッケージの更新やインターネット通信が必要な際、パブリックサブネットにNAT Gatewayを配置し、EC2インスタンスからの通信を通過させる方法が考えられます。
しかし、Amazon LinuxのパッケージはS3にホストされているため、NAT Gatewayを設置しなくても、S3のゲートウェイ型VPCエンドポイントを活用すれば、インターネット接続なしでパッケージのインストール・更新が可能です。

# 実際に構築してみた
以下に、NAT Gatewayを使用せずに、プライベートサブネット内でパッケージ更新を可能にするCloudFormationテンプレート全体を示します。
このテンプレートでは以下のリソースを構築します。
　VPC
　プライベートサブネット
　プライベートサブネット用ルートテーブル
　EC2・SSM用セキュリティグループ
　SSM接続用VPCエンドポイント
　S3ゲートウェイ型VPCエンドポイント
　EC2（Amazon Linux 2023）
　EC2用IAMロール
  

```template.yml
AWSTemplateFormatVersion: 2010-09-09

Resources:
  # VPC作成
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true

  # プライベートサブネット作成
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.1.0/24
      
  # プライベートサブネット用ルートテーブル作成
  PrivateSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC

  # プライベートサブネットにルートテーブルをアタッチ
  PrivateSubnetRoute:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateSubnetRouteTable

  # SSM用セキュリティグループの作成
  MySSMSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "for SSM VPC Endpoint"
      VpcId: !Ref MyVPC
      
  # EC2用セキュリティグループの作成
  MyEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Allow outbound access to S3"
      VpcId: !Ref MyVPC
      
  # SSM用セキュリティグループのインバウンドルールに、EC2からの疎通を許可するルールを追加
  MySSMSecurityGroupIngress: 
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref MySSMSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      SourceSecurityGroupId: !GetAtt MyEC2SecurityGroup.GroupId

  # EC2用セキュリティグループのアウトバウンドルールに、SSM用VPCエンドポイントへの疎通を許可するルールを追加
  MyEC2SecurityGroupEgressAllowToSSM: 
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId: !Ref MyEC2SecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      DestinationSecurityGroupId: !GetAtt MySSMSecurityGroup.GroupId

  # EC2用セキュリティグループのアウトバウンドルールに、S3への疎通を許可するルールを追加
  MyEC2SecurityGroupEgressAllowToS3: 
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId: !Ref MyEC2SecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      DestinationPrefixListId: pl-61a54008

  # SSM用VPCエンドポイントの作成
  VPCEndpointSSM:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcId: !Ref MyVPC
      SubnetIds: [!Ref PrivateSubnet]
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SecurityGroupIds: [!Ref MySSMSecurityGroup]

  # EC2メッセージング用VPCエンドポイントの作成
  VPCEndpointEC2Messages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
      VpcId: !Ref MyVPC
      SubnetIds: [!Ref PrivateSubnet]
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SecurityGroupIds: [!Ref MySSMSecurityGroup]

  # SSMメッセージング用VPCエンドポイントの作成
  VPCEndpointSSMMessage:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcId: !Ref MyVPC
      SubnetIds: [!Ref PrivateSubnet]
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SecurityGroupIds: [!Ref MySSMSecurityGroup]

  # S3用VPCエンドポイントの作成
  S3VPCEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref MyVPC
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      RouteTableIds:
        - !Ref PrivateSubnetRouteTable   # 対象サブネットのルートテーブルを紐づける

  # EC2作成
  MyEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      SubnetId: !Ref PrivateSubnet
      BlockDeviceMappings:
        - DeviceName: /dev/xvda  
          Ebs:
            VolumeType: gp3
            VolumeSize: 30  
            Encrypted: true  # 暗号化を有効にする
      SecurityGroupIds:
        - !Ref MyEC2SecurityGroup
      ImageId: resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64  # Amazon Linux 2023のAMI IDを指定
      IamInstanceProfile: !Ref MyInstanceProfile
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          sudo yum -y install http://s3-ap-northeast-1.amazonaws.com/amazon-ssm-ap-northeast-1/latest/linux_amd64/amazon-ssm-agent.rpm

  # インスタンスプロファイル作成
  MyInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref SSMRole

  # EC2用IAMロール作成
  SSMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
```

次に各リソースの設定方法とポイントを解説します。

## 1. VPCとプライベートサブネット
まず、仮想ネットワーク（VPC）とその内部にプライベートサブネット、ルートテーブルを作成します。

```template.yml
  # VPC作成
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true

  # プライベートサブネット作成
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.1.0/24
      
  # プライベートサブネット用ルートテーブル作成
  PrivateSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC

  # プライベートサブネットにルートテーブルをアタッチ
  PrivateSubnetRoute:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateSubnetRouteTable
```

## 2. EC2・SSM用セキュリティグループ
EC2とSSM用VPCエンドポイントに紐づけるセキュリティグループを作成します。
EC2用セキュリティグループのアウトバウンドルールには、以下のルールを追加します。
　SSM用VPCエンドポイントへのHTTPS通信を許可するルール
　S3のマネージドプレフィックスへのHTTPS通信を許可するルール
　　※ゲートウェイ型S3用VPCエンドポイントとの疎通に必要

SSM用セキュリティグループのインバウンドルールには、以下のルールを追加します。
　EC2からのHTTPS通信を許可するルール

```template.yml
  # SSM用セキュリティグループの作成
  MySSMSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "for SSM VPC Endpoint"
      VpcId: !Ref MyVPC
      
  # EC2用セキュリティグループの作成
  MyEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Allow outbound access to S3"
      VpcId: !Ref MyVPC
      
  # SSM用セキュリティグループのインバウンドルールに、EC2からの疎通を許可するルールを追加
  MySSMSecurityGroupIngress: 
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref MySSMSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      SourceSecurityGroupId: !GetAtt MyEC2SecurityGroup.GroupId

  # EC2用セキュリティグループのアウトバウンドルールに、SSM用VPCエンドポイントへの疎通を許可するルールを追加
  MyEC2SecurityGroupEgressAllowToSSM: 
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId: !Ref MyEC2SecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      DestinationSecurityGroupId: !GetAtt MySSMSecurityGroup.GroupId

  # EC2用セキュリティグループのアウトバウンドルールに、S3への疎通を許可するルールを追加
  MyEC2SecurityGroupEgressAllowToS3: 
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId: !Ref MyEC2SecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      DestinationPrefixListId: pl-61a54008
```

## 3. VPCエンドポイント

パッケージ更新に必要な通信先であるS3へ接続するために、S3のゲートウェイ型VPCエンドポイントを作成します。
このエンドポイントを、EC2が配置されているプライベートサブネットのルートテーブルに紐づけることが重要です。

また、マネジメントコンソール上でEC2へ接続するのに必要な以下のVPCエンドポイントも作成します。
　SSM用VPCエンドポイント
　EC2メッセージング用VPCエンドポイント
　SSMメッセージング用VPCエンドポイント
　S3用VPCエンドポイント



```template.yml
  # SSM用VPCエンドポイントの作成
  VPCEndpointSSM:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcId: !Ref MyVPC
      SubnetIds: [!Ref PrivateSubnet]
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SecurityGroupIds: [!Ref MySSMSecurityGroup]

  # EC2メッセージング用VPCエンドポイントの作成
  VPCEndpointEC2Messages:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
      VpcId: !Ref MyVPC
      SubnetIds: [!Ref PrivateSubnet]
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SecurityGroupIds: [!Ref MySSMSecurityGroup]

  # SSMメッセージング用VPCエンドポイントの作成
  VPCEndpointSSMMessage:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcId: !Ref MyVPC
      SubnetIds: [!Ref PrivateSubnet]
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SecurityGroupIds: [!Ref MySSMSecurityGroup]

  # S3用VPCエンドポイントの作成
  S3VPCEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref MyVPC
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      RouteTableIds:
        - !Ref PrivateSubnetRouteTable  # 対象サブネットのルートテーブルを紐づける
```


## 4. EC2インスタンス
Amazon Linux 2023のEC2インスタンスをプライベートサブネット内に配置します。

```template.yml
  # EC2作成
  MyEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      SubnetId: !Ref PrivateSubnet
      BlockDeviceMappings:
        - DeviceName: /dev/xvda  
          Ebs:
            VolumeType: gp3
            VolumeSize: 30  
            Encrypted: true  # 暗号化を有効にする
      SecurityGroupIds:
        - !Ref MyEC2SecurityGroup
      ImageId: resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64  # Amazon Linux 2023のAMI IDを指定
      IamInstanceProfile: !Ref MyInstanceProfile
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          sudo yum -y install http://s3-ap-northeast-1.amazonaws.com/amazon-ssm-ap-northeast-1/latest/linux_amd64/amazon-ssm-agent.rpm
```

## 5. IAMロール
セッションマネージャー経由でEC2に接続できるよう、EC2に付与するIAMロールにAmazonSSMManagedInstanceCoreのポリシーを設定します。

```template.yml
  # インスタンスプロファイル作成
  MyInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref SSMRole

  # EC2用IAMロール作成
  SSMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
```

# デプロイ
上記テンプレートを用いてCloudFormationによるリソースデプロイを実施します。
デプロイ方法は以下記事で分かりやすく記載されていますので、こちらをご参照ください。
https://blog.serverworks.co.jp/2022/02/09/073000

# 検証

### セッションマネージャーを使ったEC2への接続
AWSマネジメントコンソールでSystems ManagerのセッションマネージャーからEC2に接続し、パッケージインストールを試みます。
接続方法は下記をご参照ください。
https://baresupport.jp/blog/2022/03/14/82/#3-1_AWS_%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88%E3%82%B3%E3%83%B3%E3%82%BD%E3%83%BC%E3%83%AB%E3%81%8B%E3%82%89%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88

### パッケージのインストール
接続後、以下のコマンドでパッケージを更新します。

```bash
sudo dnf install -y htop
```

インストールが成功すればOKです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2485934/659344bd-c830-67f1-6ff4-e5ca1b690d3e.png)