AWS CloudFormation でミニマムな個人開発環境を構築する

# はじめに
CloudFormationを使用するとインフラ環境をコードとしてテンプレート化できるので、開発環境をサクッと構築することができます。
そこで、キャッチアップがてら、個人開発環境をCloudformationを使用してミニマムな構成で作ってみたいと思います！

今回構築する環境は↓のような環境。

![スクリーンショット 2022-03-12 20.15.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/51cb2ddb-de03-332a-0cba-829b2ef160ca.png)

---
ざっくり要件はこんな感じに設定しました。
 【EC2】
- OS : AmzonLinux2
- 踏み台サーバはsshのみ許可
- 踏み台サーバとNatGatewayへのEIPの割り当てはテンプレートファイルに記載し自動で割り当て。


【RDS】
- エンジン : MySQL 5.7
- マルチAZ構成
- サブネットグループは手動で設定
- パラメータグループは自動割り当て

※この辺は各自のやりたい要件に合わせて柔軟にテンプレートファイルを変更してみてください!
私は基本的に環境構築用にしか使用しないので、何も指定せずデフォルト設定のまま行いました。

# 実装
## スタック分割

`AWS公式BlackBelt`にもある通り、テンプレートファイルは膨大な記述量になりがちです。

そこで、可読性が下がるのを防ぐためにも、スタックはレイヤーごとに分割するほうが望ましいとされています。

これに則って、今回は、`Network , Security, Application`という3つのスタックに分割して行いました。
（それぞれテンプレートファイルは、`network.yml`, `security.yml`, `application.yml`を使用）

![スタック分割01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/e577e225-f4db-fab8-9714-686258049b63.png)

詳しくは↓`AWS公式BlackBelt`のスライド85ページ目・86ページ目を参照

https://d1.awsstatic.com/webinars/jp/pdf/services/20200826_AWS-BlackBelt_AWS-CloudFormation.pdf

## テンプレートファイル作成

今回、作成したテンプレートファイルはご自由にコピーして使っていただいて構いません！
各自に合わせて入力する必要がある箇所は、テンプレートファイルに`XXXXX`で記載しているので、そこだけ自分用に変えていただけたらと思います。

### ■ network.yml
InternetGateway, NatGateway, VPC, Subnet, RouteTableなどを記載
```yml:network.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Personal Development Environment CloudFormation
Parameters:
  AvailabilityZone:
    Type: AWS::EC2::AvailabilityZone::Name
    Default: "ap-northeast-1a"
Resources:
  # ------------------------------------------------------------#
  #  VPC
  # ------------------------------------------------------------#
  DevelopVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: DevelopVPC
  # ------------------------------------------------------------#
  #  Internet Gateway
  # ------------------------------------------------------------#
  DevelopIGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: DevelopIGW
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref DevelopVPC
      InternetGatewayId: !Ref DevelopIGW
  # ------------------------------------------------------------#
  #  Public Subnet Route Table
  # ------------------------------------------------------------#
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    # DependsOn:特定のリソースが他のリソースに続けて作成されるように指定
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref DevelopVPC
      Tags:
        - Key: Name
          Value: PublicRouteTable
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref DevelopIGW
  # ------------------------------------------------------------#
  #  Private Subnet Route Table
  # ------------------------------------------------------------#
  PrivateRouteTable01:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref DevelopVPC
      Tags:
        - Key: Name
          Value: PrivateRouteTable01
  PrivateRoute01:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PrivateRouteTable01
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref DevelopNat
  # PrivateSubnet02に関連づける
  # RDSにインターネットから何かインストールするときに使用
  PrivateRouteTable02:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref DevelopVPC
      Tags:
        - Key: Name
          Value: PrivateRouteTable02
  PrivateRoute02:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PrivateRouteTable02
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref DevelopNat
  # ------------------------------------------------------------#
  #  Pubulic Subnet 01
  # ------------------------------------------------------------#
  PublicSubnet01:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref DevelopVPC
      CidrBlock: 10.0.10.0/24
      Tags:
        - Key: Name
          Value: PublicSubnet01
  PublicRouteTableAssocName:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet01
      RouteTableId: !Ref PublicRouteTable
  # ------------------------------------------------------------#
  #  Private Subnet 01
  # ------------------------------------------------------------#
  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref DevelopVPC
      CidrBlock: 10.0.20.0/24
      Tags:
        - Key: Name
          Value: PrivateSubnet01
  PrivateRouteTable01AssocName:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet01
      RouteTableId: !Ref PrivateRouteTable01
  # ------------------------------------------------------------#
  #  Private Subnet 02
  # ------------------------------------------------------------#
  PrivateSubnet02:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      VpcId: !Ref DevelopVPC
      CidrBlock: 10.0.30.0/24
      Tags:
        - Key: Name
          Value: PrivateSubnet02
  PrivateRouteTable02AssocName:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet02
      RouteTableId: !Ref PrivateRouteTable02
  # ------------------------------------------------------------#
  #  Private Subnet 03
  #  RDSのSubnetGroup用のサブネット
  # ------------------------------------------------------------#
  PrivateSubnet03:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      VpcId: !Ref DevelopVPC
      CidrBlock: 10.0.40.0/24
      Tags:
        - Key: Name
          Value: PrivateSubnet03
  # ------------------------------------------------------------#
  #  Nat Gateway
  # ------------------------------------------------------------#
  DevelopNat:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt DevelopNatEIP.AllocationId
      SubnetId: !Ref PublicSubnet01
      Tags:
      - Key: Name
        Value: DevelopNat
  DevelopNatEIP:
    DependsOn: AttachGateway
    Type: AWS::EC2::EIP
    Properties:
        Domain: vpc
        Tags:
          - Key: Name
            Value: DevelopNatEIP

Outputs:
  DevelopVPC:
    Value: !Ref DevelopVPC
    Export: 
      Name: DevelopVPCName
  DevelopVPCCidr:
    Value: !GetAtt DevelopVPC.CidrBlock
    Export: 
      Name: DevelopVPCCidrBlock
  PuclicSubnet01:
    Value: !Ref PublicSubnet01
    Export: 
      Name: PublicSubnet01Name
  PrivateSubnet01:
    Value: !Ref PrivateSubnet01
    Export: 
      Name: PrivateSubnet01Name
  PrivateSubnet03:
    Value: !Ref PrivateSubnet03
    Export: 
      Name: PrivateSubnet03Name
```

### ■ security.yml
セキュリティグループなどを記載
```yml:security.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Personal Development Environment CloudFormation

Resources: 
  # ------------------------------------------------------------#
  #  Bastion Security Group
  # ------------------------------------------------------------#
  BastionSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: Bastion-SG
      GroupDescription: Bastion-SG
      VpcId: !ImportValue DevelopVPCName
      SecurityGroupIngress:
        IpProtocol: tcp
        CidrIp: 0.0.0.0/0
        FromPort: 22
        ToPort: 22
      Tags:
        - Key: Name
          Value: Bastion-SG
  # ------------------------------------------------------------#
  #  Web Security Group
  # ------------------------------------------------------------#
  WebSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: Web-SG
      GroupDescription: Web-SG
      VpcId: !ImportValue DevelopVPCName
      SecurityGroupIngress:
        - IpProtocol: tcp
          CidrIp: !ImportValue DevelopVPCCidrBlock
          FromPort: 22
          ToPort: 22
        - IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          FromPort: 80
          ToPort: 80
        - IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          FromPort: 443
          ToPort: 443
      Tags:
        - Key: Name
          Value: Web-SG
  # ------------------------------------------------------------#
  #  Application Database Security Group
  # ------------------------------------------------------------#
  ApplicationDatabaseSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: DB-SG
      GroupDescription: DB-SG
      VpcId: !ImportValue DevelopVPCName
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !ImportValue DevelopVPCCidrBlock
      Tags:
        - Key: Name
          Value: DevelopApplicationDB-SG

Outputs:
  BastionSecurityGroup:
      Value: !Ref BastionSecurityGroup
      Export: 
        Name: BastionSecurityGroupName
  WebSecurityGroup:
      Value: !Ref WebSecurityGroup
      Export: 
        Name: WebSecurityGroupName
  ApplicationDBSecurityGroup:
    Value: !Ref ApplicationDatabaseSecurityGroup
    Export: 
      Name: ApplicationDBSecurityGroupName
```

### ■ application.yml
EC2, RDS, ElasticIPなどを記載
```yml:application.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  KeyName:
    Description: Development Kye Pair
    Type: "AWS::EC2::KeyPair::KeyName"
    Default: XXXXX
  EC2AMIId:
    Description: AMI ID
    Type : String
    Default: ami-07b4f72c4c356c19d  
  # RDSパラメータ
  UserName:
    Description: RDS User Name
    Type : String
    Default: XXXXX
  UserPassword:
    Description: RDS User Password
    Type : String
    Default: XXXXX
  DBIdentifier:
    Description: DBInstanceIdentifier
    Type : String
    Default: XXXXX


Resources:
  # ------------------------------------------------------------#
  #  Bastion EC2 Instance
  # ------------------------------------------------------------#
  BastionEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: !Ref KeyName
      ImageId: !Ref EC2AMIId
      InstanceType: t2.micro
      Monitoring: false
      SecurityGroupIds:
        -  !ImportValue BastionSecurityGroupName
      SubnetId: !ImportValue PublicSubnet01Name
      Tags:
        - Key: Name
          Value: Bastion
  # ------------------------------------------------------------#
  #  Web EC2 Instance
  # ------------------------------------------------------------#
  WebEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: !Ref KeyName
      ImageId: !Ref EC2AMIId
      InstanceType: t2.micro
      Monitoring: false
      SecurityGroupIds:
        -  !ImportValue WebSecurityGroupName
      SubnetId: !ImportValue PrivateSubnet01Name
      Tags:
        - Key: Name
          Value: Develop-Web01
  # ------------------------------------------------------------#
  #  Elastic IP Address
  # ------------------------------------------------------------#
  BastionEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      InstanceId: !Ref BastionEC2Instance
  ElasticIPAssociate:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: !GetAtt BastionEIP.AllocationId
      InstanceId: !Ref BastionEC2Instance
  # ------------------------------------------------------------#
  #  RDS
  # ------------------------------------------------------------#
  ApplicationDatabase:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: MySQL
      EngineVersion: 5.7
      DBInstanceClass: db.t2.micro
      AllocatedStorage: 10
      StorageType: gp2
      MasterUsername: !Ref UserName
      MasterUserPassword: !Ref UserPassword
      DBName: DevelopRDS
      DBInstanceIdentifier: !Ref DBIdentifier
      VPCSecurityGroups:
        - !ImportValue ApplicationDBSecurityGroupName
      DBSubnetGroupName: !Ref ApplicationDatabaseSubnetGroup
      MultiAZ: "true"
      Tags:
        - Key: Name
          Value: DevelopApplicationDB
  ApplicationDatabaseSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Application Database Subnet Group
      SubnetIds: 
        - !ImportValue PrivateSubnet01Name
        - !ImportValue PrivateSubnet03Name
      Tags:
        - Key: Name
          Value: DevelopDBSubnetGroup
```

## スタックを作成する順番が重要!
テンプレートファイルが作成できたら、あとはスタックを作成するだけです。

**※ここで注意点！**

スタックは、**`Netowork` → `Security` → `Application`の順番でStackを作成しないといけない!**

いったい、なぜか？

`security.yml`は`network.yml`のOutputsを参照しているから
`application.yml`は`security.yml`のOutputsを参照しているから


つまり、`Network`の出力を`Security`で受け取り、`Security`の出力を`Application`が受け取っているのである

完全に呪文ですね。
テンプレートファイルの中身をみると感覚的に理解できるかと思います。

--- 
スタックの作成は、`Cloudformationのマネジメントコンソール` > `スタックの作成` > `テンプレートファイルのアップロード`で作成した`XXXXX.yml`を選択すればOK

![スクリーンショット 2022-03-13 8.03.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/be33c6e9-c9ff-d84e-974c-a7bd4e514362.png)


![スクリーンショット 2022-03-13 7.58.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/79127c7e-0cef-f747-cd71-5bdb17727453.png)


# 確認
全てのスタックが作成完了したところで、本当に環境構築できているのか確認してみます。

※事前に、キーペアを作成し、ローカルの`~/.ssh`配下に設置しておきます。

## 1.`scp`コマンドでローカルマシンから踏み台サーバへ鍵を送信 
```sh:ローカルマシン
$ scp ~/.ssh/秘密鍵.pem ec2-user@踏み台サーバのEIP:~/.ssh
```

## 2.`/.ssh/config`にssh情報を追記
```config:/Users/ユーザー名/.ssh/config
#ssh接続先情報を追記
Host bastion
  #接続先サーバーのIPアドレス
  Hostname 踏み台サーバのEIP
  #接続するサーバーのユーザー名
  User ec2-user
  Port 22
  IdentityFile /Users/ユーザー名/.ssh/秘密鍵.pem

Host Web
  #接続先サーバーのIPアドレス
  Hostname プライベートIP
  User ec2-user
  Port 22
  IdentityFile /Users/ユーザー名/.ssh/秘密鍵.pem
  #開発サーバーは踏み台サーバーをプロキシして接続する
  ProxyCommand ssh -W %h:%p bastion
```


## 3.踏み台サーバーへssh接続
```sh:ローカル
$ ssh -i ~/.ssh/秘密鍵.pem ec2-user@踏み台サーバのEIP
```



## 4.開発Webサーバーへssh接続
```sh:bastion
$ ssh -i ~/.ssh/秘密鍵.pem ec2-user@開発WebサーバのプライベートIP
```


## 5.開発WebサーバにMySQLクライアントのインストール
開発Webサーバにssh接続できたら、DBへ接続するため、MySQLクライアントを↓の手順でインストール

https://blog.kasei-san.com/entry/2021/04/21/180531

途中でコケてしまった・・・・
![スクリーンショット 2022-03-12 20.39.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/f33b2a7f-6962-ac10-5e84-d84bcc59acbf.png)

調べてみると・・・

https://blog.katsubemakito.net/mysql/mysql-update-error-gpg

無事成功🙌
```sh
$ mysql --version
=> mysql  Ver 14.14 Distrib 5.7.37, for Linux (x86_64) using  EditLine wrapper
```


## 6.RDSのmysqlに接続
```sh:開発Webサーバ
$ mysql -u マスターユーザー名 -pマスターパスワード -h RDSエンドポイント
```

![スクリーンショット 2022-03-12 20.58.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/c2c4a136-f334-8931-82c6-c59573cca8aa.png)

無事、踏み台サーバからWebサーバ、DBまで全ての接続を確認することができた！


# さいごに
Cloudfomationを使用するとインフラのコード化でき、テンプレートとして登録できる点はやはり素晴らしいと感じました！

やってみて感じたことは、`application.yml`内でも起点となる踏み台サーバとそれ以外のリソースで分割したほうが良いなと感じました。

踏み台サーバの部分だけ別のスタックで実装しておき、踏み台サーバ上にテンプレートファイルを設置し、踏み台サーバー以外の環境をaws cliでコマンドで操作（↓のような感じに)できたほうが便利だと感じたので次回はそのような構成でチャレンジしたいと思います

```sh
$ aws cloudfomation deploy --template-file XXXXX --stack-name XXXXX
```

また、ELBの設置やEC2のAutoScaling、本番環境とのVPCピアリングなど、実際のWebアプリケーションを駆動させるインフラ環境として必要な部分もやってみたいと思います。

# 参考
https://qiita.com/ktamido/items/38b9fedd5bd92985b3d9#-ssh%E3%81%AE%E8%A8%AD%E5%AE%9A

https://dev.classmethod.jp/articles/cloudformation-tips-focused-on-refs/

https://zenn.dev/soshimiyamoto/articles/3c624728438902#outputs%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6%E3%80%81%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E3%81%94%E3%81%A8%E3%81%AB%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%92%E5%88%86%E3%81%91%E3%82%8B

https://d1.awsstatic.com/webinars/jp/pdf/services/20200826_AWS-BlackBelt_AWS-CloudFormation.pdf