既存リソースをAWS CloudFormation管理に取り込んでみよう 〜AWS CloudShellを添えて〜

ごきげんよう、@An_nAです！
この記事は[ハンズラボ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/handslab) 19日目の記事です。

# この記事について
今回は、
AWS上に存在している既存リソースをスタックに取り込んでAWS CloudFormationで管理しちゃおうぜ！
という内容でお送りします。

みなさん、AWS CloudFormation(以降、CloudFormationと表記します)は活用していますか？
ここで、突然ですが2020年03月末時点のわたしのCloudFormationに対するつぶやきを見てみましょう。
> たとえば、
初対面では「なんだかあんまり好きじゃないなあ」って人を、
付き合ってるうちに深く理解して大好きになったり
逆に「なんだかこの人いいなあ」と思ってた人が、
その人を知るにつれ「ナンダコイツ」になったりすることって
ままあると思うんですよね
今のわたしとCloudFormationの関係がまさに
「ほぼ初対面、なんだかあんまり好きじゃないなぁ」状態
というわけで、彼を理解するためにユーザーガイドをちゃんと読むところから始めたいと思います

その後、なんとか彼(？)と距離を縮め、後輩向けにCloudFormationの基本をまとめた資料を作れるくらいにはなりました。AWS マネジメントコンソールをポチポチせずにリソース管理できたり、一つのテンプレートを複数の環境で使い回しが出来たり、思い切って環境まるっと削除できたりと、慣れると便利ですよね。

だがしかし、過去のわたしはそうは思っていなかったのであった...。
AWS上にはおててでポチポチ作った、スタック管理されていないリソースがゴロゴロと...！
というわけで、自分自身の成長を噛み締めつつ、
スタック管理されていないはぐれリソースをスタック管理下に入れていきましょう！

# インポート3兄弟のご紹介
ところで、既存リソースの CloudFormation 管理への取り込み＝インポート作業には大きく分けて
以下の3種類があります。

* 既存のリソースを新規スタックの管理下にする
* 既存のリソースを既存のスタックの管理下にする
* あるスタック管理下のリソースを別スタックの管理下にする

今回は、この中で一番基本となる「既存のリソースを新規スタックの管理下にする」をご紹介していきます。
これができればあと2種類は応用なので、[公式サイト](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/resource-import.html)なんかを見つつやってみてくださいね！

# AWS CloudShellを使ってみよう
今回、CloudFormationでのスタック操作はすべてコマンドラインで行います。
そこで！！！
せっかくなのでリリースされたばっかりのAWS CloudShellを使ってみたいと思います！

マネジメントコンソールにログインしたら、下画像の赤丸部分をぽちー！
![スクリーンショット 2020-12-16 19.15.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624944/8587c6c7-1df5-a90c-bea3-374aff1f633c.png)

次の画面で「Close」を押して...
![スクリーンショット 2020-12-16 19.20.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624944/8b30a26b-4b69-d2bc-89ce-8f07f12cb63c.png)

しばらく待つと、ハイ！使えるようになりました！
![スクリーンショット 2020-12-16 19.22.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624944/51d53076-e4ba-5919-0c5b-177e2ed09d65.png)

AWS CLIのインストールや認証情報の設定を行わずにコマンドラインが使えて最高ですね！

# 既存リソースを新規スタックの管理下にしよう
さて、本題に入っていきましょう。

## 事前準備
先ほど、スタック管理されていないリソースがゴロゴロあると言いましたが、ごめんなさい、あれは嘘です。
なので、まずは既存リソースに見立てたS3バケットを一個作成します。こんな感じ。
ごくごくシンプルなS3バケットを作成しました。
![スクリーンショット 2020-12-17 18.39.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624944/b649e858-be3d-3cb6-0670-87d5d90a6d25.png)
ついでに、スタックの一覧も確認しておきましょう。
（ここからはスクショではなくテキストを貼っていきますが、コマンドは全てAWS CloudShell上で実行しています）

```
$ aws cloudformation list-stacks
{
    "StackSummaries": []
}
$
```
今は一つもスタックが無いですね。

## テンプレート作成
CloudFormationで使用するテンプレートを作成します。
さきほど作成したS3バケットの情報を書いていきましょう！


```(template.yml)
AWSTemplateFormatVersion: '2010-09-09'
Description: import test

Resources:
  PekePekeBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: 'Retain'
    Properties:
      BucketName: 'pekepeke-no-bucket'
```
シンプルなS3バケット用のテンプレートなので、記述もごくシンプルです。
だがしかし、大切なポイントが2つあります！順に見ていきましょう。

### DeletionPolicyはRetainにする
インポート作業を実施する場合、テンプレートで明示的に`DeletionPolicy`を`Retain`にする必要があります。

DeletionPolicyを指定すると、スタックが削除された時にリソースを削除するかそれとも保持するかを制御できます。指定しなかった場合、基本的にはスタックを削除するとリソースも削除されます（一部例外があって、スナップショットがデフォルトのものもあります）。

DeletionPolicyをRetainにすると、スタックが削除されてもリソースは削除されません。
「えー、スタック消したらリソースにも消えてほしい」という方は、インポート作業のあとにテンプレートから`DeletionPolicy: 'Retain'`を削除してスタックを更新してあげればOKですよ。

### BucketNameを間違えない
各リソースには、リソースを特定するための識別子があります。
インポート作業を行う際、インポートしたいリソースの識別子と、その識別子の値が必要になります。
S3バケットの場合は、S3バケット名が識別子なので、テンプレートに書いてあるS3バケット名が間違っているとインポートできません！間違いのないよう記述しましょう。

## テンプレートアップロード
テンプレートが書けたら、AWS CloudShellにアップロードしましょう！
ファイルのアップロードはとっても簡単、AWS CloudShellの右上にある「Actions」から「Upload file」を選んで...

![スクリーンショット 2020-12-17 19.04.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624944/e5fe5423-9725-edd7-68da-ceedf02a6755.png)

「Select File」して...「Upload」ボタンを押せば...
![スクリーンショット 2020-12-17 19.05.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624944/89f11ddf-be35-6ca1-fe2a-8ac3f500df4b.png)

できあがり！
![スクリーンショット 2020-12-17 19.06.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/624944/60263896-5078-ee64-e5fb-70b13fc1b16f.png)

```
$ pwd
/home/cloudshell-user
$ 
$ ls -l
total 4
-rw-rw-r-- 1 cloudshell-user cloudshell-user 204 Dec 17 10:05 template.yml
$ 
```

ファイルがアップロードできていますね！

## いざ！インポート！
さて、ここまで出来たら、あとは粛々とコマンドを打っていきますよ！
まずは、さきほども出てきた識別子と値を、ここでもう一回確認します。
あとで`ResourceType`と`LogicalResourceIds`、`ResourceIdentifiers`を使うので控えておいてくださいね！

```
$ aws cloudformation get-template-summary \
> --template-body file://$PWD/template.yml
{
    "Parameters": [],
    "Description": "import test",
    "ResourceTypes": [
        "AWS::S3::Bucket"
    ],
    "Version": "2010-09-09",
    "ResourceIdentifierSummaries": [
        {
            "ResourceType": "AWS::S3::Bucket",
            "LogicalResourceIds": [
                "PekePekeBucket"
            ],
            "ResourceIdentifiers": [
                "BucketName"
            ]
        }
    ]
}
$ 
```

つぎ！変更セットを作成していきます。
スタック名や変更セット名はお好きなお名前でどうぞ。
`--resources-to-import`がだいぶややこしいですが、先ほど確認したリソース識別子を一つずつ当てはめていってみてください。
`--template-body`はさきほどアップロードしたテンプレートを指定してあげてくださいね。

```
$ aws cloudformation create-change-set \
> --stack-name pekepekeStack --change-set-name ImportChangeSet \
> --change-set-type IMPORT \
> --resources-to-import "[{\"ResourceType\":\"AWS::S3::Bucket\",\"LogicalResourceId\":\"PekePekeBucket\",\"ResourceIdentifier\":{\"BucketName\":\"pekepeke-no-bucket\"}}]" \
> --template-body file://$PWD/template.yml
{
    "Id": "arn:aws:cloudformation:ap-northeast-1:************:changeSet/ImportChangeSet/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/pekepekeStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
$ 
```

ちなみに、`DeletionPolicy: 'Retain'`を指定していなかった場合、ここで

```
An error occurred (ValidationError) when calling the CreateChangeSet operation: The following resources to import [PekePekeBucket] must have DeletionPolicy attribute specified in the template.
```
と怒られます...。

さて、変更セットができたか確認してみましょう！

```
$ aws cloudformation list-change-sets --stack-name pekepekeStack
{
    "Summaries": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/pekepekeStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
            "StackName": "pekepekeStack",
            "ChangeSetId": "arn:aws:cloudformation:ap-northeast-1:************:changeSet/ImportChangeSet/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
            "ChangeSetName": "ImportChangeSet",
            "ExecutionStatus": "AVAILABLE",
            "Status": "CREATE_COMPLETE",
            "CreationTime": "2020-12-17T10:12:18.740000+00:00"
        }
    ]
}
$ 
```

うむうむ、大丈夫そうですね。変更セットの中身も確認してみましょう！

```
$ aws cloudformation describe-change-set \
> --change-set-name "arn:aws:cloudformation:ap-northeast-1:************:changeSet/ImportChangeSet/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
{
    "Changes": [
        {
            "Type": "Resource",
            "ResourceChange": {
                "Action": "Import",
                "LogicalResourceId": "PekePekeBucket",
                "PhysicalResourceId": "pekepeke-no-bucket",
                "ResourceType": "AWS::S3::Bucket",
                "Scope": [],
                "Details": []
            }
        }
    ],
    "ChangeSetName": "ImportChangeSet",
    "ChangeSetId": "arn:aws:cloudformation:ap-northeast-1:************:changeSet/ImportChangeSet/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/pekepekeStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "StackName": "pekepekeStack",
    "Description": null,
    "Parameters": null,
    "CreationTime": "2020-12-17T10:12:18.740000+00:00",
    "ExecutionStatus": "AVAILABLE",
    "Status": "CREATE_COMPLETE",
    "StatusReason": null,
    "NotificationARNs": [],
    "RollbackConfiguration": {},
    "Capabilities": [],
    "Tags": null
}
$ 
```

`"Changes"`の中身に特に注目です！
`"Action": "Import"`になってますし、対象となっているリソースも間違いないですね。良いのではないでしょうか！
ここで一旦スタック`pekepekeStack`の管理リソースを見てみましょう。

```
$ aws cloudformation list-stack-resources --stack-name pekepekeStack
{
    "StackResourceSummaries": []
}
$ 
```

なんもないですね。
では！いざ！変更セットを実行してみます！

```
$ aws cloudformation execute-change-set \
> --change-set-name "arn:aws:cloudformation:ap-northeast-1:************:changeSet/ImportChangeSet/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
$ 
```

さてさてどうなったかな？
再度スタック`pekepekeStack`の管理リソースを見てみましょう。

```
$ aws cloudformation list-stack-resources --stack-name pekepekeStack
{
    "StackResourceSummaries": [
        {
            "LogicalResourceId": "PekePekeBucket",
            "PhysicalResourceId": "pekepeke-no-bucket",
            "ResourceType": "AWS::S3::Bucket",
            "LastUpdatedTimestamp": "2020-12-17T10:17:45.919000+00:00",
            "ResourceStatus": "UPDATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
$ 
```

どーん！！
`pekepeke-no-bucket`がスタック管理下になりました！ヤッター！！
はぐれリソースだったS3バケット`pekepeke-no-bucket`は、無事`pekepekeStack`の仲間入りを果たしました！

# おまけ：スタックが消えたらリソースも消えてほしい！という方へ
この時点では、`DeletionPolicy: 'Retain'`なので、スタックを削除してもインポートしたリソースは削除されません。スタックが消えたらリソースも消えてほしい、という場合は以下の手順を実施しましょう！

まずは、テンプレートから`DeletionPolicy: 'Retain'`を削除します（以下の例ではお手軽にコメントアウトしました）。

```(template.yml)
AWSTemplateFormatVersion: '2010-09-09'
Description: import test

Resources:
  PekePekeBucket:
    Type: AWS::S3::Bucket
    # DeletionPolicy: 'Retain'
    Properties:
      BucketName: 'pekepeke-no-bucket'
```

スタックを更新します。
以下の例ではいきなり更新していますが、変更セットの作成＆実行でももちろんOKです！

```
$ aws cloudformation update-stack --stack-name pekepekeStack \
> --template-body file://$PWD/template.yml 
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/pekepekeStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
$ 
```

スタックの更新が無事完了していることを確認。これで作業完了です！

```
$ aws cloudformation list-stack-resources --stack-name pekepekeStack
{
    "StackResourceSummaries": [
        {
            "LogicalResourceId": "PekePekeBucket",
            "PhysicalResourceId": "pekepeke-no-bucket",
            "ResourceType": "AWS::S3::Bucket",
            "LastUpdatedTimestamp": "2020-12-17T10:21:32.607000+00:00",
            "ResourceStatus": "UPDATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
$ 
```

本当にスタック削除でリソースも消えるか確認してみましょう。
まずは現在のS3バケットを確認。

```
$ aws s3 ls
2020-12-17 10:17:27 pekepeke-no-bucket
$ 
```

スタックを削除したら...

```
$ aws cloudformation delete-stack --stack-name pekepekeStack
$ 
$ aws cloudformation list-stacks
{
    "StackSummaries": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/pekepekeStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
            "StackName": "pekepekeStack",
            "TemplateDescription": "import test",
            "CreationTime": "2020-12-17T10:12:18.740000+00:00",
            "LastUpdatedTime": "2020-12-17T10:21:28.271000+00:00",
            "DeletionTime": "2020-12-17T10:24:50.744000+00:00",
            "StackStatus": "DELETE_COMPLETE",
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
$ 
```

もう一度、S3バケットを確認。消えてますね！

```
$ aws s3 ls
$
```

# おわりに
いかがでしたでしょうか？
記事がちょっと長くなっちゃったので煩雑に感じるかもしれませんが、実際にやってみると結構簡単です。
機会があったらお試ししてみてくださいね！

では、ごきげんよう！