EC2にSSH接続できる環境を、CloudFormationを利用して作ってみた

# はじめに
今回の目標は、以下のとおりです。

```
パブリックサブネットに配置されたEC2にSSH接続できる環境を、CloudFormationで作成する。
また、そのテンプレートをローカルからAWSCLIを利用してデプロイする。
```
以降では、本記事で利用したテンプレートを記載し、またそれらのパラメータについて説明しようと思います。
最後に、実際にテンプレートをCLIを利用してローカルからデプロイし、実際にSSHしてみます。

# 構成内容
- VPC
- InternetGateway
- EC2(Public) 

![最低限なインフラ構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586524/b8b5bf5a-293e-b95d-b5b6-0bac1bae44f1.png)


# やったこと
1. `CloudFormation`で利用するテンプレートの作成
1. テンプレートのデプロイ
1. 作成したインスタンスへの、SSH接続

※事前にデプロイ用のIAMユーザ、SSH接続用のキーペアは作成済み

# `CloudFormation`で利用するテンプレートの作成
## 今回利用したテンプレート
```

AWSTemplateFormatVersion: "2010-09-09"
Description: A sample template
Mappings:
  RegionMap:
    ap-northeast-1a: 
      Name: "ap-northeast-1a"
    ap-northeast-1c: 
      Name: "ap-northeast-1c"

Resources:
#            ____  
#   ___  ___|___ \ 
#  / _ \/ __| __) |
# |  __/ (__ / __/ 
#  \___|\___|_____|
  WebInstance:
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: ami-011facbea5ec0363b
      InstanceType: t2.micro
      KeyName: "keypair"
      SubnetId: 
        Ref: WebPublicSubnet
      SecurityGroupIds:
        [Ref: PublicSecurityGroup]
      Tags:
        -
          Key: "Name"
          Value: "web"

# __   ___ __   ___ 
# \ \ / / '_ \ / __|
#  \ V /| |_) | (__ 
#   \_/ | .__/ \___|
#       |_|         
  WebVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        -
          Key: Name
          Value: PublicVpc

  WebPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !FindInMap [RegionMap, ap-northeast-1a, Name]
      VpcId:
        Ref: WebVpc
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        -
          Key: Name
          Value: WebPublicSubnet

#  ___       _                       _   
# |_ _|_ __ | |_ ___ _ __ _ __   ___| |_ 
#  | || '_ \| __/ _ \ '__| '_ \ / _ \ __|
#  | || | | | ||  __/ |  | | | |  __/ |_ 
# |___|_| |_|\__\___|_|  |_| |_|\___|\__|
                                       
#   ____       _                           
#  / ___| __ _| |_ _____      ____ _ _   _ 
# | |  _ / _` | __/ _ \ \ /\ / / _` | | | |
# | |_| | (_| | ||  __/\ V  V / (_| | |_| |
#  \____|\__,_|\__\___| \_/\_/ \__,_|\__, |
#                                    |___/ 
  InternetGateway:
    Type: AWS::EC2::InternetGateway

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref WebVpc

#  ____             _      _____     _     _      
# |  _ \ ___  _   _| |_ __|_   _|_ _| |__ | | ___ 
# | |_) / _ \| | | | __/ _ \| |/ _` | '_ \| |/ _ \
# |  _ < (_) | |_| | ||  __/| | (_| | |_) | |  __/
# |_| \_\___/ \__,_|\__\___||_|\__,_|_.__/|_|\___|
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref WebVpc

  DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: RouteTable
      DestinationCidrBlock: 0.0.0.0/0 
      GatewayId: 
        Ref: InternetGateway
    DependsOn: 
      - SubnetRouteTableAssociation

  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref WebPublicSubnet
#                     _   _ _                                       
#  ___  ___  ___ _   _| |_(_) |_ _   _    __ _ _ __ ___  _   _ _ __  
# / __|/ _ \/ __| | | | __| | __| | | |  / _` | '__/ _ \| | | | '_ \ 
# \__ \  __/ (__| |_| | |_| | |_| |_| | | (_| | | | (_) | |_| | |_) |
# |___/\___|\___|\__,_|\__|_|\__|\__, |  \__, |_|  \___/ \__,_| .__/ 
#                                |___/   |___/                |_|   
  PublicSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "sg for public"
      GroupName: "public sg"
      VpcId: !Ref WebVpc
      SecurityGroupIngress:
        - 
          CidrIp: 0.0.0.0/0
          IpProtocol: tcp
          FromPort: 22
          ToPort: 22
```
## 各設定値について（カッコ内は本記事での論理ID）
### AWS::EC2::Instance(`WebInstance`)
#### ImageId
どのAMIでインスタンスを作成するかを設定する。今回はAWSが提供している標準的なAMIを利用した。ここを動的に指定すれば、常に起動インスタンスのバージョンを最新にする、的なことができる、はず。

#### InstanceType
起動するインスタンスのタイプを設定する（そのまんま）。

#### KeyName
作成したインスタンスにSSHする際に利用するキーペアを設定する。ここで設定しておかないと、SSHできなるなるので注意（セッションマネージャを利用しての接続があるので、できないこともない。むしろ、そっちを利用したほうがわざわざセキュリティグループのSSH用の穴を開けなくて済むので、安全かも）。

#### SubnetId
インスタンスが、どのサブネットに属するかを設定する。

#### SecurityGroupIds
インスタンスが、どのセキュリティグループに属するかを設定する。`Id「s」`とあるように、リストで書く必要がある。

#### Tags
タグです（そのまんま）。`Key`と`Value`を設定し、リソースに情報を与えることができる。「特定のインスタンスだけ操作したいぜ」等なときに便利。これ以降の`Tags`も同様。なので以降は割愛。

### AWS::EC2::VPC(`WebVpc`)
#### CidrBlock
利用するIPアドレスの範囲（CIDR表記）。

#### EnableDnsHostnames
パブリック IP アドレスを持つインスタンスが、対応するパブリック DNS ホスト名を取得するかどうか。これと`EnableDnsSupport`の両方が`true`じゃないと、インスタンスにパブリックDNSが割り当てられない。

#### EnableDnsSupport
DNS 解決がサポートされているかどうか。これと`EnableDnsHostnames`の両方が`true`じゃないと、インスタンスにパブリックDNSが割り当てられない。

参考：<br>
AWS公式ドキュメント<br>
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-dns.html

AWSでPublic DNS(パブリックDNS)が割り当てられない時の解決法<br>
https://qiita.com/sunadoridotnet/items/4ea689ce9f206e78a523

### AWS::EC2::Subnet(`WebPublicSubnet`)
#### AvailabilityZone
作成するサブネットがどの`AvailabilityZone`に属するかを設定する。

#### VpcId
作成するサブネットがどの`VPC`に属するかを設定する。

#### CidrBlock
利用するIPアドレス範囲（CIDR表記）。

#### MapPublicIpOnLaunch
このサブネットで起動するインスタンスが、パブリックIPを取得するかどうかを設定する。

### AWS::EC2::InternetGateway(`InternetGateway`)
設定値は特にないです（そもそもタグしかつけられない）。<br>
ただ、これがないとインタネットゲートウェイが作成できない。<br>
「どの`VPC`にアタッチするか」を設定するために、
後述する`AWS::EC2::VPCGatewayAttachment`と一緒に使いましょう。

### AWS::EC2::VPCGatewayAttachment(`InternetGatewayAttachment`)
#### InternetGatewayId 及び VpcId
インタネットゲートウェイと`VPC`の関係を設定する。<br>
「このインタネットゲートウェイ(=`InternetGatewayId`)を、この`VPC`(=`VpcId`)にアタッチする」というように使う。

### AWS::EC2::RouteTable(`RouteTable`)
#### VpcId
このルートテーブルを使用する`VPC`を指定。

### AWS::EC2::Route(`DefaultRoute`)
#### RouteTableId
ここで作成するルーティング情報を、どのルートテーブルに適用するかを設定する。

#### DestinationCidrBlock
転送するIPアドレスの範囲を設定する。今回は、これをデフォルトゲートウェイとして扱いたいので、`0.0.0.0/0`を指定。

#### GatewayId
転送先のインタネットゲートウェイのIDを設定する。他にも転送先としてインスタンス等があり、どれか一つのみを指定する必要がある。

#### DependsOn
これを記述すると、
「このリソースは、`DependsOn`で指定したリソースを作成したあとに作成してね」
というように、リソースの作成順序を明示的に指定できる。<br>
基本、AWSがよしなにやってくれるが、たまに明示的に指示しないとスタックの作成に失敗する。
ルーティング設定のリソース絡みは要注意？らしい。

主な例外：<br>
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html

### AWS::EC2::SubnetRouteTableAssociation(`SubnetRouteTableAssociation`)
#### RouteTableId 及び SubnetId
`AWS::EC2::VPCGatewayAttachment`みたいなもの。「どのルートテーブルをどのサブネットにアタッチするか」を設定する。

### AWS::EC2::SecurityGroup(`PublicSecurityGroup`)
#### GroupDescription
このセキュリティグループの説明を記述する。`Description`なのだが、`必須`なので注意。

#### GroupName
このセキュリティグループの名前をつける。こっちは任意。

#### VpcId
このセキュリティグループを適用する`VPC`のIDを設定する。

#### SecurityGroupIngress
所謂インバウンドルールを設定する。以下の４つが必要。

- CidrIp<br>
所謂ソース。`SourceSecurityGroupId`等、何でもいいからソースは必要
- IpProtocol<br>
どのプロトコルを利用するかを設定する
- FromPort, ToPort<br>
ポートの利用範囲を設定する

# テンプレートのデプロイ
作成したテンプレートを、実際にデプロイします。冒頭で述べたとおり、今回は`AWSCLI`を利用します。
といっても、下記コマンドを入力するだけですが…

```
aws cloudformation deploy --template ./template.yaml --stack-name standard-vpc --profile admin
```
一応コマンドの簡単な説明は以下のとおりです。

- aws cloudformation deploy<br>
変更セットを作成し、その変更セットを実行してから停止する。今回の実行結果の予測が既についており、とっとと反映させたい場合`deploy`コマンドを利用するのが便利そう。

- --template ./template.yaml<br>
「どのテンプレートを利用して、変更セットを作成するか」を指定する。

- --stack-name standard-vpc<br>
今回作成するスタックの名称を指定する。

- --profile admin<br>
利用するプロファイル情報を指定する。デフォルト以外のプロファイル情報を利用する場合はこのオプションを利用して切り替える。ベストプラクティス的には最小権限で行うべきなんでしょうが、今回は個人的な遊びだからってことで…

デプロイが完了したら、コンソールでスタックが作成され、変更セットの実行が成功しているのを確認。

<img width="871" alt="スクリーンショット 2020-02-16 15.37.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586524/ca4bc670-b049-da66-b959-faa17376b6eb.png">


# 作成したインスタンスへの、SSH接続
最後に今回作成したインスタンスへちゃんと接続できるかを検証します。SSH接続のコマンドは以下の通り。

```
ssh -i keypair.pem ec2-user@{dns_name}
```
<img width="869" alt="スクリーンショット 2020-02-16 15.50.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586524/fc37c22b-8b2b-0f3f-cc2d-3e539ce8dcdd.png">


# 終わりに
今回は「インスタンスへのSSH接続ができる環境」を`CloudFormation`で作成してみました。
必要なリソース自体はコンソールで作成する場合と同じなのですが、「インタネットゲートウェイのVPCへのアタッチ」等リソースに対する操作をどう`CloudFormation`で表現すればいいかにすごく悩みました。

ただ、これで簡単に一つの環境をいつでも作れるようになったことを考えると、やっぱり`CloudFormation`は必須なんですねぇ…

今後はこれをベースに継ぎ足していきたいと思います。

※タグがあったりなかったり、組み込み関数を利用したりしなかったり等、纏まりがなくすいません…