ハードコーディングしたCloudFormationテンプレートの更新時の動作を確認してみた

2021.07.06

はじめに

CloudFormation使ってますか?
Resourcesセクションのパラメータをテンプレート内でハードコーディングした後、組み込み関数やParametersを追加してテンプレートを更新したいってことありますよね?
本日は、ハードコーディングされたCFnテンプレートを更新するときの動作を確認してみました。

今回の構成

以下のネットワークのみ構成を作成して動作確認していきます。

ハードコーディングされたテンプレート

CFnテンプレートを用意しました。 CidrBlockやAvailabilityZone、各リソースのタグでは環境(dev)、システム名(nkhr)をハードコーディングしています。

VPCテンプレート
---
AWSTemplateFormatVersion: "2010-09-09"
Description: "Network Template."

Resources:
  # -----
  # VPC1
  # -----
  vpc1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: dev-nkhr-vpc1
  # -----
  # VPC1 Internet gateway
  # -----
  internetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: dev-nkhr-igw
  attachIgw:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref vpc1
      InternetGatewayId: !Ref internetGateway
  # -----
  # VPC1 Public Subnet
  # -----
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 10.0.0.0/24
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-public-subnet-1
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: 10.0.1.0/24
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-public-subnet-2
  # -----
  # VPC1 Public RouteTable
  # -----
  PublicRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-public-rtb-1
  PublicRoutingA1:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref internetGateway
      RouteTableId: !Ref PublicRouteTable1
  PublicRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable1
  PublicRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable1
  # -----
  # VPC1 Private Subnet
  # -----
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 10.0.10.0/24
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-private-subnet-1
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: 10.0.11.0/24
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-private-subnet-2
  # -----
  # VPC1 Private RouteTable
  # -----
  PrivateRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-private-rtb-1
  PrivateRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTableA
  PrivateRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTableA

このテンプレートをデプロイするとスタックが作成されます。
AWSのCLI実行例

aws cloudformation create-stack \
  --stack-name dev-network \
  --template-body file://network.yml

ハードコーディングされたパラメータを修正することでstg環境やprod環境にもデプロイできますが、ハードコーディングされた値が多く修正を間違えると厄介です。

組み込み関数を使ったテンプレート

CloudFormationには組み込み関数が用意されており、サブネットのAvailabilityZoneとCidrBlockをハードコーディングすることなく意図したパラメータを代入できます。
まず、GetAZs関数を使って、スタックを作成するリージョンのアベイラビリティーゾーンをアルファベット順にリストした配列を返します。ap-northeast-1(東京)では、ap-northeast-1aap-northeast-1cap-northeast-1dの順番でリストされます。リストした配列は、Select関数を使って指定した値を取り出せます。

抜粋した以下の記述では、GetAZs関数でリスト化された配列の0(ap-northeast-1a)の値が参照されます。なお、AWS::RegionはAWSで定義されたパラメータです。スタックを作成するリージョン名(ap-northeast-1)を返します。


AvailabilityZoneプロパティ抜粋
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region

次にCidr関数とSelect関数を使ってVPCのCidrBlockからアドレスブロックを生成し、リストされた配列から参照するように記述します。
抜粋した以下の記述では、論理IDvpc1のCidrBlock(10.0.0.0/16)からサブネットマスク/24のアドレスブロックを2つ生成(10.0.0.0/24、10.0.1.0/24)し、リストした配列の1(10.0.1.0/24)の値が参照されます。


サブネットのCidrBlockプロパティ抜粋
      CidrBlock: !Select [ 1, !Cidr [ !GetAtt vpc1.CidrBlock, 2, 8 ]]

Cidr関数についてわかりやすく解説しているブログがあったので貼り付けておきます。

先程のテンプレートに組み込み関数を追加したテンプレートが以下です。

VPCテンプレート(組み込み関数あり)
---
AWSTemplateFormatVersion: "2010-09-09"
Description: "Network Template."

Resources:
  # -----
  # VPC1
  # -----
  vpc1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: dev-nkhr-vpc1
  # -----
  # VPC1 Internet gateway
  # -----
  internetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: dev-nkhr-igw
  attachIgw:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref vpc1
      InternetGatewayId: !Ref internetGateway
  # -----
  # VPC1 Public Subnet
  # -----
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 0, !Cidr [ !GetAtt vpc1.CidrBlock, 1, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-public-subnet-1
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 1
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 1, !Cidr [ !GetAtt vpc1.CidrBlock, 2, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-public-subnet-2
  # -----
  # VPC1 Public RouteTable
  # -----
  PublicRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-public-rtb-1
  PublicRoutingA1:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref internetGateway
      RouteTableId: !Ref PublicRouteTable1
  PublicRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable1
  PublicRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable1
  # -----
  # VPC1 Private Subnet
  # -----
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 10, !Cidr [ !GetAtt vpc1.CidrBlock, 11, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-private-subnet-1
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 1
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 11, !Cidr [ !GetAtt vpc1.CidrBlock, 12, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-private-subnet-2
  # -----
  # VPC1 Private RouteTable
  # -----
  PrivateRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: dev-nkhr-private-rtb-1
  PrivateRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTableA
  PrivateRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTableA

このテンプレートで変更セットを作成します。
AWSのCLI実行例

aws cloudformation create-change-set \
  --stack-name dev-network \
  --change-set-name add-functions \
  --template-body file://network.yml

変更セットのステータス確認と変更対象を確認してみます。

# ステータス確認
aws cloudformation describe-change-set \
  --change-set-name add-functions \
  --stack-name dev-network  | \
  jq '. | { Status: .Status, StatusReason: .StatusReason }'

# 変更対象の確認
aws cloudformation describe-change-set \
  --change-set-name add-functions \
  --stack-name dev-network | \
  jq '.Changes[] | {Action: .ResourceChange.Action, LogicalResourceId: .ResourceChange.LogicalResourceId }'

ステータスがCREATE_COMPLETE、変更対象のリソースとしてサブネット、ルートテーブル関連が出力されています。これは変更セット実行のタイミングでは、アドレスブロックが生成されておらずハードコーディングされたものとは異なる値の為、変更対象として出力されます。

出力結果

# ステータス確認
{
  "Status": "CREATE_COMPLETE",
  "StatusReason": null
}

# 変更対象の確認
{
  "Action": "Modify",
  "LogicalResourceId": "PrivateRouteTableAssociation1"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PrivateRouteTableAssociation2"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PrivateSubnet1"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PrivateSubnet2"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PublicRouteTableAssociation1"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PublicRouteTableAssociation2"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PublicSubnet1"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PublicSubnet2"
}

変更対象のリソースは再作成されるのか?答えは否です。組み込み関数で参照される値がハードコーディングされた値と同等であれば、リソースの変更はありません。

以下のコマンドで変更セットを実行します。

aws cloudformation execute-change-set \
  --stack-name dev-network 
  --change-set-name add-functions

スタックイベントを見ると、リソースの更新が無いことが確認できます。(先頭4件のイベントのTimestamp,ResourceStatusを表示)

aws cloudformation describe-stack-events \
  --stack-name dev-network | \
  jq '.StackEvents[:4] | .[] | {Timestamp: .Timestamp, ResourceStatus: .ResourceStatus }'

出力結果

{
  "Timestamp": "2021-07-05T11:57:33.859Z",
  "ResourceStatus": "UPDATE_COMPLETE"
}
{
  "Timestamp": "2021-07-05T11:57:32.994Z",
  "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"
}
{
  "Timestamp": "2021-07-05T11:57:26.663Z",
  "ResourceStatus": "UPDATE_IN_PROGRESS"
}
{
  "Timestamp": "2021-07-05T09:24:21.982Z",
  "ResourceStatus": "CREATE_COMPLETE"
}

Parametersを使ったテンプレート

今度はVPCのCidrBlock、Tagの環境名(dev)、システム名(nkhr)をParametersを使ってテンプレートを修正します。ParametersでVpcCidr、Env、SystemNameを追加し、Defaultに値を設定しました。
各リソースのプロパティではSub関数を使ってParametersを参照できるようにします。


Sub関数 抜粋
  vpc1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Sub ${VpcCidr}
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-vpc

先程のテンプレートにParametersを追加したテンプレートが以下です。

VPCテンプレート(組み込み関数、Parametersあり)
---
AWSTemplateFormatVersion: "2010-09-09"
Description: "Network Template."

Parameters:
  VpcCidr:
    Type: String
    Default: 10.0.0.0/16
  Env:
    Type: String
    Default: dev
  SystemName:
    Type: String
    Default: nkhr

Resources:
  # -----
  # VPC1
  # -----
  vpc1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Sub ${VpcCidr}
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-vpc1
  # -----
  # VPC1 Internet gateway
  # -----
  internetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-igw
  attachIgw:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref vpc1
      InternetGatewayId: !Ref internetGateway
  # -----
  # VPC1 Public Subnet
  # -----
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 0, !Cidr [ !GetAtt vpc1.CidrBlock, 1, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-public-subnet-1
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 1
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 1, !Cidr [ !GetAtt vpc1.CidrBlock, 2, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-public-subnet-2
  # -----
  # VPC1 Public RouteTable
  # -----
  PublicRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-public-rtb-1
  PublicRoutingA1:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref internetGateway
      RouteTableId: !Ref PublicRouteTable1
  PublicRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable1
  PublicRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable1
  # -----
  # VPC1 Private Subnet
  # -----
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 10, !Cidr [ !GetAtt vpc1.CidrBlock, 11, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-private-subnet-1
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 1
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 11, !Cidr [ !GetAtt vpc1.CidrBlock, 12, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-private-subnet-2
  # -----
  # VPC1 Private RouteTable
  # -----
  PrivateRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-private-rtb-1
  PrivateRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTableA
  PrivateRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTableA

このテンプレートで変更セットを作成します。
AWSのCLI実行例

aws cloudformation create-change-set \
  --stack-name dev-network \
  --change-set-name add-parameters \
  --template-body file://network.yml

変更対象を確認してみます。

# ステータス確認
aws cloudformation describe-change-set \
  --change-set-name add-parameters \
  --stack-name dev-network | \
  jq '. | { Status: .Status, StatusReason: .StatusReason }'

# 変更対象の確認
aws cloudformation describe-change-set \
  --change-set-name add-parameters \
  --stack-name dev-network | \
  jq '.Changes[] | {Action: .ResourceChange.Action, LogicalResourceId: .ResourceChange.LogicalResourceId }'

ステータスがFAILED、変更対象リソースは何も表示されません。

出力結果

# ステータス確認
{
  "Status": "FAILED",
  "StatusReason": "The submitted information didn't contain changes. Submit different information to create a change set."
}

StatusReasonには、変更対象が含まれていないことでFAILEDとなったと書かれています。ParametersはCidr関数のように生成されるパラメータではなく固有パラメータを設定するので、変更セット実行時にパラメータにある値と現在の値が同等であれば変更対象となりません。
変更セットから実行できないので、変更セットなしでスタックをアップデートします。

aws cloudformation update-stack \
  --stack-name dev-network 
  --template-body file://network.yml

スタックイベントを見ると、リソースの更新が無いことが確認できます。

aws cloudformation describe-stack-events \
  --stack-name dev-network | \
  jq '.StackEvents[:4] | .[] | {Timestamp: .Timestamp, ResourceStatus: .ResourceStatus }'

出力結果

{
  "Timestamp": "2021-07-05T15:31:58.975Z",
  "ResourceStatus": "UPDATE_COMPLETE"
}
{
  "Timestamp": "2021-07-05T15:31:58.199Z",
  "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"
}
{
  "Timestamp": "2021-07-05T15:31:49.479Z",
  "ResourceStatus": "UPDATE_IN_PROGRESS"
}
{
  "Timestamp": "2021-07-05T15:18:03.495Z",
  "ResourceStatus": "UPDATE_COMPLETE"
}

ただ、上記方法だと変更セットで変更リソースを確認できないのは怖いですよね。どうしても変更セットから確認したい場合は、任意のリソースのタグを追加または変更することで変更セットを作成できます。 以下のテンプレートでは、論理IDvpc1にChangSetタグを追加します。

VPCテンプレート(組み込み関数、Parametersあり、タグ追加)
---
AWSTemplateFormatVersion: "2010-09-09"
Description: "Network Template."

Parameters:
  Env:
    Type: String
    Default: dev
  SystemName:
    Type: String
    Default: nkhr

Resources:
  # -----
  # VPC1
  # -----
  vpc1:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-vpc1
        - Key: ChangSet
          Value: "True"
  # -----
  # VPC1 Internet gateway
  # -----
  internetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-igw
  attachIgw:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref vpc1
      InternetGatewayId: !Ref internetGateway
  # -----
  # VPC1 Public Subnet
  # -----
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 0, !Cidr [ !GetAtt vpc1.CidrBlock, 1, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-public-subnet-1
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 1
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 1, !Cidr [ !GetAtt vpc1.CidrBlock, 2, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-public-subnet-2
  # -----
  # VPC1 Public RouteTable
  # -----
  PublicRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-public-rtb-1
  PublicRoutingA1:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref internetGateway
      RouteTableId: !Ref PublicRouteTable1
  PublicRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable1
  PublicRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable1
  # -----
  # VPC1 Private Subnet
  # -----
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 10, !Cidr [ !GetAtt vpc1.CidrBlock, 11, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-private-subnet-1
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select
        - 1
        - !GetAZs
          Ref: AWS::Region
      CidrBlock: !Select [ 11, !Cidr [ !GetAtt vpc1.CidrBlock, 12, 8 ]]
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-private-subnet-2
  # -----
  # VPC1 Private RouteTable
  # -----
  PrivateRouteTableA:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpc1
      Tags:
        - Key: Name
          Value: !Sub ${Env}-${SystemName}-private-rtb-1
  PrivateRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTableA
  PrivateRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTableA

このテンプレートで変更セットを作成します。
AWSのCLI実行例

aws cloudformation create-change-set \
  --stack-name dev-network \
  --change-set-name add-parameters-tag \
  --template-body file://network.yml

変更対象を確認してみます。

# ステータス確認
aws cloudformation describe-change-set \
  --change-set-name add-parameters-tag \
  --stack-name dev-network | \
  jq '. | { Status: .Status, StatusReason: .StatusReason }'

# 変更対象の確認
aws cloudformation describe-change-set \
  --change-set-name add-parameters-tag \
  --stack-name dev-network | \
  jq '.Changes[] | {Action: .ResourceChange.Action, LogicalResourceId: .ResourceChange.LogicalResourceId }'

出力結果

# ステータス確認
{
  "Status": "CREATE_COMPLETE",
  "StatusReason": null
}

# 変更対象の確認
{
  "Action": "Modify",
  "LogicalResourceId": "PrivateRouteTableAssociation1"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PrivateRouteTableAssociation2"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PrivateSubnet1"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PrivateSubnet2"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PublicRouteTableAssociation1"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PublicRouteTableAssociation2"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PublicSubnet1"
}
{
  "Action": "Modify",
  "LogicalResourceId": "PublicSubnet2"
}
{
  "Action": "Modify",
  "LogicalResourceId": "vpc1"
}

変更対象が出力されましたね。以下のコマンドで変更セットを実行します。

aws cloudformation execute-change-set \
  --stack-name dev-network \
  --change-set-name add-parameters-tag

スタックイベントを見ると、VPCリソースのみ更新されていることが確認できます。

aws cloudformation describe-stack-events \
  --stack-name dev-network | \
  jq '.StackEvents[:5] | .[] | {Timestamp: .Timestamp, ResourceStatus: .ResourceStatus }'

出力結果

{
  "Timestamp": "2021-07-05T15:54:37.159Z",
  "ResourceStatus": "UPDATE_COMPLETE"
}
{
  "Timestamp": "2021-07-05T15:54:36.532Z",
  "ResourceStatus": "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"
}
{
  "Timestamp": "2021-07-05T15:54:32.603Z",
  "ResourceStatus": "UPDATE_COMPLETE"
}
{
  "Timestamp": "2021-07-05T15:54:32.141Z",
  "ResourceStatus": "UPDATE_IN_PROGRESS"
}
{
  "Timestamp": "2021-07-05T15:54:26.913Z",
  "ResourceStatus": "UPDATE_IN_PROGRESS"
}

さいごに

作成したテンプレートを更新していくときに不安要素となるリソースがどうなるのか、動作確認してみました。結果としては設定済みの値と同等であれば再作成されないということがわかりました。
とはいえ、予期せぬ事象が起きる可能性も考えて、検証することをオススメします。より良いテンプレート作りの参考となれば幸いです。