Cloudformationの!Refについて深堀してみた

## はじめに
前々からCloudformationをやろうやろう思っていたのですが、`!Ref`の正体がいまいち理解できずにあと伸ばししていました。
先日幸いにも、有識者の方と会話する機会があり、`!Ref`について整理できましたので、まとめてみたいと思います。

## 今回使用するテンプレートの説明
以下のようなテンプレートを使用していきます。
あくまでVPCとサブネットを作成するだけです。EC2関連は別途記事にできればと思っております。

```yaml
Description:  This template deploys a VPC, with a pair of public and private subnets spread
  across two Availability Zones. It deploys an internet gateway, with a default
  route on the public subnets. It deploys a pair of NAT gateways (one in each AZ),
  and default routes for them in the private subnets.

Parameters:
  EnvironmentName:
    Description: An environment name that is prefixed to resource names
    Type: String

  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16

  PublicSubnet1CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.192.10.0/24

  PublicSubnet2CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone
    Type: String
    Default: 10.192.11.0/24



  PublicSubnet3CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone
    Type: String
    Default: 10.192.12.0/24



  PrivateSubnet1CIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone
    Type: String
    Default: 10.192.20.0/24

  PrivateSubnet2CIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone
    Type: String
    Default: 10.192.21.0/24


  PrivateSubnet3CIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone
    Type: String
    Default: 10.192.22.0/24


Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-ane1-vpc-vpc01


  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ1)

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs  '' ]
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ2)

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs  '' ]
      CidrBlock: !Ref PrivateSubnet1CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Subnet (AZ1)

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs  '' ]
      CidrBlock: !Ref PrivateSubnet2CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Subnet (AZ2)



Outputs:
  VPC:
    Description: A reference to the created VPC
    Value: !Ref VPC

  PublicSubnets:
    Description: A list of the public subnets
    Value: !Join [ ",", [ !Ref PublicSubnet1, !Ref PublicSubnet2 ]]

  PrivateSubnets:
    Description: A list of the private subnets
    Value: !Join [ ",", [ !Ref PrivateSubnet1, !Ref PrivateSubnet2 ]]

  PublicSubnet1:
    Description: A reference to the public subnet in the 1st Availability Zone
    Value: !Ref PublicSubnet1

  PublicSubnet2:
    Description: A reference to the public subnet in the 2nd Availability Zone
    Value: !Ref PublicSubnet2

  PrivateSubnet1:
    Description: A reference to the private subnet in the 1st Availability Zone
    Value: !Ref PrivateSubnet1

  PrivateSubnet2:
    Description: A reference to the private subnet in the 2nd Availability Zone
    Value: !Ref PrivateSubnet2
```
### parameterとは？
要するに、変数定義しています。

```yaml
Parameters:
  EnvironmentName:
    Description: An environment name that is prefixed to resource names
    Type: String

  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16

  PublicSubnet1CIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.192.10.0/24

```
YAMLファイルですので、コロンの後にスペースを入れて値を入れることになります。
EnviromentNameに関しては、値が空白となっていますので、変数には何も値が入りません。

その下のVpcCIDRに関しては、値は空白ですが、Defaultに値が入っています。これは、値が空白だったら、Defaultを入れてねということなので、**VpcCIDRという変数には10.192.0.0/16の値が定義されている**ことになります。

ここで定義した変数は、このYAMLファイル内では好きに使うことが出来ます。


### Resourcesとは？
実際に作成するAWSリソースのことを指します。
今回は以下のAWSリソースを作成します。
・VPC
・PublicSubnet1~2
・PrivateSubnet1~2

```yaml
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-ane1-vpc-vpc01


  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ1)

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs  '' ]
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ2)

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs  '' ]
      CidrBlock: !Ref PrivateSubnet1CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Subnet (AZ1)

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs  '' ]
      CidrBlock: !Ref PrivateSubnet2CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Private Subnet (AZ2)

```


VPCについての記載を見ると、**!Ref**がいます。
これは、**さっきParameterで定義した変数を持ってきてね**という意味になりますので、
**このVPCのCiderBlockは10.192.0.0/16になるよ**と読み解くことが出来ます。

```yaml
  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16
```

また、**!Sub** という記号もあります。
こちらも基本的には!Refと同じなのですが、!Subの場合は文字列と組み合わせることが出来るのが特徴です。

つまり以下のValueには**EnvironmentNameで定義した文字列-ane1-vpc-vcp01がはいる**ことになります。

```yaml
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName}-ane1-vpc-vpc01
```

こちら公式の参考です。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html

注意が必要なのは、以下の!Refの使い方だと思っています。

```yaml
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${EnvironmentName} Public Subnet (AZ1)
```
VpcIDの値が !Ref VPC となっています。しかしVPCという変数は明示的に定義していません。
実はこの!Ref VPCは**VPCのID**を指します。!Ref リソース名　だとそのリソースのIDを取得することが多いですね。
AWSには、今回のようなユーザが定義しなくても使用できる関数がいくらかあります。
以下参考です。



https://dev.classmethod.jp/articles/lim-cloudformation-function-1/

https://dev.classmethod.jp/articles/lim-cloudformation-function-2/


### Outputsとは？

```yaml
Outputs:
  VPC:
    Description: A reference to the created VPC
    Value: !Ref VPC

  PublicSubnets:
    Description: A list of the public subnets
    Value: !Join [ ",", [ !Ref PublicSubnet1, !Ref PublicSubnet2 ]]

  PrivateSubnets:
    Description: A list of the private subnets
    Value: !Join [ ",", [ !Ref PrivateSubnet1, !Ref PrivateSubnet2 ]]

  PublicSubnet1:
    Description: A reference to the public subnet in the 1st Availability Zone
    Value: !Ref PublicSubnet1

  PublicSubnet2:
    Description: A reference to the public subnet in the 2nd Availability Zone
    Value: !Ref PublicSubnet2

  PrivateSubnet1:
    Description: A reference to the private subnet in the 1st Availability Zone
    Value: !Ref PrivateSubnet1

  PrivateSubnet2:
    Description: A reference to the private subnet in the 2nd Availability Zone
    Value: !Ref PrivateSubnet2
```
作成したリソースのIDなどが、まとまって確認出来たら便利ですよね。
Outputsを設定すると、コンソール上から確認することが出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2666121/bbf0e780-e4ae-fb15-bebe-fa1ddb01f623.png)

こちら参考です。

https://dev.classmethod.jp/articles/cloudformation-firstcontact/


## ベストプラクティス
AWSでは、一つのスタックにすべてのリソースのまとめるのではなく、リソースのカテゴリー（ネットワーク/アプリケーション/DB など）で分けることが推奨されています。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html

その際に必要なのが**クロススタック**という考え方で**Fn::ImportValue**関数が使用されます。
これは、YAMLファイル内で定義した変数を複数のYAMLファイルで使用することを指します。

こちらについては別途記事に出来れば思いますので、詳細は以下をご確認ください。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html

## CLIを使った作成のコツ
Cloudformatoinのテンプレート作成は、なかなか時間がかかります。
そこで

①コンソール上で実際に作成したいリソースを作成
➁CLIを使用して、必要な値を確認する
③CLIで取得した値をもとに、テンプレートを作成する

くらいの気持ちでやると、サクサク進むのではないでしょうか。

## 終わりに
かなり雑でしたが、個人的に理解に時間がかかった!Refについて深堀しました。
今後のリソース作成も、徐々にコンソールからCloudformatoinへと切り替えていければと思います。