AWS CloudFormationを使用したEC2インスタンスのプロビジョニング

# はじめに
下記のQiita記事では、VPC環境を自動作成するAWS CloudFormationのサンプルコードを紹介しました。

https://qiita.com/duelist2020jp/items/111b18950ba82de6e338

今回は、上記のCloudFormationで作成したVPC環境上に、EC2インスタンスをプロビジョニングするためのサンプルコードを紹介します。

# CloudFormationのクロススタック機能の利用
既にCloudFormationによって作成された環境を利用して、新たなリソースをプロビジョニングしたい場合、CloudFormationのクロススタック機能を利用します。クロススタック機能の使い方は、下記のAWS CloudFormationドキュメントを参照ください。
[Walkthrough: Refer to resource outputs in another AWS CloudFormation stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/walkthrough-crossstackref.html)

本記事では、下記のような流れでクロススタック機能を使用します。
* 事前にCloudFormationでVPC環境のスタックを作成しておく。(スタック名：MyNetwork-01)
* 上記で作成されたVPCのサブネット上に、EC2インスタンスをプロビジョニングするCloudFormationコードを追加で実行し、新たなスタックを作成する。(スタック名：MyEC2Instance-01)

# クロススタック機能を使用したEC2プロビジョニング用サンプルコード
今回作成したサンプルコードは下記ようなYAMLファイルです。

```yaml:Create_EC2Instance_Stack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Sample Script for Creating Public/Private EC2 Instance

Parameters:
  MyNetworkStackName:
    Description: This stack name is already created Network Stack for provisioning Public/Private EC2 Instance
    Type: String
    Default: MyNetwork-Stack01

Resources:
  MyKey:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: MyEC2Key
      PublicKeyMaterial: '{{resolve:ssm:publickeymaterial:1}}'
      Tags:
        - Key: Name
          Value: My-Key
  MyPublicInstance01:
    Type: AWS::EC2::Instance
    Properties:
      CreditSpecification: 
        CPUCredits: standard
      InstanceType: t2.micro
      KeyName: !Ref MyKey
      ImageId: ami-078296f82eb463377
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: 
            Fn::ImportValue: 
              !Sub "${MyNetworkStackName}--PublicSubnetId01"
          GroupSet:
            - 
              Fn::ImportValue: 
                !Sub "${MyNetworkStackName}--PublicSubnetSecGrpId01"
      Tags:
        - Key: Name
          Value: My-Public-instance-01
  MyPrivateInstance01:
    Type: AWS::EC2::Instance
    Properties:
      CreditSpecification: 
        CPUCredits: standard
      InstanceType: t2.micro
      KeyName: !Ref MyKey
      ImageId: ami-078296f82eb463377
      NetworkInterfaces:
        - AssociatePublicIpAddress: "false"
          DeviceIndex: "0"
          SubnetId: 
            Fn::ImportValue: 
              !Sub "${MyNetworkStackName}--PrivateSubnetId01"
          GroupSet: 
            -
              Fn::ImportValue: 
                !Sub "${MyNetworkStackName}--PrivateSubnetSecGrpId01"
      Tags:
        - Key: Name
          Value: My-Private-instance-01
  MyPublicInstance02:
    Type: AWS::EC2::Instance
    Properties:
      CreditSpecification: 
        CPUCredits: standard
      InstanceType: t2.micro
      KeyName: !Ref MyKey
      ImageId: ami-078296f82eb463377
      NetworkInterfaces:
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: 
            Fn::ImportValue: 
              !Sub "${MyNetworkStackName}--PublicSubnetId02"
          GroupSet:
            - 
              Fn::ImportValue: 
                !Sub "${MyNetworkStackName}--PublicSubnetSecGrpId02"
      Tags:
        - Key: Name
          Value: My-Public-instance-02
  MyPrivateInstance02:
    Type: AWS::EC2::Instance
    Properties:
      CreditSpecification: 
        CPUCredits: standard
      InstanceType: t2.micro
      KeyName: !Ref MyKey
      ImageId: ami-078296f82eb463377
      NetworkInterfaces:
        - AssociatePublicIpAddress: "false"
          DeviceIndex: "0"
          SubnetId: 
            Fn::ImportValue: 
              !Sub "${MyNetworkStackName}--PrivateSubnetId02"
          GroupSet: 
            -
              Fn::ImportValue: 
                !Sub "${MyNetworkStackName}--PrivateSubnetSecGrpId02"
      Tags:
        - Key: Name
          Value: My-Private-instance-02
Outputs:
  MyPublicInstanceIPaddr01:
    Description: My Public Instance's Public IP address for AzName01
    Value: !GetAtt MyPublicInstance01.PublicIp
  MyPrivateInstanceIPaddr01:
    Description: My Private Instance's Private IP address for AzName01
    Value: !GetAtt MyPrivateInstance01.PrivateIp
  MyPublicInstanceIPaddr02:
    Description: My Public Instance's Public IP address for AzName02
    Value: !GetAtt MyPublicInstance02.PublicIp
  MyPrivateInstanceIPaddr02:
    Description: My Private Instance's Private IP address for AzName02
    Value: !GetAtt MyPrivateInstance02.PrivateIp
```
上記サンプルコードの主なポイントは下記の部分になります。
* Parametersセクション
  * 既に作成されているVPC環境に対応するCloudFormationのスタック名を指定します。
* Resourcesセクション
  * <font color="blue">AWS::EC2::KeyPair</font>
    * EC2インスタンスに接続するための公開鍵を指定します。新規にコード内で鍵を作成することも可能ですが、ここでは自分のPC上に既に存在している公開鍵(<font color="Blue">ssh-keygen</font>コマンドで作成)をAWS上にアップロードして使用するようにしています。
なお、コード内に直接鍵の中身を記述するのはセキュリティ上良くないので、AWS Systems Managerのパラメータ・ストア内に公開鍵の中身を事前に保管しておきます。それを、<font color="Green">PublicKeyMaterial:</font><font color="Blue"> '{{resolve:ssm:publickeymaterial:1}}'</font>のロジックを使用して値を入手できるように設定します。Terraformでは、file関数があるので自身のPC上にある公開鍵のファイル・パスを指定すればOKなのですが、CloudFormationにはそのような機能はなさそうです。
  * <font color="Blue">AWS::EC2::Instance</font>
    * EC2インスタンスのプロビジョニングに必要な情報を記載します。VPC環境作成用のCloudFormationコードの実行によって作成されたサブネットIDとセキュリティー・グループIDの情報を下記のように<font color="Blue">ImportValue</font>関数を利用して取得します。
```yaml
NetworkInterfaces:
  - AssociatePublicIpAddress: "true"
    DeviceIndex: "0"
    SubnetId: 
      Fn::ImportValue: 
        !Sub "${MyNetworkStackName}--PublicSubnetId01"
    GroupSet:
      - 
        Fn::ImportValue: 
          !Sub "${MyNetworkStackName}--PublicSubnetSecGrpId01"
```
<font color="Blue">ImportValue</font>関数の詳細については、下記リンク先のドキュメントを参照ください。
[Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html)
なお、上記リンク先のサンプルコードを模倣して<font color="Blue">ImportValue</font>関数部分を書こうとするとエラーになり動きません。色々試行錯誤を繰り返して、今回紹介したコードのような形で記載すると正常稼働することが確認できました。ですので、<font color="Blue">ImportValue</font>関数を書いていて上手くいかない状況に陥った方は参考にしていただければと思います。
* Outputsセクション
  * パブリック・インスタンスのパブリックIPアドレスと、プライベート・インスタンスのプライベートIPアドレスを出力するようにしています。

# EC2インスタンス・プロビジョニング用サンプルコードの実行
既にVPC環境作成用CloudFormationコードの実行が終了していることを確認後、今回のサンプルコードを実行します。
* スタック名を入力
![p14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1567099/97b85f8a-4f80-1332-7689-acf532ee19c3.jpeg)
パラメータには、既に実行済みのVPC環境に対するスタック名を指定します。(本記事では、MyNetwork-01を指定。)

・パラメータの確認
![p15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1567099/11fbd192-2610-58ca-19b4-85a188082b51.jpeg)
* コードの実行
![p16.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1567099/fb73b303-9e47-ea8e-0902-76eab792fbc8.jpeg)
* スタック作成完了後のイメージ
![p17.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1567099/3fae9c1f-7061-d0b6-371a-71049ce70dcd.jpeg)
* 「出力」タブの確認
サンプルコードのOutputsセクションに指定した、パブリック・インスタンスのIPアドレスとプライベート・インスタンスのIPアドレスの一覧が表示されます。
![p18.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1567099/493e7247-5359-cd06-78ce-1de53c6ae12b.jpeg)

# EC2インスタンスへの接続確認
ここでは上記の「出力」イメージにおけるAzName01のアベイラビリティー・ゾーンに作成されたパブリック・インスタンス(パブリックIP：18.181.201.108)、プライベート・インスタンス(プライベートIP：10.0.2.231)にそれぞれ接続できることを確認します。本記事ではTeratermを使用して接続確認を行います。(AzName02に対するものについては同様なので割愛します。)

* パブリック・インスタンスへの接続確認
![p19.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1567099/648e8852-874a-d4d8-a60d-1210a03b70a2.jpeg)
接続確認OKです。なお、プライベート・インスタンスに対するセキュリティー・グループにおいて、パブリック・インスタンスからのping応答を許可するように設定しているため、プライベート・インスタンスに対するpingも問題なく返ってくることが確認できます。
![p20.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1567099/f384f115-f808-0d90-4b0a-2a4c28418c88.jpeg)

* プライベート・インスタンスへの接続確認
プライベート・インスタンスには直接TeratermでSSH接続はできないため、パブリック・インスタンスにSSH接続を行った後、そこからプライベート・インスタンスに対してSSH接続を実施します。その為には、パブリック・インスタンス上で、プライベート・インスタンスにSSH接続するための秘密鍵を作成しておきます。(PC上にある秘密鍵をそのままエディタでコピーすればOKです。)
秘密鍵はMyEC2Key.pemというネーミングで作成しているので、同ネーミングでパブリック・インスタンス上に作成後、chmodコマンドで秘密鍵のアクセス権限をec2-userユーザーのみ読み書き権限を付与するように変更します。後は、作成した秘密鍵を指定の上、プライベート・インスタンスのIPアドレスに対して、SSHコマンドでログオンすればプライベート・インスタンスに接続可能です。
![p23.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1567099/e84608d1-902a-14ad-461b-af45cceee06a.jpeg)
接続確認OKです。

# おわりに
今回はCloudFormationのクロススタック機能を利用して、既にCloudFormationのコードで作成されているVPC環境上に、新たにEC2インスタンスをプロビジョニングするためのサンプルコードを紹介しました。コードを書く過程で、AWSのリソースに対する知識も色々得ることができるため、CloudFormationを使いこなせるようになると自身のAWSスキルの幅も拡げることができるかなと思いました。今後も他のAWSサービスを学んだ折りには、CloudFormationでのコード化にチャレンジしてみようと思います。