CloudFormationをゼロから勉強する。（その７：変更セットとドリフト検出）

# はじめに
今回はスタック実行前に事前に変更箇所を確認できる`変更セット`と、スタック実行後に手動操作などで変更されたリソースを確認できる`ドリフト検出`を試してみようと思います。

- 【前】[CloudFormationをゼロから勉強する。（その６：Transformによるインクルード）](https://qiita.com/sakai00kou/items/57019ef4b3b6b7d61251)
- 【次】[CloudFormationをゼロから勉強する。（その８：スタックのネスト）](https://qiita.com/sakai00kou/items/553aba6cc924aff3c9b8)
- 【番外編】[CloudFormationをゼロから勉強する。（番外編：AWS Perspective）](https://qiita.com/sakai00kou/items/b019de5266d81cae9971)

# 変更セットについて
前述の通り、スタック実行前に変更箇所を確認することができる機能です。

スタック実行前に変更されるリソースやパラメータを確認できるので、スタック実行することによって**意図せずリソースが削除されたりする事故が防げます。**

テスト環境ならともかく、サービスが動作している本番環境でスタックを実行する際には**必須の機能**かと思います。

# ドリフト検出について
これも前述の通り、スタック実行後に手動操作などで変更されたリソースを確認できる機能です。

本来は環境を`CloudFormation`で管理しているのであればスタックで作成したリソースや設定に対して手動で操作することは極力避けるべきですが、何かしらの理由で手動操作した場合でも変更箇所を検出することができます。

例えば障害対応などで手動で変更した設定を、後日ドリフト検出で確認して`CloudFormation`テンプレートに落とし込む場合に便利です。

# 今回のシナリオ
`変更セット`と`ドリフト検出`を試すため、1つの`VPC`と2つの`サブネット`リソースのみのシンプルな構成とし、サブネットリソースだけタグ付けした構成をベースとして以下を実施します。

1. サブネットのタグを修正して変更セットを作成する。
2. 手動で1つのサブネットの削除、VPCのタグの追加とサブネットのタグを修正してドリフト検出する。
3. テンプレートからサブネットの削除とタグの追加・修正を行い、再度変更セットを作成する。

また、コンソールやコマンドで実行する際のスタック名等は以下で行うため、適宜読み替えるようにして下さい。

|項目|名前|
|:--|:--|
|スタック名|stack-test|
|変更セット名|change-set-test|
|テンプレートファイルパス|/home/ec2-user/cf.yaml|
|パラメータファイルパス|/home/ec2-user/param.yaml|

## ベーステンプレートの作成
今回は`VPC`と`サブネット`のみのテンプレートとパラメータファイルを新たに作成します。

`Metadata`や不要な設定は削除して極力シンプルにしました。

<details><summary>ベーステンプレートの作成（展開して下さい）</summary><div>

```yaml:/home/ec2-user/cf.yaml
AWSTemplateFormatVersion: 2010-09-09
Resources:
  EC2VPC1:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Ref VPCRange
  EC2Subnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref EC2VPC1
      CidrBlock: !Ref SubnetRange1
      Tags:
        - Key: "Name"
          Value: "CloudFormation"
  EC2Subnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref EC2VPC1
      CidrBlock: !Ref SubnetRange2
      Tags:
        - Key: "Name"
          Value: "CloudFormation"
Parameters:
  VPCRange:
    Type: String
    Description: "VPC Subnet Range"
  SubnetRange1:
    Type: String
    Description: "Subnet Range1"
  SubnetRange2:
    Type: String
    Description: "Subnet Range2"
```
</div></details>

<details><summary>ベースパラメータファイルの作成（展開して下さい）</summary><div>

```yaml:/home/ec2-user/param.json
[
  { "ParameterKey" : "VPCRange", "ParameterValue" : "172.24.0.0/16" },
  { "ParameterKey" : "SubnetRange1", "ParameterValue" : "172.24.0.0/24" },
  { "ParameterKey" : "SubnetRange2", "ParameterValue" : "172.24.1.0/24" }
]
```
</div></details>

```shell:テンプレートの作成
aws cloudformation create-stack --template-body file:///home/ec2-user/cf.yaml --stack-name stack-test --parameters file:///home/ec2-user/param.json
```

# ベーステンプレートの修正
前準備として作成したベーステンプレートを以下の内容に修正します。

1. サブネットリソースのタグを`cf_Subnet`という名前に修正する。

## 変更セットの作成
`変更セット`は作成済みスタックのテンプレートと修正したテンプレートの差分をチェックして変更となる項目を確認することができます。

`AWSマネジメントコンソール`から操作する場合は作成したスタックを選択し、`変更セット`→`変更セットの作成`から修正したテンプレートをアップロードするか、直接編集を行います。

![capture_28092020_073042.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/9d7c4a35-0904-6b40-b644-4de7fad882c2.jpeg)

`AWS CLI`で実行する場合は以下コマンドを実行します。

```shell:変更セットの作成
aws cloudformation create-change-set --change-set-name change-set-test --template-body file:///home/ec2-user/cf.yaml --stack-name stack-test --parameters file:///home/ec2-user/param.json
```

`変更セット`を作成すると以下画面のような比較結果が表示され、どのリソースが変更されるかが確認できます。

![capture_29092020_075226.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/9cb6db5b-7467-5364-d282-842520764ecc.jpeg)

コマンドの場合は以下。

```shell:変更セットの確認
aws cloudformation describe-change-set --change-set-name change-set-test --stack-name stack-test
```

<details><summary>コマンド実行結果（展開して下さい）</summary><div>

```json:コマンド実行結果
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:335417439444:stack/stack-test/d5249730-01dc-11eb-8e2a-0af1581a9dde", 
    "Status": "CREATE_COMPLETE", 
    "ChangeSetName": "change-set-test", 
    "Parameters": [
        {
            "ParameterValue": "172.24.0.0/24", 
            "ParameterKey": "SubnetRange1"
        }, 
        {
            "ParameterValue": "172.24.1.0/24", 
            "ParameterKey": "SubnetRange2"
        }, 
        {
            "ParameterValue": "172.24.0.0/16", 
            "ParameterKey": "VPCRange"
        }
    ], 
    "Changes": [
        {
            "ResourceChange": {
                "ResourceType": "AWS::EC2::Subnet", 
                "PhysicalResourceId": "subnet-0c76cd08741e9b6af", 
                "Details": [
                    {
                        "ChangeSource": "DirectModification", 
                        "Evaluation": "Static", 
                        "Target": {
                            "Attribute": "Tags", 
                            "RequiresRecreation": "Never"
                        }
                    }
                ], 
                "Action": "Modify", 
                "Scope": [
                    "Tags"
                ], 
                "LogicalResourceId": "EC2Subnet1", 
                "Replacement": "False"
            }, 
            "Type": "Resource"
        }, 
        {
            "ResourceChange": {
                "ResourceType": "AWS::EC2::Subnet", 
                "PhysicalResourceId": "subnet-04755c355840e4956", 
                "Details": [
                    {
                        "ChangeSource": "DirectModification", 
                        "Evaluation": "Static", 
                        "Target": {
                            "Attribute": "Tags", 
                            "RequiresRecreation": "Never"
                        }
                    }
                ], 
                "Action": "Modify", 
                "Scope": [
                    "Tags"
                ], 
                "LogicalResourceId": "EC2Subnet2", 
                "Replacement": "False"
            }, 
            "Type": "Resource"
        }
    ], 
    "CreationTime": "2020-09-28T22:50:52.425Z", 
    "Capabilities": [], 
    "StackName": "stack-test", 
    "NotificationARNs": [], 
    "ExecutionStatus": "AVAILABLE", 
    "ChangeSetId": "arn:aws:cloudformation:ap-northeast-1:335417439444:changeSet/change-set-test/845b6229-3800-448c-9f80-d54cdf81f17d", 
    "RollbackConfiguration": {}
}
```
</div></details>

## 変更セットの実行
`変更セット`は作成しただけでは差分結果を表示するだけなので、差分結果に問題が無ければ実際に修正したテンプレートを反映させるため`変更セット`を実行します。

修正したテンプレートを実行するためには、コンソールからなら`実行`ボタン、コマンドからなら`execute-change-set`を実行します。

```shell:変更セットの実行
aws cloudformation execute-change-set --change-set-name change-set-test --stack-name stack-test
```

変更セット実行後は以下の様にタグが追加、修正されているのが確認できます。

![capture_29092020_080047.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/1b8d9e92-2876-13ae-b990-f303052cd734.jpeg)

## サブネットリソースの手動削除とタグ追加・修正
`ドリフト検出`を試すため、`SubnetRange2`で作成したサブネットを手動で削除してみます。

コマンドで実行する場合は事前に`サブネットID`を調べておいて下さい。

```shell:サブネットの削除
aws ec2 delete-subnet --subnet-id [サブネットID]
```

```shell:VPC、サブネットのタグ追加・修正
aws ec2 create-tags --resources [VPC ID] --tags Key=Name,Value=cf_VPC1
aws ec2 create-tags --resources [サブネットID] --tags Key=Name,Value=cf_Subnet1
```

## ドリフト検出の実行
スタック一覧画面でスタックを選択し、`スタックアクション`から`ドリフトの検出`を選択することで最新のテンプレートと異なる設定をチェックできます。

![capture_28092020_083002.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/90f818f6-5444-60c7-2f9c-97228dbafb2c.jpeg)

コマンドの場合は以下。

```shell:ドリフト検出の実行
aws cloudformation detect-stack-drift --stack-name stack-test
```

## ドリフト結果の確認
スタック一覧画面でスタックを選択し、`スタックアクション`から`ドリフト結果を表示`を選択することで先ほど実行した`ドリフト検出`の結果を確認できます。

今回は`EC2Subnet1`のタグの修正と`EC2Subnet2`リソースを手動で削除しているので、それぞれ`MODIFIED`、`DELETED`と検出されていることが確認できます。

![capture_29092020_082201.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/ffab0aae-626c-4840-1c79-6f0e1a0b062d.jpeg)

コマンドの場合は以下。

```shell:ドリフト結果の表示
aws cloudformation describe-stack-resource-drifts --stack-name stack-test
```

<details><summary>コマンド実行結果（展開して下さい）</summary><div>

```json:コマンド実行結果
{
    "StackResourceDrifts": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:335417439444:stack/stack-test/d5249730-01dc-11eb-8e2a-0af1581a9dde", 
            "ActualProperties": "{\"CidrBlock\":\"172.24.0.0/24\",\"Tags\":[{\"Key\":\"Name\",\"Value\":\"cf_Subnet1\"}],\"VpcId\":\"vpc-0d228fc345e494e45\"}", 
            "ResourceType": "AWS::EC2::Subnet", 
            "Timestamp": "2020-09-28T23:20:59.182Z", 
            "PhysicalResourceId": "subnet-0c76cd08741e9b6af", 
            "StackResourceDriftStatus": "MODIFIED", 
            "ExpectedProperties": "{\"CidrBlock\":\"172.24.0.0/24\",\"Tags\":[{\"Key\":\"Name\",\"Value\":\"cf_Subnet\"}],\"VpcId\":\"vpc-0d228fc345e494e45\"}", 
            "PropertyDifferences": [
                {
                    "PropertyPath": "/Tags/0/Value", 
                    "ActualValue": "cf_Subnet1", 
                    "ExpectedValue": "cf_Subnet", 
                    "DifferenceType": "NOT_EQUAL"
                }
            ], 
            "LogicalResourceId": "EC2Subnet1"
        }, 
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:335417439444:stack/stack-test/d5249730-01dc-11eb-8e2a-0af1581a9dde", 
            "ResourceType": "AWS::EC2::Subnet", 
            "Timestamp": "2020-09-28T23:20:59.746Z", 
            "PhysicalResourceId": "subnet-04755c355840e4956", 
            "StackResourceDriftStatus": "DELETED", 
            "LogicalResourceId": "EC2Subnet2"
        }, 
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:335417439444:stack/stack-test/d5249730-01dc-11eb-8e2a-0af1581a9dde", 
            "ActualProperties": "{\"CidrBlock\":\"172.24.0.0/16\"}", 
            "ResourceType": "AWS::EC2::VPC", 
            "Timestamp": "2020-09-29T22:21:03.431Z", 
            "PhysicalResourceId": "vpc-0d228fc345e494e45", 
            "StackResourceDriftStatus": "IN_SYNC", 
            "ExpectedProperties": "{\"CidrBlock\":\"172.24.0.0/16\"}", 
            "PropertyDifferences": [], 
            "LogicalResourceId": "EC2VPC1"
        }
    ]
}
```
</div></details>

また、上記ドリフトの画面からリソースを選択し、`ドリフトの詳細の表示`をクリックすると`JSON`形式ですがテンプレートとの差分を確認することができるので、実際にテンプレートを修正する場合に便利です。

![capture_29092020_082340.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/c77ccb7b-32a3-9190-da7d-f3f731896130.jpeg)

### ドリフト検出の注意点
今回、手動修正した個所は`EC2Subnet1`のタグ修正、`EC2Subnet2`のリソース削除、**`EC2VPC1`のタグ追加**ですが、上記の画像を見ると`EC2VPC1`のタグ追加が検出されておりません。

はっきり調べていませんが、`ドリフト検出`はテンプレートにもともと記載されている設定に対する検出なので、記載されていた設定値の変更は検出できても、そもそも記載されていなかった設定を検出することはできないようです。

そのような設定変更を検出するためには`AWS Config`で検出する必要があるのかな？

## 変更セットの更新
テンプレートとパラメータファイルからも`EC2VPC1`と`EC2Subnet1`のタグ追加・修正と`EC2Subnet2`リソースを削除して先ほどと同様`変更セット`を実行します。

![capture_30092020_074702.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/afcc706c-e1ad-2c7b-f8c7-bfbe01d39c89.jpeg)

コマンドから実行する場合も、先ほどと同様のコマンドで実行できます。

```shell:変更セットの作成
aws cloudformation create-change-set --change-set-name change-set-test --template-body file:///home/ec2-user/cf.yaml --stack-name stack-test --parameters file:///home/ec2-user/param.json
```

```shell:変更セットの実行
aws cloudformation execute-change-set --change-set-name change-set-test --stack-name stack-test
```

# おわりに
`変更セット`、`ドリフト検出`をこまめに行うことでシステムを運用していると常に付きまとう設計書と実機の差分を減らすことができるのは非常にありがたいですね。
※`CloudFormation`で管理されていない設定については検出できませんが。

次回は`スタックのネスト`を試してみようと思います。