AWS初学者がCloudFormation使ってインフラを自動構築に挑戦してみたの話

# インフラをコード化して自動構築に挑戦してみました！
なんとインフラの構築をコード化できてて、再利用性が高く面倒臭い設定が
ぽちぽちっと一気にできてしまう方法を共有します！
**Terraformではありません。** ←ものすごく気になりできるようになりたいがまずは転職成功から！
転職成功したら勉強したいですね!
本当にインフラコード化楽なので、一度しっかりとした構築をコードで残せば再利用できるので

気楽に構築を**破壊**できて気が楽です

### 今回はCloudFormationを使用しています

基本的にコストの低いものと必要最低限なもので
PF作成など未経験転職を目指している方向けです

余談↓

NATゲートウェイやVPCエンドポイント使ったら、、恐ろしくて放置できませんw

### どこまでコード化したのか

- **VPC**
- **Internet Gateway**
- **Public Route Table**
- **Public Subnet**
- **Private Route Table**
- **Private Subnet**
- **Security Group**

あとはVPCをIGWにアタッチや**Public Route TableとIGWをアタッチ**
あとは各Route Tableをそれぞれのsubnetとassociationなど
必須で細かいところもやってます

## できるインフラ構成図

![インフラ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124781/1f682406-e640-706e-6552-61f1df0f4fc8.png)

雛形みたいなものですね！
自動化してみよう！

## コード説明

`cloudformation-vpc.yml`でymlファイルに書いていきます

### まず最初に
```yml
# CloudFormationテンプレートのバージョンを示します。
AWSTemplateFormatVersion: "2010-09-09" 
# テンプレートの概要を記述します,名前みたいなものです
Description: vpc construction template
```

### VPCとそれぞれのサブネットのサイダー(CIDR)を設定する
```yml
# キーと値のマッピングを作成,この例ではVPCやサブネットのCIDRブロックが定義してます。
Mappings:
  StackConfig:
    VPC:
      CIDR: 10.1.0.0/16
    PublicSubnet1:
      CIDR: 10.1.0.0/24
    PublicSubnet2:
      CIDR: 10.1.1.0/24
    PrivateSubnet1:
      CIDR: 10.1.2.0/24
    PrivateSubnet2:
      CIDR: 10.1.3.0/24
```
後々の設定で直接CIDRを設定するとタイポミスなどが起きてしまう可能性があるからです
このように設定してあげるとミスが減ります。コードを変数に代入するイメージと一緒ですね！

### ここから構築スタートまずはVPCから！
**`Resources`**: AWSリソースを定義します！
ここからネストして各それぞれのサービスの設定をしていきます
なので次からのブロックコードはResourcesを書きませんがネストしていると思ってください！

```yml
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [StackConfig, VPC, CIDR]
      EnableDnsSupport: true # DNS サポートを有効化
      EnableDnsHostnames: true # DNS ホスト名を有効化
      Tags:
        - Key: Name
          Value: sample-vpc # VPC に名前タグを付ける
```
`Type: AWS::EC2::VPC`AWSのリソースタイプはVPC
`Properties`は設定を定義する感じです
`CidrBlock: !FindInMap [StackConfig, VPC, CIDR]`この部分は
Mappingsで設定した、StackConfigのVPCのCIDRなので、、、
CidrBlockには`10.1.0.0/16`が入ります。
次にタグでキーが名前値が自分が名付けたいvpcの名前を入れてください。

### 次インターネットゲートウェイを作成します
```yml
InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: vpc-igw
```
ここはそのままですね、IGWを作成し`sample-vpc-igw`と名付けています

### IGWを作成したVPCとアタッチします
```yml
AttachGateway:
# VPC ゲートウェイアタッチメント。上記のVPCとインターネットゲートウェイを接続します
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC # 作成した VPC への参照
      InternetGatewayId: !Ref InternetGateway # 作成した IGW への参照
```
`VpcId: !Ref VPC`これは上記で作成したVPCIDを返します。
`InternetGatewayId: !Ref InternetGateway`作成した IGWのIDを返す
この二行でインターネットゲートウェイはVPCとアタッチできます

:::note
!Refを使用して今回はVPCリソースIDを参照しました

ルートテーブルやサブネット、セキュリティグループを特定のVPCに関連付ける際にも使用します
:::

### 公開のルートテーブルを作成
```yml
PublicRouteTable:
    Type: AWS::EC2::RouteTable
    DependsOn: AttachGateway
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: public-route-table
```
`DependsOn: AttachGateway`
これはIGWがVPCにアタッチされた後に作成してくださいという意味です。

ここでもきました！`VpcId: !Ref VPC`です
これはこの公開ルートテーブルが関連づけられるVPCのIDを指定しています
あとは名前で`public-route-table`としています

### IGWと公開するルートテーブルのルートを追加する
```yml
PublicRoute:
    Type: AWS::EC2::Route # 公開ルートのリソースを定義します。
    DependsOn: AttachGateway # インターネットゲートウェイがVPCにアタッチされた後に作成されます。
    Properties:
      RouteTableId: !Ref PublicRouteTable # このルートが追加されるルートテーブルのIDを指定します。
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway # このルートのターゲットとなるインターネットゲートウェイを指定します。
```
先ほどと似たようなところはコメントアウトでまとめました

`DestinationCidrBlock: 0.0.0.0/0`これは
このルートの宛先CIDRを指定します。ここでは全てのIPアドレスを意味します

### 次にパブリックサブネット達を作成
```yml
PublicSubnet1:
    Type: AWS::EC2::Subnet # 公開サブネットのリソースを定義します。
    DependsOn: AttachGateway # インターネットゲートウェイがVPCにアタッチされた後に作成されます。
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      VpcId: !Ref VPC # このサブネットが関連付けられるVPCのIDを指定します。
      CidrBlock: !FindInMap [StackConfig, PublicSubnet1, CIDR]
      Tags:
        - Key: Name
          Value: Public Subnet a
```

`AvailabilityZone: "ap-northeast-1a"`これはサブネットが配置されるAZを指定してます

久々に来ました`CidrBlock: !FindInMap [StackConfig, PublicSubnet1, CIDR]`
これはVPCのサイダーを指定する時に出ましたね
`10.1.0.0/24`を意味します。
名前はPublic Subnet aと名付けました

### 先程のサブネットを公開ルートテーブルに紐づけています
```yml
PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation # サブネットとルートテーブルの関連付けを定義します。
    Properties:
      SubnetId: !Ref PublicSubnet1 # 関連付けられるサブネットのIDを指定します。
      RouteTableId: !Ref PublicRouteTable # 関連付けられるルートテーブルのIDを指定します。
```
公開ルートテーブルに先ほど作った`Public Subnet a`を紐付け（関連づけ）ています

### 同じ感じでもう一つのパブリックサブネットを作成
```yml
PublicSubnet2:
    Type: AWS::EC2::Subnet
    DependsOn: AttachGateway
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [StackConfig, PublicSubnet2, CIDR]
      Tags:
        - Key: Name
          Value: Public Subnet c
  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable
```

まとめてどん！

変更点はAZをaからcへサイダーもPublicSubnet2になっています
名前はPublic Subnet cです
関連づけはPublicSubnet2も同じ公開ルートテーブルに関連づけています

### 次にプライベートサブネットです大体同じ感じなのでまとめます

```yml
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: private-route-table
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: "ap-northeast-1a"
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [StackConfig, PrivateSubnet1, CIDR]
      Tags:
        - Key: Name
          Value: Private Subnet b
  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: "ap-northeast-1c"
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [StackConfig, PrivateSubnet2, CIDR]
      Tags:
        - Key: Name
          Value: Private Subnet d
  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
          RouteTableId: !Ref PrivateRouteTable
```

### ルートテーブル
作成したVPCにprivate-route-tableと名付け
非公開ルートテーブルを作成

### サブネット
これらも作成したVPCの中に作ってます

PrivateSubnet1: Private Subnet bと名付け
AZは東京リージョンのaでサイダーもそれぞれ違うものを指定

PrivateSubnet2： Private Subnet dと名付け
AZは東京リージョンのcでサイダーもそれぞれ違うものを指定

### 作ったサブネットを非公開ルートテーブルと関連づけ
```yml
PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable
```
PrivateSubnet1はPrivateRouteTableと関連づけできたなと
みた感じそのまんまのイメージでいいと思います。

ちなみに非公開なのでIGWとはルートはつなげません。


### 最後にセキュリティグループ作成
まずはパブリックな方から

```yml
PublicSecurityGroup:
    Type: AWS::EC2::SecurityGroup # 公開セキュリティグループのリソースを定義します。
    Properties:
      GroupDescription: Allow SSH snd HTTP and HTTPS # グループの説明を設定します。
      VpcId: !Ref VPC # このセキュリティグループが関連付けられるVPCのIDを指定します。
      SecurityGroupIngress: # セキュリティグループのインバウンドルールを定義します。
        - IpProtocol: tcp # プロトコルを指定します。ここではTCPを指定しています。
          FromPort: 22 # 開始ポート番号を指定します。ここでは22（SSH）を指定しています。
          ToPort: 22 # 終了ポート番号を指定します。ここでは22（SSH）を指定しています。
          CidrIp: 0.0.0.0/0 # 許可する送信元CIDR IPアドレス範囲を指定します。ここでは全てのIPアドレスを意味します。
        - IpProtocol: tcp
          FromPort: 80 # HTTP（80）を許可します。
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443 # HTTPS（443）を許可します。
          ToPort: 443
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 3000 # ポート3000を許可します。
          ToPort: 3000
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: public-sg # 名前
```

ブロックコードでコメントアウトで書いた方が自分も理解が深まる気がする

### RDS用のセキュリティグループ
```yml
RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup # RDS用のセキュリティグループを定義します。
    Properties:
      GroupDescription: Allow Postgres SQL from ECS only # グループの説明を設定します。
      VpcId: !Ref VPC # このセキュリティグループが関連付けられるVPCのIDを指定します。
      SecurityGroupIngress: # セキュリティグループのインバウンドルールを定義します。
        - IpProtocol: tcp # プロトコルを指定します。ここではTCPを指定しています。
          FromPort: 5432 # 開始ポート番号を指定します。ここでは5432（PostgreSQL）を指定しています。
          ToPort: 5432 # 終了ポート番号を指定します。ここでは5432（PostgreSQL）を指定しています。
          SourceSecurityGroupId: !GetAtt PublicSecurityGroup.GroupId # 許可する送信元セキュリティグループを指定します。ここでは公開セキュリティグループを指定しています。
      Tags:
        - Key: Name
          Value:rds-sg # 名前
```

DB置き場なのでここでは接続許可できるのは自分のアプリのみとなります。
外部からの通信はできないようにしています

### これにて必要最低限の設定をコード化しました！

次はどうやってこのファイルを使うのかを紹介します。

AWSのコンソールへ行ってCloudFormationと検索

![スクリーンショット 2023-07-04 11.58.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124781/be99bc83-b47d-800f-4797-66c3adc9f980.png)
このマークです

スタック作成でymlファイルを選択してください。
![スクリーンショット 2023-07-04 12.00.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124781/1180f7e6-21a4-e00e-44c8-b14f71080eee.png)

デザイナーで表示するとこんな感じの図が

![スクリーンショット 2023-07-04 12.02.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124781/1d907973-ba85-0177-94df-d3bd3c65cc4e.png)

なんもいじってない状態です。この図でインフラ構成図を理解するには僕にはまだ早かったようです

次にスタック名はお好きにどうぞ
他は何もいじってません
送信ボタンを押したらできます

![スクリーンショット 2023-07-04 12.06.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124781/0501256a-f8cc-ced7-4471-55db760cfeb0.png)

こんな感じでステータスが緑色でcreate_completeになればおk！！

VPC周りが自動で構築してくれています

同じような設定を何度もするのはめんどくさいですし
このように一回作ったりテンプレができれば名前とかサイダーの変更などひょいひょいっと変更するだけで
あとは自動でやってくれます。

終わります！
お疲れ様でした！！！

### 余談
実はRDSとECS(クラスター、タスク、サービス)も自動化したのですが細かいところの設定がわからなく諦めました
コストの高い部分ですし慎重にいきたい部分なので手動がいいかなと。

一様挑戦したコード達
ECS編
```yml
Resources:
  MyLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: "/ecs/real_world"
  MyCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: real_world_ecs 

  MyTaskDefinition:
    Type: AWS::ECS::TaskDefinition 
    Properties:
      Family: real-world-api
      Cpu: '256' 
      Memory: '512' 
      NetworkMode: awsvpc 
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole"
      ContainerDefinitions: 
        - Name: rails-api
          Image: ここにECRでpushしたイメージのURIが入る
          Memory: 128
          PortMappings:
            - ContainerPort: 3000
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/real_world 
              awslogs-region: ap-northeast-1 
              awslogs-stream-prefix: ecs 
    MyECSService: 
        Type: "AWS::ECS::Service"
        Properties: 
          Cluster: !Ref MyCluster 
          TaskDefinition: !Ref MyTaskDefinition 
          DesiredCount: 1 
          LaunchType: FARGATE
          NetworkConfiguration:
            AwsvpcConfiguration: 
              AssignPublicIp: ENABLED 
              SecurityGroups: 
                - !Ref PublicSecurityGroup
              Subnets:
                - !Ref PublicSubnet1
                - !Ref PublicSubnet2
```

RDS編
```yml
MyDBInstance:
  Type: "AWS::RDS::DBInstance"
  Properties:
    Engine: postgres
    EngineVersion: "14.6"
    DBInstanceIdentifier: "real-world-db"
    MasterUsername: "admin"
    MasterUserPassword: "password" 
    AllocatedStorage: "20"  
    StorageType: "gp2" 
    DBInstanceClass: "db.t3.micro"  
    VPCSecurityGroups: 
      - !Ref RDSSecurityGroup
    DBSubnetGroupName: !Ref DBSubnetGroup
    PubliclyAccessible: false
    MultiAZ: false 
    AutoMinorVersionUpgrade: false 
    BackupRetentionPeriod: 0
    DBName: "real_world"  
DBSubnetGroup: 
  Type: AWS::RDS::DBSubnetGroup
  Properties: 
    DBSubnetGroupDescription: "My DB Subnet Group"
    SubnetIds: 
      - !Ref PrivateSubnet1
      - !Ref PrivateSubnet2
```

本当はマルチAZのがいいのはわかっていますがコストが高くて。。。。。orz
細かい設定が大事だから今回は手動にしました。