入門 AWS CloudFormation #4（組み込み関数編）

インフラやクラウドの専門というわけではないのですがそろそろIaC周りもちゃんと勉強しておきたい・・・という感じなのでAWSのCloudFormationについて入門しつつ復習として記事にまとめておきます。

※とりあえず最初ということでごく基本的なところを中心に記事を書いていきます。

# 注意事項と前記事までの振り返り

本記事の処理を動かすとEC2関係などで色々と追加になったり起動したりします。その辺はリソースの停止や削除などをしないとお金がかかったりしてくる可能性があるためご注意ください。

また、本記事は4記事目となります。前回までの内容を踏襲していく形となっているのでご注意ください。

1記事目:

https://qiita.com/simonritchie/items/330391e741f394897550

2記事目:

https://qiita.com/simonritchie/items/a1922199a5b6d131dca9

3記事目:

https://qiita.com/simonritchie/items/8b1c3046474ca7c6863c

また、前回までの内容を踏まえYAMLのテンプレートは以下のような内容（EC2のごく基本的な設定）をベースに色々編集する形で進めていきます。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: t2.nano
```

# 本記事で触れること

前記事までで少しだけ触れたCloudFormationの組み込み関数についてもっと内容を把握するために全体的に各関数などに触れていきます。以下の関数を対象とします。

- `Fn::GetAtt`
- `Fn::FindInMap`
- `Fn::Join`
- `Fn::Split`
- `Fn::Select`
- `Fn::Sub`
- `Fn::GetAZs`

※組み込み関数の概要などに関しては前記事までで触れたので割愛します。

※あまり使う機会が無さそうな関数は割愛します。

※既に前の記事で触れたRef関数などは割愛します。

※後の記事で恐らく触れるかと思われる複数のスタックを使う場合やテンプレートのモジュール化をするケースなどに絡んだ関数はまだ触れません（`Fn::ImportValue`や`Fn::Transform`など）。

※条件分岐の関数は別の記事で扱います。

また、関数による値の取得結果の確認などのためにOutputsの機能を軽くだけ触れて使っていったり、Mappingsの機能に関しても関数に合わせて少し触れていきます。

# Outputs（出力）について

後の記事で詳しく触れるかもしれないため本記事では軽く触れるのみとなりますがOutputs関係についても説明しておきます（関数の値取得結果の確認などに使用します）。

OutputsというキーでYAML上で定義することで、CloudFormationのテンプレートの反映結果の値を一部出力することができます。

この出力値は確認として使えるだけでなく、他のテンプレートから参照したりすることもできます。

今回は値の確認のみで使用します。

最低限の書き方としては以下のように任意の論理名（今回は`MyFirstEC2InstanceId`としました）の指定と`Value`キーで出力したい値を指定します。

```yaml
Outputs:
  MyFirstEC2InstanceId:
    Value: !Ref MyFirstEC2Instance
```

Ref関数でEC2インスタンスの論理名を指定すると値としてはインスタンスIDとなります。

YAML全体としては以下のようになっています。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: t3.nano
Outputs:
  MyFirstEC2InstanceId:
    Value: !Ref MyFirstEC2Instance
```

試しに既存スタックに対して変更セットを作成・反映してみて挙動を確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/1b52708e-8a3c-efba-06f6-ad0423c71f39.png)

正常に処理が通りました。

Outputsの内容は出力タブに表示されるのでイベントタブから出力タブに切り替えます。更新処理完了後に出力タブでもリロードしないと結果が反映されないかもしれません。

キーにはYAML上で指定した論理名の`MyFirstEC2InstanceId`、値には対象のインスタンスのIDが設定されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/3d636c11-09c2-0872-607a-3fe0258e58ca.png)

一応EC2のインスタンスのページにアクセスしてインスタンス IDを確認してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/c138e0f6-bfbc-2364-692f-ab69527b80fe.png)

CloudFormationの出力結果のインスタンス IDと一致していることが確認できました。
本記事ではこの出力内容を確認用として使用していきます。

# `Fn::GetAtt`関数

`Fn::GetAtt`関数は任意のリソースの特定の属性の値を取得します。短縮系では`!GetAtt`となります。

書き方としては`Fn::GetAtt: [<対象リソースの論理名>, <属性名>]`もしくは`!GetAtt <対象リソースの論理名>.<属性名>`となります。

属性名に関しては以下のドキュメントで対象のサービスを選択すると表示されるページのリンクを辿っていくと確認できます。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html

たとえばEC2インスタンスのAZを参照したい・・・という場合にはAmazon EC2 → AWS::EC2::Instanceと各リンクをクリックしていって[表示されるページ](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html)内にあるReturn values節のFn::GetAtt部分の中からAZ関係を探して参照すればOKとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/18f9283b-b7fa-4fca-2de7-c258beeda910.png)

もっと色々な属性が取れるかな？と思っていたら割と内容が限られている気配があります。欲しい属性が取れるようになっているかどうかは事前に確認しておいた方が良いかもしれません。

AZは`AvailabilityZone`という属性名とドキュメントに書かれているため、YAML上では`Value: !GetAtt MyFirstEC2Instance.AvailabilityZone`という記述になります。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: t3.nano
Outputs:
  MyFirstEC2Az:
    Value: !GetAtt MyFirstEC2Instance.AvailabilityZone
```

試しに変更セットを作成して反映してみます。

更新処理が終わるまでまってから出力タブをリロードして確認すると、`ap-northeast-1d`というAZの値が取れていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/f1ca3470-6d8f-a06c-df05-f2db3260ec82.png)

# `Fn::Join`関数

`Fn::Join`関数では指定された配列の文字列の値を別途指定された区切り文字で連結します。

値はリストで指定し、リストの最初のインデックスには区切り文字、2番目のインデックスには連結したい文字列をさらにリストで指定します。

例えば`[AWS, EC2, Instance]`という文字列のリストを`::`の区切り文字で連結したい場合には`Fn::Join: ["::", [AWS, EC2, Instance]]`といった書き方になります。

短縮系の場合には`!Join ["::", [AWS, EC2, Instance]]`といった書き方となります。

試しにYAMLのOutputs部分でこの関数を使って試してみます。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: t3.nano
Outputs:
  MyResourceType:
    Value: !Join ["::", [AWS, EC2, Instance]]
```

更新などが終わったら出力タブでリロードして内容を確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/5b50f18b-dcf6-9a2a-0e3c-90a148cdee42.png)

`AWS::EC2::Instance`というリストの各文字列を`::`記号で連結した値が取得できていることが確認できました。

# `Fn::Select`関数

`Fn::Select`関数ではリストの中から特定のインデックスの値を取得します。

値はリストで指定し、リストの最初のインデックスには取得したい値のインデックス、2番目のインデックスには対象のリストを指定します。インデックスは0からスタートします。

例えば`[AWS, EC2, Instance]`というリストの中から2番目の値（`EC2`）を取得したい場合には`Fn::Select: [1, [AWS, EC2, Instance]]`という書き方になります。

短縮系の場合には`!Select [1, [AWS, EC2, Instance]]`といった書き方になります。

試しに以下のYAMLで変更セットの作成や反映を行ってみます。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: t3.nano
Outputs:
  MyString:
    Value: !Select [1, [AWS, EC2, Instance]]
```

更新が終わって出力タブでリロードすると`EC2`という値がリストの中から取れていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/056bb2f9-36e8-4f5c-3c52-ff240821f35c.png)

# `Fn::Split`関数

`Fn::Split`関数は`Fn::Join`の逆のような挙動をします。つまり特定の記号などで連結された文字列をリストに分割します。

値はリストで指定します。最初のインデックスには区切り文字、2番目のインデックスには分割したい文字列を指定します。

たとえば`AWS::EC2::Instance`という文字列を`::`という区切り文字でリストに分割したい場合には`Fn::Split: ["::", "AWS::EC2::Instance"]`といったように指定します。

短縮系の場合には`!Split ["::", "AWS::EC2::Instance"]`という書き方になります。

Outputsでの指定にはリストは設定できないため、以下の例では`Split`関数で分割してから`Select`関数で特定の値を取得して設定しています。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: t3.nano
Outputs:
  MyString:
    Value: !Select [2, !Split ["::", "AWS::EC2::Instance"]]
```

スタックの変更セットを作成・反映し、更新が終わってから出力タブの画面でリロードすると文字列分割後のリストの中でインデックスが2の`Instance`という値が取れていることを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/842dddcb-f2d5-0e39-17c0-9df884e84434.png)

# `Fn::Sub`関数

`Fn::Sub`関数では文字列内に対して別の値を挿入したりすることができます。Pythonのformatメソッドとかに近い形になります。

短縮系は`!Sub`となります。

引数には2つのインデックスを持つリストで指定します。最初のインデックスには置換対象の文字列、2つ目のインデックスには置換に指定するパラメーター名とパラメーターの辞書の値を指定します。

YAML上では以下のような感じになります。複数の置換パラメーターを指定したい場合には2つ目のインデックスに必要数分追加していきます。

```yaml
!Sub
  - <置換対象の文字列>
  - <置換で使うパラメーター名1>: <置換で使うパラメーター1>
    <置換で使うパラメーター名2>: <置換で使うパラメーター2>
    ...
```

置換対象の文字列には`${パラメーター名}`といった記述で置換したい箇所を含めておきます。
Pythonで言うと文字列中の`%s`とか`{formatで置換するパラメーター名}`とかみたいな部分になります。

置換で使うパラメーターの値には他の関数などを利用することも出来ます。
例えば以下の例では`GetAtt`関数を使ってAvailability Zoneの値を取得し、それを使って`Sub`関数で文字列の値の一部を置換しています。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: t3.nano
Outputs:
  MyString:
    Value: !Sub
      - "My instance's AZ is ${AvailabilityZone}."
      - AvailabilityZone: !GetAtt MyFirstEC2Instance.AvailabilityZone
```

スタックの変更セットを作成・反映してみて挙動を確認してみます。

完了後にリロードなどをして出力内容を見てみると`My instance's AZ is ${AvailabilityZone}.`とYAML上で指定していた文字列が`My instance's AZ is ap-northeast-1d.`と置換された文字列になっていることが確認できます。

# `Fn::GetAZs`関数

`Fn::GetAZs`関数は指定されたリージョンで利用可能なAvailability Zoneのリストを返却します。

`Fn::GetAZs: <対象のリージョン名>`という書き方をします。短縮系では`!GetAZs <対象のリージョン名>`という記述になります。

以下の例では東京リージョン（`ap-northeast-1`）のリストを`GetAZs`関数で取得しています。Outputsへの指定ではリストは指定できないので`Join`関数でコンマ区切りで連結しています。

余談ですが`ap-northeast-1`の指定をしているとなぜかcfn-lintに引っかかりました。`us-east-1`とかの他のリージョンでは普通にエラーなく通っています・・・。良く分からなかったのと実際に変更セットを作成した限りでは普通にエラー無く通ったので気にしないで進めます。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: t3.nano
Outputs:
  MyString:
    Value: !Join [",", !GetAZs "ap-northeast-1"]
```

変更セットの作成や反映などをして出力結果を確認すると`ap-northeast-1a,ap-northeast-1c,ap-northeast-1d`といったように東京リージョンの各AZ名が取れていることが確認できます。

# `Fn::FindInMap`関数

最後に`Fn::FindInMap`関数について触れていきます。ただし`Fn::FindInMap`関数に触れる前に`Mappings`の機能について先に触れておきます（`Fn::FindInMap`と基本的にセットで使用するため）。

## Mappingsの機能について

CloudFormationには`Mappings`のセクションに多次元の辞書（マッピング）を使う形で各キーに応じた定数のようなものを定義することができます。

`Mappings`セクション内にマッピング名、対象名、キー名といった具合に3つの階層が必要になります。キー名は同じパラメーターであれば同じキー名とかで大丈夫です。

これによって例えばパラメータで対象の環境を選択し、環境に応じた開発環境・ステージング・本番環境の各インスタンスタイプのマッピングを定義しておいて、選択された環境のインスタンスタイプの値をマッピングから取得する・・・みたいなことができるようになります。

YAMLだと以下のようなフォーマットになります。

```yaml
Mappings:
  <マッピング名>:
    <対象名1>:
      <キー名1>: <値1>
    <対象名2>:
      <キー名2>: <値2>
    ...
```

例えば先ほどのインスタンスタイプの例に合わせてマッピング名を`InstanceTypeMapping`、対象名を開発環境・ステージング・本番として`Dev`, `Stg`, `Prd`、キー名を`InstanceType`として値に各インスタンスタイプを持つマッピングだと以下のようになります。

```yaml
Mappings:
  InstanceTypeMapping:
    Dev:
      InstanceType: t3.nano
    Stg:
      InstanceType: t3.nano
    Prd:
      InstanceType: t3.small
```

このマッピングの個別の値を`Fn::FindInMap`関数を使って取得することができます。

## `Fn::FindInMap`関数の使い方

`Fn::FindInMap`関数の値には3つのインデックスを持つリストを指定します。1つ目のインデックスにはマッピング名（前節の例で言うと`InstanceTypeMapping`部分が該当）、2つ目のインデックスには対象名（最初のキー名。前節の例で言うと`Dev`や`Std`などの部分が該当）、3つ目のインデックスにはキー名（2つ目のキー目。前節で言うと`InstanceType`部分が該当）を指定します。返却値としてはマッピングの値（前節で言うと`t3.nano`などの部分が該当）が返却されます。

YAMLだと`Fn:FindInMap: [<マッピング名>, <対象名>, <キー名>]`といった書き方になります。YAMLのリストの書き方的に以下のように改行とインデント・ハイフンを使う形でも受け付けてくれます。

```yaml
Fn:FindInMap:
  - <マッピング名>
  - <対象名>
  - <キー名>
```

短縮系だと`!FindInMap [<マッピング名>, <対象名>, <キー名>]`といったように指定します。

また、この各インデックスの部分には他の`Ref`などの関数を挟むこともできます。つまりパラメータで環境を選択させて、選択された値を`Ref`関数で参照して`Fn::FindInMap`関数のリスト内で使う・・・みたいな書き方ができます。

例として`Environment`という名前のパラメータを定義していてそちらを`Fn::FindInMap`関数で産所してマッピングの値を取得したい・・・といった場合には以下のような記述になります。

```yaml
...
!FindInMap:
  - InstanceTypeMapping
  - !Ref Environment
  - InstanceType
```

実際にパラメータも定義したYAMLファイルを書いて変更セットを作成して試してみます。

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Mappings:
  InstanceTypeMapping:
    Dev:
      InstanceType: t3.nano
    Std:
      InstanceType: t3.nano
    Prd:
      InstanceType: t3.small
Parameters:
  Environment:
    Type: String
    AllowedValues:
      - Dev
      - Std
      - Prd
Resources:
  MyFirstEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0590f3a1742b17914
      InstanceType: !FindInMap
        - InstanceTypeMapping
        - !Ref Environment
        - InstanceType
```

YAMLをアップロードして設定を進めます。今回のYAMLテンプレートでは環境のパラメータを設定しているため選択が必要になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/7440fe3e-89df-3258-bc95-2d7fb172443f.png)

とりあえず試しとしてインスタンスタイプを変更してみたいためPrdを選択して進めます（現在はインスタンスタイプがnanoになっているのでPrdのsmallに変更してみます）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/81d7a64a-6459-2fd9-75a7-b996406ce1a2.png)

イベントが完了するまで待ちます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/f41afe87-a931-9c0b-08a6-c8d4292e9317.png)

完了後にEC2のページにアクセスするとインスタンスタイプがPrdのマッピングに合わせた`t3.small`になっていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/31255aaf-7482-9aa8-1629-38ff502a4dc3.png)

今度は同じテンプレートでパラメータでDevを選択してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/dc5df42d-a9e6-47d7-3b96-3c193c07f06b.png)

再度変更セットを反映してから完了するまで待った後にEC2インスタンスのページにアクセスするとインスタンスタイプがDevのマッピングに合わせた`t3.nano`になっていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/271aad61-033c-0537-f634-dc660b94b12f.png)

このように`Fn:FindInMap`関数を使ってマッピングの定義から値を取得できることが確認できました。

# 参考文献・参考サイト・参考講座まとめ

https://www.udemy.com/course/aws-cloudformation-simplified-hands-on-learning/

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

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

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html

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

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

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

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

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

https://note.com/shift_tech/n/nb8f61f1a866c