AWS CloudFormationで他のスタックからImportValueした文字列をSub関数で結合する

表題のとおり単純なことなのですが、CloudFormation 初心者が一般的なプログラム言語の感覚で使うと、細かいところで引っ掛かりがちでした。以下はその中の1つです。

# 変更前

スタック内で定義したパラメータを Sub 関数で結合して、Name プロパティに設定していました。
`www.${Environment}.${DnsDomain}`は「www.dev.mydomain.com」のようになります。

```Yaml:
Parameters:
  Environment:
    Type: String
  DnsDomain:
    Type: String

Resources:
  DnsRecordSet1:
    Type: AWS::Route53::RecordSet
    Properties:
      Name: !Sub www.${Environment}.${DnsDomain}
```

# 変更後

Environment と DnsDomain の定義を他のスタックに移動したので、ImportValue で取得することにしました。公式のサンプルと同様に、インポートするための共有名も Sub 関数でパラメータと結合して作っています。

* `${Environment}` → `{'Fn::ImportValue': !Sub '${PjCode}-environment'}`
* `${DnsDomain}` 　→ `{'Fn::ImportValue': !Sub '${PjCode}-dns-domain'}`

参考: 公式ドキュメント[「 組み込み関数リファレンス » Fn::ImportValue」](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html)

## 不正解

:::note alert
以下のようにパラメータと置き換えただけでは使えません。
:::

```Yaml:
    Properties:
      Name: !Sub www.{'Fn::ImportValue': !Sub '${PjCode}-environment'}.{'Fn::ImportValue': !Sub '${PjCode}-dns-domain'}
```

## 公式ドキュメントを読む
公式ドキュメントの[「組み込み関数リファレンス » Fn::Sub」](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html)を見ると、実行時に取得する値（以下の Var1Value、Var2Value ）を Sub 関数のパラメータに含むことができると書かれています。

```Yaml:
!Sub
  - String
  - { Var1Name: Var1Value, Var2Name: Var2Value }
```

`String` には、実行時に取得する値と置き換えるプレースホルダを "${VarName}" のように置くことができます。

以下の関数がサポートされていると書かれているので、ImportValue は問題なく使えます。
> Fn::Base64
Fn::FindInMap
Fn::GetAtt
Fn::GetAZs
Fn::If
Fn::ImportValue
Fn::Join
Fn::Select
Ref

## 正解

:::note info
こうなります
:::

```Yaml:
    Properties:
      Name: !Sub
        - www.${Environment}.${DnsDomain}
        - Environment:  {'Fn::ImportValue': !Sub '${PjCode}-environment'}
          DnsDomain:    {'Fn::ImportValue': !Sub '${PjCode}-dns-domain'}
```

`!Sub` に続く配列の最初の要素は、プレースホルダを含むテンプレートです。配列の 2 番目の要素は辞書で、プレースホルダと置き換える「キー: 値」の組を必要なだけ格納します。

:::note alert
うっかり「DnsDomain」の前にも「-」を付けてしまうと、「DnsDomain」が配列の 3 番目の要素になってしまうので不正解になります。
:::

```Yaml:これも不正解！
    Properties:
      Name: !Sub
        - www.${Environment}.${DnsDomain}
        - Environment:  {'Fn::ImportValue': !Sub '${PjCode}-environment'}
        - DnsDomain:    {'Fn::ImportValue': !Sub '${PjCode}-dns-domain'}
```

# より複雑な例 (CloudWatch Dashboard)

プレースホルダを含むテンプレート文字列が複数行になったり、スタック内で定義した Parameters や Mappings から FindInMap 関数で取り出した値を使用する場合も、考え方は同じです。

以下の例では、他のスタックで構成した 2 つの EC2 インスタンスのメモリ使用率を折れ線グラフで表示するダッシュボードを構成します。

インスタンス ID は別のスタックで生成された値をインポートしますが、インスタンスタイプ（"t2.micro" など）はスタック内の Parameters で、イメージ (AMI) ID はスタック内の Mappings から、現在のリージョンに対応する値を取り出すことにします。

```Yaml:
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  PjCode:
    Type: String
  Ec2InstanceType1:
    Type: String
  Ec2InstanceType2:
    Type: String

Mappings:
  Resion2Ami:
    ap-northeast-1:
      hvm: ami-00d101850e971728d

Resources:
  CloudWatchDashboard:
    Type: "AWS::CloudWatch::Dashboard"
    Properties:
      DashboardName: !Sub '${PjCode}-dashboard'
      DashboardBody: !Sub
        - |
            {
              "widgets": [
                {
                  "type": "metric",
                  "x": 0,
                  "y": 0,
                  "width": 6,
                  "height": 6,
                  "properties": {
                    "view": "timeSeries",
                    "stacked": false,
                    "metrics": [
                      [
                        "CWAgent",
                        "mem_used_percent",
                        "InstanceId",
                        "${Ec2Instance1}",
                        "ImageId",
                        "${Ec2ImageId}",
                        "InstanceType",
                        "${Ec2InstanceType1}"
                      ],
                      [
                        "...",
                        "${Ec2Instance2}",
                        ".",
                        "${Ec2ImageId}",
                        ".",
                        "${Ec2InstanceType2}"
                      ]
                    ],
                    "region": "${AWS::Region}"
                  }
                }
              ]
            }
        - Ec2Instance1: {'Fn::ImportValue': !Sub '${PjCode}-ec2-1'}
          Ec2Instance2: {'Fn::ImportValue': !Sub '${PjCode}-ec2-2'}
          Ec2ImageId: !FindInMap [ Resion2Ami, !Ref "AWS::Region", hvm ]
```

- スタック内で定義した Parameters 値は、単純にテンプレート内に `${Ec2InstanceType1}` のように埋め込みます
- ImportValue して Sub 関数で結合した値は、上の例と同様に配列の2番目の要素内にハッシュで定義します
- Mapping から FindInMap 関数で取り出した値も同様です

参考: [amazon cloudformation - Using Fn::ImportValue in Dashboard Cloudwatch - Stack Overflow](https://stackoverflow.com/questions/52990020/using-fnimportvalue-in-dashboard-cloudwatch)