CloudFormationの「JSONの変更」を解読する

CloudFormationの変更セットを作成すると変更内容を確認することができます。 しかし、変更内容が書かれた変更JSONは読み解くのが難しいです。 今回はそんな変更JSONを読み解いていきたいと思います。
2023.11.27

はじめに

CloudFormationのテンプレート更新の際に変更内容の確認はしていますか?
変更内容は確認しているけど、よく分からんから神に祈りながら実行ボタンを押していませんか? 今回はそんな実行ボタンを押す気持ちを少しでも軽くできるようにJSONの変更の読み方を調べてみました。

変更セットの見方

それではまず、EC2を作成します。   構築に使用したymlファイルはこんな感じです。

AWSTemplateFormatVersion: "2010-09-09"
Description: EC2 Instance in a VPC with selectable security group

Parameters:
  SubnetId:
    Description: The Subnet ID to launch the instance in
    Type: AWS::EC2::Subnet::Id

  SecurityGroupId:
    Description: The Security Group ID to associate with the EC2 instance
    Type: AWS::EC2::SecurityGroup::Id

Resources:
  MyInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-012261b9035f8f938
      InstanceType: t2.micro
      SubnetId: !Ref SubnetId
      SecurityGroupIds:
        - !Ref SecurityGroupId

それではこちらに変更を加えてCloudFormationで変更スタックを作成します。
今回はタグを追加します。

AWSTemplateFormatVersion: "2010-09-09"
Description: EC2 Instance in a VPC with selectable security group

Parameters:
  SubnetId:
    Description: The Subnet ID to launch the instance in
    Type: AWS::EC2::Subnet::Id

  SecurityGroupId:
    Description: The Security Group ID to associate with the EC2 instance
    Type: AWS::EC2::SecurityGroup::Id

Resources:
  MyInstance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-012261b9035f8f938
      InstanceType: t2.micro
      SubnetId: !Ref SubnetId
      SecurityGroupIds:
        - !Ref SecurityGroupId
+     Tags:
+       - Key: Name
+         Value: MyEC2Instance

タグを追加しました。

      Tags:
        - Key: Name
          Value: MyEC2Instance

今回はコンソール画面から変更スタックを作成します。
まずは変更するスタックを選択して「既存スタックの変更セットを作成」を選択します。

「既存テンプレートを置き換える」を選択して更新したymlファイルを添付します。

今回は他の項目をデフォルトにして変更セットを作成します。
少し待つと変更セットが作成されるので、変更される箇所を確認していきます。

まずは表示された変更セットの変更タブを確認します。
ここで大まかに、どのリソースに変更が発生するのか把握できます。

画像の左下のアクションは変更の種類を表しています。
今回はタグを追加したので、既存のEC2に"Modify"(変更)があることを示しています。

アクションには"Modify"以外に"Add", "Remove", "Dynamic"というステータスがあります。
Addはリソースの追加、Removeは削除、Dynamicはスタックがネストされていて判断ができないということを表します。

もう一つ重要な項目として、置換という列があります。
これは今あるリソースを削除して、新たにリソースを作り直す必要があるかを判断します。
今回は"False"なので、置換の必要がないことを示しています。

これらの項目が自分の想定していた動きと違う場合は、もう少し詳しく変更内容を確認する必要があります。

JSONの変更

では、もう少し詳しい変更内容を確認します。
JSONの変更タブを選択します。

すると、変更箇所を表すJSONが表示されます。
こちらに詳細が記載されています。

[
  {
    "type": "Resource",
    "resourceChange": {
      "action": "Modify",
      "logicalResourceId": "MyInstance",
      "physicalResourceId": "i-1234567890abcdefgj",
      "resourceType": "AWS::EC2::Instance",
      "replacement": "False",
      "scope": [
        "Tags"
      ],
      "details": [
        {
          "target": {
            "attribute": "Tags",
            "requiresRecreation": "Never"
          },
          "evaluation": "Static",
          "changeSource": "DirectModification"
        }
      ]
    }
  }
]

いかがでしょうか?私は初めて見たとき、呪文か?と思いました。
それでは重要な部分を抜粋して、何がどう変更されるのか見ていきましょう。

まず、actionは先ほども出てきました。
これは"Modify"や"Add", "Remove"といった変更内容の大枠になります。

"action": "Modify",

次にリソースのIdやTyepeと続きます。
これは、変更対象のリソースを表しています。

"logicalResourceId": "MyInstance",
"physicalResourceId": "i-1234567890abcdefg",
"resourceType": "AWS::EC2::Instance",

replacement、これも先ほど出てきた置換を表します。
このパラメーターはリソースの停止が発生する可能性があるので、かなり重要です。
今回の"False"以外に"True"と"Conditional"があります。

"replacement": "False",

"True"の場合はリソースの置き換えが必ず発生します。
"Conditional"は条件によって置き換えが発生します。

条件というのは、インスタンスタイプ変更のようにリソースの新規作成が必要となるような場合です。

"Conditional"の場合は変更を把握している、または変更内容を調べる必要があるので注意が必要です。

replacementはどうやって決まるの?

replacementの値を決めるのは、detailsです。
details内にはattribute, requiresRecreation, evaluation, changeSourceがあります。(場合によっては他の項目もあります)

      "details": [
        {
          "target": {
            "attribute": "Tags",
            "requiresRecreation": "Never"
          },
          "evaluation": "Static",
          "changeSource": "DirectModification"
        }
      ]

attributeは変更対象です、今回の場合はタグを追加するのでTagsです。
requiresRecreationは置き換えの必要性を表す"Never", "Conditionally", "Always"があります。

"Never"は再作成の必要がありません、そのためrequiresRecreationが"Never"ならreplacementは"False"になります。
"Never"以外の場合、"Always"は置き換えが必要で、"Conditionally"は条件によって置き換えが必要となります。
ちょっとややこしいので図にしてみました。

そして、ここで思います。
あれ、replacementとrequiresRecreationは何が違うの?と。

replacementは最終的な置換の有無です。
requiresRecreationは、各項目の置換の必要性を表します。
複数のrequiresRecreationの結果からreplacementが決まる、ということです。

evaluationには"static", "dynamic"があります。
"static"はテンプレート内で定義した値がそのまま反映されるということを意味しています。
"dynamic"は対象の変更が他のリソースの状態や動作によって影響を受けるということを意味しています。

今回はテンプレートにタグを追加しました。
そして、変更セットを実行すれば必ずそのタグ名が付与されます。
つまり、定義されたstatic(静的)な値ということになります。

最後のchangeSourceは、何によって今回の変更が行われか?です。
今回の変更はテンプレートを直接変更したので"DirectModification"となっています。
他に、"ParameterReference", "Automatic"などがあります。

やってみよう

ではいくつかの変更を行います。
一つはサブネットの変更です、これはEC2の再作成が必要です。
もう一つはEC2のインスタンスタイプの変更です。
こちらは、インスタンスタイプの互換性がなければEC2の再作成が必要です。

それでは変更セットを作成します。

[
  {
    "type": "Resource",
    "resourceChange": {
      "action": "Modify",
      "logicalResourceId": "MyInstance",
      "physicalResourceId": "i-1234567890abcdefg",
      "resourceType": "AWS::EC2::Instance",
      "replacement": "True",
      "scope": [
        "Properties"
      ],
      "details": [
        {
          "target": {
            "attribute": "Properties",
            "name": "InstanceType",
            "requiresRecreation": "Conditionally"
          },
          "evaluation": "Static",
          "changeSource": "DirectModification"
        },
        {
          "target": {
            "attribute": "Properties",
            "name": "SubnetId",
            "requiresRecreation": "Always"
          },
          "evaluation": "Static",
          "changeSource": "ParameterReference",
          "causingEntity": "SubnetId"
        },
        {
          "target": {
            "attribute": "Properties",
            "name": "SubnetId",
            "requiresRecreation": "Always"
          },
          "evaluation": "Dynamic",
          "changeSource": "DirectModification"
        }
      ]
    }
  }
]

今回はreplacementが"True"となっています。
これはEC2を起動するサブネットを変更したので、予想通りです。

ではもう少し詳しく見ましょう。
1つ目はInstanceTypeを変更したことで、requiresRecreationが"Conditionally"となっています。

{
  "target": {
    "attribute": "Properties",
    "name": "InstanceType",
    "requiresRecreation": "Conditionally"
  },
  "evaluation": "Static",
  "changeSource": "DirectModification"
},

インスタンスタイプはインスタンスが停止状態で変更することができますが、互換性のないタイプへ変更する場合は再作成が必要です。

実際にEC2のインスタンスタイプを変更する場合は、インスタンスタイプの互換性があるか調べる必要あります。

2つ目、3つ目はSubnetIdの変更についてです。

        {
          "target": {
            "attribute": "Properties",
            "name": "SubnetId",
            "requiresRecreation": "Always"
          },
          "evaluation": "Static",
          "changeSource": "ParameterReference",
          "causingEntity": "SubnetId"
        },
        {
          "target": {
            "attribute": "Properties",
            "name": "SubnetId",
            "requiresRecreation": "Always"
          },
          "evaluation": "Dynamic",
          "changeSource": "DirectModification"
        }

SubnetIdに関する変更が2つあります。
これはドキュメントにも記載されていますが、変更されたパラメータ(SubnetId)が一つでも、"static"と"dynamic"の2つの変更が表示されることがあります。
このような場合は、"Static"に注目します、Static側にはcausingEntity(原因の主体)が書かれています。

今回の例では、ParameterReference(サブネットの変更)が原因となってSubnetIdが置き換わったことが分かります。

また、今回の変更ではrequiresRecreationに"Conditionally"と"Always"が含まれています。
インスタンスタイプ変更のみを見ると条件次第(Conditionally)ですが、全体を通して最も大きい変更(Always)が適用されます。
そのため、この変更セットではインスタンスの置き換えが発生します。

まとめ

特に重要なのはreplacement(置換)が発生するか否かという部分です。
あらかじめ予想できているものであれば問題ありませんが、そうでない変更は要注意です。

特にConditionallyは置き換えが発生するかもしれないということなので、実行前には必ず影響の範囲を確認するようにしましょう。
少しでも変更スタックの実行を押す気持ちが軽くなれば幸いです。