🥰

CloudFormation で踏み台サーバとプライベートサブネット内インスタンスを作成する方法

2021/06/13に公開

こんにちは、Masuyama です。

業務で検証用インスタンスをプライベートサブネット内に立てる機会がありました。
作っては壊しを繰り返したり、AWS が不慣れな社内メンバーへも展開しやすいようにしたかったので CloudFormation を使って IaaC 化しててみました。

Single AZ 構成なので冗長化も何もあったものではありませんが、検証をサクッとする上ではそれなりに便利だと思います。

構成

本当は図で構成を描けたらいいのですが、取り急ぎ文章ベースで構成を紹介します。

  • VPC
    • パブリックサブネット
      • インターネットゲートウェイ
      • NAT ゲートウェイ
    • プライベートサブネット
  • EC2
    • パブリックサブネット内 x1 (踏み台サーバ)
    • プライベートサブネット内 x1 (DB インスタンス等を想定。以下、目的インスタンスと呼称)

CloudFormation テンプレート全文

テンプレートの全文となります。
それなりにコメントを入れているつもりですが、もしよく分からない点があれば解説を付け加えるのでコメントいただければと思います m(_ _)m

---
AWSTemplateFormatVersion: "2010-09-09"
Description: VPC and EC2 Creation in Single AZ

# Variables
Parameters:
  Environment:
    Type: String
    Default: Dev
    Description: Dev or Stg or Pro
  ProjectName:
    Type: String
    Default: Practice
    Description: Project Name
  VPCCidr:
    Type: String
    Default: 10.1.0.0/16
    Description: VPC IP Range
  PublicSubnetCidr:
    Type: String
    Default: 10.1.1.0/24
    Description: Public Subnet IP Range
  PrivateSubnetCidr:
    Type: String
    Default: 10.1.2.0/24
    Description: Private Subnet IP Range
  AZ:
    Type: String
    Default: ap-northeast
    Description: Tokyo
  Zone:
    Type: String
    Default: 1a
    Description: 1a or 1c or 1d
  MyIP:
    Description: IP address range which is allowed to access public subnet EC2 from it
    Type: String
    Default: 0.0.0.0/0
  KeyName:
    Description: EC2 Key Pair to allow SSH access to the instance
    Type: "AWS::EC2::KeyPair::KeyName"

Resources:
  # --------------------------------------
  # VPC Creation
  # --------------------------------------

  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidr
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Join ["-", [!Ref Environment, !Ref ProjectName, vpc]]

  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Join ["-", [!Ref Environment, !Ref ProjectName, igw]]
  # Attach Internet Gateway to VPC
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Route Table for Public Subnet
  PublicRouteTableIGW:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Join ["-", [!Ref Environment, !Ref ProjectName, public-rt]]
  # Route to Internet (Internet Gateway)
  PublicRouteIGW:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTableIGW
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  # Route Table for Private Subnet
  PrivateRouteTableNatGW:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Join ["-", [!Ref Environment, !Ref ProjectName, private-rt]]
  # Route to Internet (Nat Gateway)
  PrivateRouteNatGW:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PrivateRouteTableNatGW
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  # Public Subnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Join ["-", [!Ref AZ, !Ref Zone]]
      CidrBlock: !Ref PublicSubnetCidr
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value:
            !Join [
              "-",
              [!Ref Environment, !Ref ProjectName, public-subnet, !Ref Zone],
            ]

  # Associate Route Table for Public Subnet to Public Subnet
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTableIGW

  # Private Subnet
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Join ["-", [!Ref AZ, !Ref Zone]]
      CidrBlock: !Ref PrivateSubnetCidr
      Tags:
        - Key: Name
          Value:
            !Join [
              "-",
              [!Ref Environment, !Ref ProjectName, private-subnet, !Ref Zone],
            ]

  # Associate Route Table for Private Subnet to Private Subnet
  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTableNatGW

  # Elastic IP to attach to Nat Gateway
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: VPC

  # Nat Gateway Creation
  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet
      Tags:
        - Key: Name
          Value:
            !Join [
              "-",
              [!Ref Environment, !Ref ProjectName, subnet, ngw, !Ref Zone],
            ]

  # --------------------------------------
  # EC2 Creation
  # --------------------------------------

  # EC2 in Public Subnet
  PublicSubnetEC2:
    Type: AWS::EC2::Instance
    Properties:
      # Amazon Linux 2
      ImageId: ami-00d101850e971728d
      KeyName: !Ref KeyName
      InstanceType: t2.micro
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: !Ref PublicSubnet
          GroupSet:
            - !Ref PublicSubnetEC2SecurityGroup
      Tags:
        - Key: Name
          Value:
            !Join [
              "-",
              [!Ref Environment, !Ref ProjectName, public, ec2, !Ref Zone],
            ]
  # Security Group for EC2 in Public Subnet
  PublicSubnetEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: public-ec2-sg
      GroupDescription: Allow SSH and HTTP access from MyIP
      VpcId: !Ref VPC
      SecurityGroupIngress:
        # Inbound SSH
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
      Tags:
        - Key: Name
          Value:
            !Join [
              "-",
              [!Ref Environment, !Ref ProjectName, public, ec2, sg, !Ref Zone],
            ]

  # EC2 in Private Subnet
  PrivateSubnetEC2:
    Type: AWS::EC2::Instance
    Properties:
      # Amazon Linux 2
      ImageId: ami-00d101850e971728d
      KeyName: !Ref KeyName
      InstanceType: t2.micro
      NetworkInterfaces:
        - AssociatePublicIpAddress: "false"
          DeviceIndex: "0"
          SubnetId: !Ref PrivateSubnet
          GroupSet:
            - !Ref PrivateSubnetEC2SecurityGroup
      Tags:
        - Key: Name
          Value:
            !Join [
              "-",
              [!Ref Environment, !Ref ProjectName, private, ec2, !Ref Zone],
            ]
  # Security Group for EC2 in Private Subnet
  PrivateSubnetEC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: private-ec2-sg
      GroupDescription: Allow SSH and HTTP access from PublicSubnetCidr
      VpcId: !Ref VPC
      SecurityGroupIngress:
        # Inbound SSH
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref PublicSubnetCidr
        # Inbound HTTP
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref PublicSubnetCidr
        # Inbound HTTPS
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref PublicSubnetCidr
      Tags:
        - Key: Name
          Value:
            !Join [
              "-",
              [!Ref Environment, !Ref ProjectName, private, ec2, sg, !Ref Zone],
            ]

パラメータの説明

色々なところで使い回せるよう、いくつかのパラメータを設定するように構成しています。
テンプレートをアップデートした際のコンソールに説明が出るようにしています。

なお、KeyName では各インスタンスで使用するキーペアを指定するため、事前にキーペアを作成しておくとよいです。
今回はパブリックサブネット内、プライベートサブネット内のインスタンスのどちらも同じキーペアを指定するような構成になっていますが、使い回しを避けたい場合はパラメータを増やすようにテンプレートを修正しましょう。

踏み台サーバ経由で目的インスタンスに SSH ログイン

それではターミナル等から SSH を使い、目的インスタンスへ SSH ログインしてみましょう。

やったことが無いとちょっとイメージがしづらいかもしれませんが、ローカルから叩くコマンドを少し工夫してあげることで、直接目的インスタンスへの SSH ログインを踏み台サーバが中継することができます。

ではやってみましょう。

指定するキーペア、踏み台サーバのパブリック IP、そして目的インスタンスのプライベート IP を事前に確認しておきます。
今回は以下の通りだったとしましょう。

  • キーペア (ローカル端末内): ~/.ssh/awskey.pem
  • 踏み台サーバのパブリック IP: 1.2.3.4
  • 目的インスタンスのプライベート IP: 10.1.2.101

この時、ローカル端末で叩くコマンドは以下のようになります。

ssh -i ~/.ssh/awskey.pem -o ProxyCommand='ssh -i ~/.ssh/awskey.pem -W %h:%p ec2-user@1.2.3.4' ec2-user@10.1.1.101

※前半で指定している秘密鍵は目的インスタンス用、後半のものは踏み台サーバ用となります。
    もしテンプレートをいじって別々のキーペアを指定している時は、前半と後半で書き換えてください。

このコマンドで目的インスタンスへローカル端末からログインできるようになります。

$ ssh -i ~/.ssh/awskey.pem -o ProxyCommand='ssh -i ~/.ssh/awskey.pem -W %h:%p ec2-user@1.2.3.4' ec2-user@10.1.2.101
Last login: Sun Jun 13 13:21:45 2021 from 10.1.1.99

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-10-1-2-101 ~]$ 

ログインできました!

目的インスタンスで yum update してみる

NAT ゲートウェイとインターネット向けルートも用意しているので、プライベートサブネット内にある目的インスタンスからでもインターネットに出られるはずです。
取り急ぎ、yum update しておきましょう。

$ sudo yum -y update
...
Complete!

問題なくインターネットに出られることを確認できました。

Discussion