PrivateLinkを通じてSSMを使用するCFnテンプレートを作ってみた

2019.07.31

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

なつのうたを聴きながら、暑い暑いと連日思っています。コウペンちゃんが可愛いです。
日本の夏はとっても暑いので、プライベートサブネット配下のインスタンスをPrivateLinkを通じてSSMを使用して操作したくなりました。
ついでにCloudFormationのテンプレートも作成したくなったのでつくりました。

VPC Endpointとは

本題に入る前に、VPC Endpointについて少しだけ記載します。
2015年の4月まで、EC2インスタンスから各種AWSリソース(S3、DynamoDB、CodeCommit...)に対してアクセスするためにはパブリックIPアドレスが必要でした。

パブリックサブネット配下のインスタンスは、インスタンス -> Internet GW -> S3のような経路で、プライベートサブネット配下のインスタンスは、インスタンス -> NAT GW -> S3のような経路でアクセスする必要がありました。

NAT GWが必要になったり、インターネットを通じてアクセスするため経路をAWS内に閉じ込めるということができませんね。
同年の5月にVPC Endpointが新機能として提供され、S3に対してインターネットを通さずにアクセスできるようになりました。
インスタンスからS3への通信をエンドポイントを通じて行います。

ルートテーブル単位でエンドポイントへのルーティングを行います。
なので、通信経路はインスタンス -> エンドポイント -> S3となります。
このルートテーブルを使用して制御するエンドポイントのことを、ゲートウェイVPCエンドポイントと呼び、S3とDynamoDBのみ対応しています。

2017年11月にインターフェイスVPCエンドポイントという方式のエンドポイントが発表されました。
この方式では、VPC配下にENIを作成して、ENIと各種サービスのエンドポイントをPrivateLinkで繋ぎます。
ENIを使用するのでセキュリティグループを紐づける必要があり、またサブネット単位で制御することになります。
なので、インスタンス -> ENI -> SSM Endpoint のような経路で通信を行います。

実装について

冒頭でも記載した通り、CloudFormationのテンプレートを作成していきます。
実際に使用する場合は色々拡張してください。
Resources配下で、下記のパラメータを使用しているので頭の部分だけ載せておきます。

thin-private-subnet-ssm.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: SSM for Private Subnet
Parameters:
  Prefix:
    Description: Prefix
    Type: String
    Default: thin-ssm
  AZ1:
    Description: AZ1
    Type: String
    Default: a
  AMZN2:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"

今後使う予定のAZとAMI IDのみ記載しています。
SSMのエンドポイントはap-northeast-1dを現在サポートしていないのでご注意ください。

VPC

まずは、大枠のVPCから書いていきましょう。CIDRやタグなどはご自由にどうぞ。

ハイライトがある5、6行目のEnableDnsSupportEnableDnsHostnamesは必ず有効化してください。
特に、EnableDnsHostnamesのデフォルトがfalseなので、書き忘れていて動かない...とかよくありそうです。

thin-private-subnet-ssm.yaml VPC

  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 192.168.0.0/24
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-vpc"
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-private-rtb"
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Sub "${AWS::Region}${AZ1}"
      VpcId: !Ref VPC
      CidrBlock: 192.168.0.0/28
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-private-subnet"
  PrivateSubnetAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

Security Group

1つ目のSecurity Group(SG)はEC2インスタンスに紐づけるためのものです。
特に外部と通信するとかの要件がなかったので、何もルールをつけていません。
2つ目のVPCEndpoint用のSecurity Group(SSMEndpointSG)では、VPC内のインスタンスからHTTPSでの通信を許可しています。
このルールは必ず必要なので作成しましょう。

thin-private-subnet-ssm.yaml: Security Group

  SG:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref VPC
      GroupName: !Sub "${Prefix}-sg"
      GroupDescription: !Sub "${Prefix}-sg"
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-sg"
  SSMEndpointSG:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref VPC
      GroupName: !Sub "${Prefix}-ssm-endpoint-sg"
      GroupDescription: !Sub "${Prefix}-ssm-endpoint-sg"
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-ssm-endpoint-sg"
  SSMEndpointSGIngress1:
    Type: "AWS::EC2::SecurityGroupIngress"
    Properties:
      GroupId: !Ref SSMEndpointSG
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      CidrIp: 192.168.0.0/24

VPC Endpoint

VPC Endpointを作っていきましょう。
SSMのドキュメントに書いてある通りに下記のエンドポイントを作成していきます。また今回は東京リージョンに作成するので、regionの値はap-northeast-1になります。
ssmmessagesのみ作成するかは任意選択となっています。
セッションマネージャーを使用したい場合にssmmessagesのエンドポイントを作成します。

  • com.amazonaws.region.ssm
  • com.amazonaws.region.ec2messages
  • com.amazonaws.region.ec2:
  • com.amazonaws.region.ssmmessages
  • com.amazonaws.region.s3

インタフェース型のVPC Endpointは、PrivateDnsEnabledをtrueにしてください。
注意点はこのくらいです。後は今までの流れを組んでよしなに書いていきます。

thin-private-subnet-ssm.yaml: VPC Endpoint

  SSMEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SSMEndpointSG
  EC2MessageEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SSMEndpointSG
  EC2Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SSMEndpointSG
  SSMAgentEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SSMEndpointSG
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcId: !Ref VPC
      RouteTableIds:
        - !Ref PrivateRouteTable

EC2 Instance

インスタンスプロファイルを指定します(IAMの項で作成します)。
それ以外についてはよしなに書いていきます。
OSが要件を満たしているかどうかは、こちらを確認してください。

thin-private-subnet-ssm.yaml: EC2 Instance

  Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      IamInstanceProfile: !Ref ServerProfile
      ImageId: !Ref AMZN2
      InstanceType: t3.small
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp2
            VolumeSize: 8
            DeleteOnTermination: true
      NetworkInterfaces:
        - AssociatePublicIpAddress: false
          DeviceIndex: "0"
          DeleteOnTermination: true
          GroupSet:
            - !Ref SG
          SubnetId: !Ref PrivateSubnet
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-Instance"

IAM

SSM用のポリシー作成の方針として、AmazonSSMManagedInstanceCoreをベースにして必要なポリシーを追加していく必要があります。
今回は最小限の設定で問題ないので、こちらを参考にして最小のS3アクセスのみを追加しています。
作成したインスタンスプロファイルをEC2側で参照するようにしています。

thin-private-subnet-ssm.yaml: IAM

  ServerRole:
    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"
  S3BucketPolicyForSSM:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: S3BucketPolicyForSSM
      Roles:
        - !Ref ServerRole
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action: "s3:GetObject"
            Resource:
              - !Sub "arn:aws:s3:::aws-ssm-${AWS::Region}/*"
              - !Sub "arn:aws:s3:::aws-windows-downloads-${AWS::Region}/*"
              - !Sub "arn:aws:s3:::amazon-ssm-${AWS::Region}/*"
              - !Sub "arn:aws:s3:::amazon-ssm-packages-${AWS::Region}/*"
              - !Sub "arn:aws:s3:::${AWS::Region}-birdwatcher-prod/*"
              - !Sub "arn:aws:s3:::patch-baseline-snapshot-${AWS::Region}/*"
  ServerProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Path: "/"
      Roles:
        - Ref: ServerRole
      InstanceProfileName: !Sub "${Prefix}-Server"

全体像

今までの内容をまとめるとこのようなテンプレートが出来上がりました。
これで、プライベートサブネット配下のEC2インスタンスに対してSSMで作業ができますね。

thin-private-subnet-ssm.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: SSM for Private Subnet
Parameters:
  Prefix:
    Description: Prefix
    Type: String
    Default: thin-ssm
  AZ1:
    Description: AZ1
    Type: String
    Default: a
  AMZN2:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"

Resources:
  #-----------------------------------------------------------------------------
  # VPC
  #-----------------------------------------------------------------------------
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 192.168.0.0/24
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-vpc"
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-private-rtb"
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Sub "${AWS::Region}${AZ1}"
      VpcId: !Ref VPC
      CidrBlock: 192.168.0.0/28
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-private-subnet"
  PrivateSubnetAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable
  #-----------------------------------------------------------------------------
  # Securty Group
  #-----------------------------------------------------------------------------
  SG:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref VPC
      GroupName: !Sub "${Prefix}-sg"
      GroupDescription: !Sub "${Prefix}-sg"
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-sg"
  SSMEndpointSG:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref VPC
      GroupName: !Sub "${Prefix}-ssm-endpoint-sg"
      GroupDescription: !Sub "${Prefix}-ssm-endpoint-sg"
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-ssm-endpoint-sg"
  #-----------------------------------------------------------------------------
  # SG(Security Group) Rules
  #-----------------------------------------------------------------------------
  SSMEndpointSGIngress1:
    Type: "AWS::EC2::SecurityGroupIngress"
    Properties:
      GroupId: !Ref SSMEndpointSG
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      CidrIp: 192.168.0.0/24
  #-----------------------------------------------------------------------------
  # Endpoint
  #-----------------------------------------------------------------------------
  SSMEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SSMEndpointSG
  EC2MessageEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SSMEndpointSG
  EC2Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SSMEndpointSG
  SSMAgentEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages"
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet
      SecurityGroupIds:
        - !Ref SSMEndpointSG
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcId: !Ref VPC
      RouteTableIds:
        - !Ref PrivateRouteTable
  #-----------------------------------------------------------------------------
  # EC2 Instance
  #-----------------------------------------------------------------------------
  Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      IamInstanceProfile: !Ref ServerProfile
      ImageId: !Ref AMZN2
      InstanceType: t3.small
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp2
            VolumeSize: 8
            DeleteOnTermination: true
      NetworkInterfaces:
        - AssociatePublicIpAddress: false
          DeviceIndex: "0"
          DeleteOnTermination: true
          GroupSet:
            - !Ref SG
          SubnetId: !Ref PrivateSubnet
      Tags:
        - Key: Name
          Value: !Sub "${Prefix}-Instance"
  #-----------------------------------------------------------------------------
  # IAM
  #-----------------------------------------------------------------------------
  ServerRole:
    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"
  S3BucketPolicyForSSM:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: S3BucketPolicyForSSM
      Roles:
        - !Ref ServerRole
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action: "s3:GetObject"
            Resource:
              - !Sub "arn:aws:s3:::aws-ssm-${AWS::Region}/*"
              - !Sub "arn:aws:s3:::aws-windows-downloads-${AWS::Region}/*"
              - !Sub "arn:aws:s3:::amazon-ssm-${AWS::Region}/*"
              - !Sub "arn:aws:s3:::amazon-ssm-packages-${AWS::Region}/*"
              - !Sub "arn:aws:s3:::${AWS::Region}-birdwatcher-prod/*"
              - !Sub "arn:aws:s3:::patch-baseline-snapshot-${AWS::Region}/*"
  ServerProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Path: "/"
      Roles:
        - Ref: ServerRole
      InstanceProfileName: !Sub "${Prefix}-Server"

このテンプレートを元にスタックを作成するには下記のコマンドを実行します。
IAMを操作するのでcapabilitiesを追加しています。

aws cloudformation create-stack \
  --stack-name ssm-thin-sampple \
  --template-url https://gist.githubusercontent.com/37108/ad4519a138addf291000e44491ec46ed/raw/e9bcf573db6c18b3d756f8c73dba781d799acd6b/thin-private-subnet-ssm.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --region ap-northeast-1

さいごに

VPC Endpointは便利ですね。自分のやろうとしたことに似たテンプレートが見つからなかったので自分で作って公開してみました。
ぜひ、なつのうたを聴いて癒されてください。