AWS CloudFormation 実践 第2回 EC2編

前回まで

CloudFormation連載の続きです。

前回は、CloudFormationのテンプレートを使って、AWSインフラ環境にVPC、Subnet、InternetGateway、NATGatewayを構築しました。

今回はPublic/PrivateそれぞれのSubnet内にEC2を二つたてて、ローカル環境からSSHを使ってPubilc EC2→ Private EC2 の順でログインします。

CloudFormationの基礎知識や、テンプレートの基礎、実行方法は前回の記事をご確認ください。

事前準備. キーペアの作成

CloudFormationの作業に入る前に、マネージメントコンソールでEC2にログインするための鍵ファイルを作成します。

  • 補足.調べたところ、キーペアはCloudFormationで動的に作成することもできるそうなのですが、手順が複雑化するので、本連載の後半で、余力があればトライすることにしようと思います。 qiita.com

AWSマネージメントコンソールでEC2サービスを開き、左メニューから「キーペア」を選択し、以下のように鍵を作成します。この時「名前」に入力した値は、後ほどテンプレートファイルで指定することになります。

「キーペアを作成」ボタンをクリックすると、ローカルディスクにpemファイルがダウンロードされますので、ログインのために保存しておきます。

テンプレートファイルに追記

前回まで記述したテンプレートファイルに加え、以下の内容を加筆していきます。

(Public)EC2とセキュリティグループ

早速、EC2の内容を追記していきます。最初は、Public Subnet 内に配置される、外部からSSHアクセス可能なEC2です。SSHを通すには、ポート22を許可するセキュリティグループをEC2に適用する必要がありますので、セキュリティグループも併せて作成します。

# EC2(public)
  cftPublicEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: t2.micro
      SubnetId: !Ref cftPublicSubnet
      Monitoring: false
      SecurityGroupIds:
        - !Ref cftPublicEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
        - Key: Name
          Value: cf-tutorial-public-ec2

# SecurityGroup(public EC2)
  cftPublicEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: cf-tutorial-public-ec2-sg
      GroupDescription: Allow SSH Access.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
        - 
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 60.111.xxx.xxx/32  # ← 許可ソースするIPに発信元IP(自PCのIP)を設定
      Tags:
        - Key: Name
          Value: cf-tutorial-public-ec2-sg

EC2のリソースでは、ImageIdInstanceType で対象のEC2を決定します。ImageIdは、AMIのイメージIDとなります。標準のカタログからも選択が可能であり、上記テンプレートで指定した「ami-020283e959651b381」は、以下の画面で確認した内容を指定しました。

InstanceType は、リファレンスの一覧で確認し、そこから選択します。今回は、最小レベルの種類である「t2.micro」を指定しました。

KeyNameには、事前準備したキーペアの「名前」cf-tutorial-ec2-key を指定します。

セキュリティグループリソースでは、VpcId に 該当するVPCを指定し、SecurityGroupIngress に インバウンドのルールを設定します。SecurityGroupIngress には許可するポート番号と、CidrIp(許可するソースIP)を設定します。

なお、アウトバウンドに指定を行う場合は、これに加え SecurityGroupEgress を設定します。

ちなみに、なぜかわかりませんが GroupDescription が必須だったりします。任意でよいので、今回は「Allow SSH Access.」と設定しました。

(Public)EC2にElastic IPアドレスを割り当てる

せっかくなので、PublicのEC2には、固定のグローバルIPアドレスを割当しましょう。前回NAT Gatewayでもやりましたが、AWSのElastic IPサービスを使って固有のIPを取得します。

# ElasicIp for public EC2
  cftPublicEc2Eip:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref cftPublicEc2

記述自体はそれほど難しいことはなく、InstanceId に、割り当てるEC2インスタンス!Ref で指定するだけです。これにより、Ipが自動割当てされます。

ただ、IPアドレスは自動割り当てとしているため、割り当てられたIPがいくつになるか知りたくなりますね。

割り当てられた Elastic IP をOutputする

せっかくなので、ここでテンプレートファイルの Outputs セクションを使って割当られたIPアドレスを出力してみましょう。

# IP Address Allocated to Public EC2
Outputs:
  cftPublicEc2Eip:
    Value: !GetAtt cftPublicEc2Eip.PublicIp

上記の内容は、Resources セクションと同列のインデントに記載することに注意して下さい。

cftPublicEc2EipPublicIp!GetAtt 組込み関数を使って取得し、リソース名 cftPublicEc2Eip に対する値(Value )として設定します。

!GetAttでどのような値が取得できるかは、各リソースのリファレンスの「Return Values」(EIPのResult Valuesはこちら)に書かれています。参考にしてみましょう。

<Elastic IP の Return Values>

(Private)EC2とセキュリティグループ

Publicと同様にPrivateEC2を構築します。PublicEC2との違いのポイントは以下です。

「PublicEC2のプライベートIPアドレス」 は、CloudFormationに自動割り振りされていますが、このIPはどのように取得するのでしょうか。

こちらも、前述の !GetAtt 組込み関数で取得することができます。

<EC2::Instance の Return Values>

これを踏まえ、テンプレートファイルを以下のとおり記載します。

# EC2(private)
  cftPrivateEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: t2.micro
      SubnetId: !Ref cftPrivateSubnet
      Monitoring: false
      SecurityGroupIds:
        - !Ref cftPrivateEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
        - Key: Name
          Value: cf-tutorial-private-ec2

# SecurityGroup(private EC2)
  cftPrivateEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: cf-tutorial-prviate-ec2-sg
      GroupDescription: Allow SSH Access from Public EC2 Only.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
        - 
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 
            !Sub
              - ${privateIp}/32
              - { privateIp : !GetAtt cftPublicEc2.PrivateIp }
      Tags:
        - Key: Name
          Value: cf-tutorial-prviate-ec2-sg
  • SecurityGroupIngress の CidrIp(送信元)には、前述のとおり「PublicEC2のプライベートIPアドレス」を指定したいわけですが、 CIDR表記とする必要があります。そこで今回は !Sub関数を使いました。
  • !Sub は文字列の置換関数です。${privateIp}の部分に、その下にある「privateIp」の設定内容が置換されます。設定内容には、!GetAtt cftPublicEc2.PrivateIp からは「10.0.1.55」のようなIPアドレスが返却されるので、!Sub関数のテンプレ―ト ${privateIp}/32 の結果は「10.0.1.55/32」(CIDR表記) となります。

なお、この内容でデプロイした結果をマネージメントコンソールで見ると、このEC2にはパブリックIPが割当されていません。これは前回、Private Subnetでは、IPアドレスの割り当てを false に設定したためです。これによって、外部からアクセスはできないことになります。

テンプレートファイル全文

ここまでの記述内容をまとめると次のようになります。

(このまま使用する場合は、cftPublicEc2SgSecurityGroupIngressCidrには、自PCのグローバルIPアドレスを設定してください)

▼(テンプレート全文はここをクリックしてください)

AWSTemplateFormatVersion: 2010-09-09

Resources: 
# VPC
  cftVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/21
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: cf-tutorial-vpc
  
# Internet Gateway
  cftIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: cf-tutorial-igw
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref cftVpc
      InternetGatewayId: !Ref cftIgw

# Subnet (public)  
  cftPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.1.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-public-subnet
  
  cftPublicRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-public-rtb
  
  cftPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPublicRtb
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref cftIgw

  cftPublicRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPublicSubnet
      RouteTableId: !Ref cftPublicRtb  

# Subnet (private)  
  cftPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      VpcId: !Ref cftVpc
      CidrBlock: 10.0.2.0/24
      Tags:
        - Key: Name
          Value: cf-tutorial-private-subnet
  
  cftPrivateRtb:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref cftVpc
      Tags:
        - Key: Name
          Value: cf-tutorial-private-rtb
  
  cftPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref cftPrivateRtb
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref cftNatgw

  cftPrivateRtAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref cftPrivateSubnet
      RouteTableId: !Ref cftPrivateRtb  

# NAT Gateway  
  cftNatgw:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt cftNatgwEip.AllocationId
      SubnetId: !Ref cftPublicSubnet
      Tags:
        - Key: Name
          Value: cf-tutorial-natgw

  cftNatgwEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

# EC2(public)
  cftPublicEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: t2.micro
      SubnetId: !Ref cftPublicSubnet
      Monitoring: false
      SecurityGroupIds:
        - !Ref cftPublicEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
        - Key: Name
          Value: cf-tutorial-public-ec2

# SecurityGroup(public EC2)
  cftPublicEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: cf-tutorial-public-ec2-sg
      GroupDescription: Allow SSH Access.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
        - 
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 60.111.xxx.xxx/32  # ← 許可ソースするIPに発信元IP(自PCのIP)を設定
      Tags:
        - Key: Name
          Value: cf-tutorial-public-ec2-sg

# ElasicIp for public EC2
  cftPublicEc2Eip:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref cftPublicEc2

# EC2(private)
  cftPrivateEc2:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: cf-tutorial-ec2-key
      DisableApiTermination: false
      ImageId: ami-020283e959651b381
      InstanceType: t2.micro
      SubnetId: !Ref cftPrivateSubnet
      Monitoring: false
      SecurityGroupIds:
        - !Ref cftPrivateEc2Sg
      UserData: !Base64 |
        #!/bin/bash -ex
        # put your script here
      Tags:
        - Key: Name
          Value: cf-tutorial-private-ec2

# SecurityGroup(private EC2)
  cftPrivateEc2Sg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: cf-tutorial-prviate-ec2-sg
      GroupDescription: Allow SSH Access from Public EC2 Only.
      VpcId: !Ref cftVpc
      SecurityGroupIngress:
        - 
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 
            !Sub
              - ${privateIp}/32
              - { privateIp : !GetAtt cftPublicEc2.PrivateIp }
      Tags:
        - Key: Name
          Value: cf-tutorial-prviate-ec2-sg

# ======================
#  Outputs Statement
# ======================
# IP Address Allocated to Public EC2
Outputs:
  cftPublicEc2Eip:
    Value: !GetAtt cftPublicEc2Eip.PublicIp

デプロイを実施

上記のテンプレートファイル(cf.yaml)を、AWS環境に対してデプロイします。

デプロイは次のコマンドで実行します。

> aws cloudformation deploy --template-file ./cf.yaml --stack-name cf-tutorial

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - cf-tutorial

CloudFormationが正常終了すれば、作業終了です。マネージメントコンソールから、各リソースを開いて、内容がテンプレートファイルのとおり作られているかチェックしてみましょう。

接続確認

SSH接続をするには、接続先となるEC2のIPアドレスを把握する必要があります。マネージメントコンソール画面のCloudFormationを開き、スタック cf-tutorial の「出力」タブを選択してください。テンプレートファイルに Outputs セクション を記述したことで、PublicEC2のグローバルIPアドレスが表示されています。

この値(グローバルIPアドレス)に対してSSH接続を行います。

PublicEC2にSSH接続

Powershell(またはコマンドプロンプト)を開き、キーペアをダウンロードしたフォルダまで移動してから、SSHコマンドで接続を行います。

PS> ssh -i cf-tutorial-ec2-key.pem ec2-user@3.115.208.35

The authenticity of host '3.115.208.35 (3.115.208.35)' can't be established.
ED25519 key fingerprint is SHA256:0PtpyHKlYHCUUh7jnd/wD1TIOVqtkb6Y6/Cp6D3V0Ls.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '3.115.208.35' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-10-0-1-49 ~]$

無事、PublicEC2 まで接続できたようです。

次の作業のため、一度、接続を切断します。

[ec2-user@ip-10-0-1-49 ~]$ exit
logout
Connection to 3.115.208.35 closed.
PS> 

PublicEC2に鍵を転送する

次に、PublicEC2に、PrivateEC2に接続するための鍵を転送します。ファイルの転送には、SCPコマンドを使用します。

PS> scp -i cf-tutorial-ec2-key.pem cf-tutorial-ec2-key.pem ec2-user@3.115.208.35:~

cf-tutorial-ec2-key.pem                              100% 1675    95.9KB/s   00:00

これにより、PublicEc2内に、接続用の鍵が送り込まれました。

PublicEC2経由でPrivateEC2にSSH接続

  • 最初に、ローカルPCでPowershellを開き、PublicEC2にSSH接続します。
PS> ssh -i cf-tutorial-ec2-key.pem ec2-user@3.115.208.35

The authenticity of host '3.115.208.35 (3.115.208.35)' can't be established.
ED25519 key fingerprint is SHA256:0PtpyHKlYHCUUh7jnd/wD1TIOVqtkb6Y6/Cp6D3V0Ls.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '3.115.208.35' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-10-0-1-49 ~]$
[ec2-user@ip-10-0-1-49 ~]$ sudo chmod 600 cf-tutorial-ec2-key.pem
  • 最後に、この秘密鍵を使用して、PrivateEC2にSSH接続します。
[ec2-user@ip-10-0-1-49 ~]$ ssh -i cf-tutorial-ec2-key.pem ec2-user@10.0.2.54

The authenticity of host '10.0.2.54 (10.0.2.54)' can't be established.
ED25519 key fingerprint is SHA256:uLWt5bgqBiGE4cQchtz2ihaoIzHOgrX4N8GNgMJkLqI.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.0.2.54' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-10-0-2-54 ~]$

無事接続できました。画面だとわかりずらいですが、図にするとこんな感じです。

また念のため、このPrivateEC2から、外部アクセスができるかを確認しています。

# hostコマンドでgoogleページのIPアドレスをチェック
[ec2-user@ip-10-0-2-54 ~]$ host www.google.co.jp
www.google.co.jp has address 142.251.222.3
www.google.co.jp has IPv6 address 2404:6800:4004:81f::2003

# nslookupで応答を確認
[ec2-user@ip-10-0-2-54 ~]$ nslookup 142.251.222.3
3.222.251.142.in-addr.arpa      name = nrt13s71-in-f3.1e100.net.

Authoritative answers can be found from:

無事、応答できていることが分かります。

まとめ

前回と併せて、Cloud FormationでAWSの基本的なEC2構成のAWSインフラを構築できました。これまで手作業で構築していたEC2が、コマンド1回(2分程度)でできるので、簡単に検証環境などを作ることができます。これは便利!

次回は、Parameters、Mappingsといった、Resources以外のセクションも活用して、同じ構成を理想的なIaCに近づけていこうと思います。

なお、この結果については、以下のGitHubに掲載していますのでご確認ください。 github.com