[JAWS-UG CLI] CloudFormation:#1 はじめてのCloudFormation (S3webサイトホスティングの作成から削除まで)

http://jawsug-cli.doorkeeper.jp/events/13797 でのハンズオン資料です。

このハンズオンでは、CloudFormationを利用してS3上で以下の作業を行なってみます。

* バケットの作成
* バケットの変更 (Webサイトホスティングの設定を追加)
* バケットポリシーの作成
* バケットポリシーの削除
* バケットの削除

CloudFormationの学習が目的なので、最小のテンプレートでバケットを作成し、後からWebサイトホティングの設定をします。
(通常はこの2つの作業内容を1つのテンプレートに記述して1回で実行します。)
更に、バケットポリシーは独立したスタックとして作成します。
最後に各スタックを削除して終了とします。

CloudFormationとしての作業は下記になります。

* スタックの作成 (最低限の項目によるS3バケットの作成)
* スタックの更新 (webサイトホスティングの設定を追加)
* 別のスタックの作成 (バケットポリシー)
* バケットポリシースタックの削除
* バケットスタックの削除


前提条件
========

* S3へのフルアクセス権限


0. 事前作業
=================

0.1. S3バケット名の決定
------------------------

S3バケット名を決めておいてください。

```bash:コマンド:
   $ CF_BUCKET_NAME='web-example'
```

バケット名はS3全体でユニークである必要があります。



0.2. コンテンツ用ディレクトリの作成
---------------------------------

webサイトホスティングの動作確認用コンテンツを置くためのディレクトリを作成します。

```bash:コマンド:
  $ CF_CONTENTS_DIR="${HOME}/tmp/contents"
  $ mkdir -p ${CF_CONTENTS_DIR}
```

0.3. 作業用ディレクトリの作成
----------------------------

テンプレートを作成する作業用ディレクトリを作成します。

```bash:コマンド:
   $ WORK_DIR="${HOME}/tmp/aws_cf"
   $ mkdir -p ${WORK_DIR}
   $ cd ${WORK_DIR}
```


1. S3バケット作成
=================

まず最初に、create-stackサブコマンドの実習として、S3のバケットを作成してみます。

1.1. テンプレートの作成 (S3バケット作成)
----------------------------------------

S3バケットを作成するCloudFormationテンプレートを作成します。

```bash:コマンド:
   $ FILE_CF_TEMPLATE="${CF_BUCKET_NAME}-bucket.json"
   $ CF_RESOURCE_ID='S3Bucket'
   $ CF_DESCRIBE='CloudFormation web bucket'
```

```bash:コマンド:
$ cat << EOF > ${FILE_CF_TEMPLATE}
{
     "AWSTemplateFormatVersion" : "2010-09-09",
     "Resources" : {
       "${CF_RESOURCE_ID}" : {
         "Type" : "AWS::S3::Bucket",
         "Properties" : {
            "BucketName" : "${CF_BUCKET_NAME}"
         }
       }
     },
     "Description": "${CF_DESCRIBE}"
}
EOF
```

JSONファイルを作成したら、フォーマットが壊れてないか必ず確認します。

```bash:コマンド:
   $ cat ${FILE_CF_TEMPLATE} | json_verify
```

```text:結果:
   JSON is valid
```


1.2. S3バケットの作成
---------------------

実際にCloudFormationでS3バケットを作成してみましょう。

最初に、スタック名を決めます。

```bash:コマンド:
   $ CF_STACK_NAME="${CF_BUCKET_NAME}-bucket" && echo ${CF_STACK_NAME}
```

```text:結果:
web-example-bucket
```

次に、実際にスタックを実行して、S3バケットを作成します。

```bash:コマンド:
   $ aws cloudformation create-stack --stack-name ${CF_STACK_NAME} --template-body file://${FILE_CF_TEMPLATE}
```

```json:結果:
   {
       "StackId": "arn:aws:cloudformation:us-west-1:XXXXXXXXXXXX:stack/web-example-bucket/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
   }
```


1.3. スタックの状況確認
-----------------------

list-stacksコマンドで、スタックの状況を確認してみましょう。

```bash:コマンド:
   $ aws cloudformation list-stacks \
     | jq -r --arg stackname ${CF_STACK_NAME} '.StackSummaries[] | select(.StackName == $stackname) | .StackStatus'
```

```text:結果:
CREATE_COMPLETE
```

StackStatusが'CREATE_COMPLETE'になっていれば作成は完了です。

それ以外が表示されている場合は、下記コマンドでFailedの文字が出ている前後を見て原因を調べます。

```bash:コマンド
$ aws cloudformation describe-stack-events --stack-name ${CF_STACK_NAME}
```


1.4. S3バケットの確認
---------------------

実際にS3バケットが存在することを確認しましょう。

```bash:コマンド:
   $ aws s3 ls | grep ${CF_BUCKET_NAME}
```

```text:結果:
   2014-09-09 15:19:34 web-example
```


1.5. スタックの内容確認
-----------------------

get-templateサブコマンドでスタックの内容を確認します。

```bash:コマンド:
   $ aws cloudformation get-template --stack-name ${CF_STACK_NAME}
```

```json:結果:
   {
       "TemplateBody": {
           "AWSTemplateFormatVersion": "2010-09-09", 
           "Description": "CloudFormation web bucket", 
           "Resources": {
               "S3BucketWeb": {
                   "Type": "AWS::S3::Bucket", 
                   "Properties": {
                       "BucketName": "web-example"
                   }
               }
           }
       }
   }
```

1.6. バケットのURL取得
-----------------------

作成したバケットのアクセスURL(HTTP)を取得します。

```bash:コマンド:
  $ CF_BUCKET_URL="${CF_BUCKET_NAME}.s3-website-`aws s3api get-bucket-location --bucket=${CF_BUCKET_NAME} --query LocationConstraint --output text`.amazonaws.com" \
  && echo ${CF_BUCKET_URL}
```

```text:結果:
web-example.s3-website-us-west-1.amazonaws.com
```



2. コンテンツの転送
===================

確認用コンテンツを作成して、バケットに転送します。


2.1. コンテンツの作成
---------------------

確認用コンテンツをコンテンツ用ディレクトリで作成します。

```bash:コマンド:
$ cd ${CF_CONTENTS_DIR}/
```

```bash:コマンド:
$ cat << EOF > ${CF_CONTENTS_DIR}/index.html
<html>
 <head>
   <title>CF</title>
 </head>
 <body>
   <h1>CF</h1>
 </body>
 <html>
EOF
```


2.2. コンテンツの同期
---------------------

確認用コンテンツをS3バケットに転送します。

```bash:コマンド:
   $ aws s3 sync . "s3://${CF_BUCKET_NAME}/"
```

```text:結果
upload: ./index.html to s3://web-example/index.html
```


2.3. コンテンツの確認
---------------------

S3バケット上に転送されたことを確認します。

```bash:コマンド:
   $ aws s3 ls s3://${CF_BUCKET_NAME}/
```

```text:結果:
   2014-09-21 19:52:50         78 index.html
```


3. Webサイトホスティングの設定
==============================

update-stackサブコマンドの実習として、S3バケットにWebサイトホスティング設定を反映してみます。


3.1. テンプレートの作成 (S3バケット更新)
----------------------------------------

最初に、更新用のテンプレートを作成します。

```bash:コマンド:
   $ FILE_CF_TEMPLATE="${CF_BUCKET_NAME}-bucket-website.json"
```

```bash:コマンド:
$ cd ${WORK_DIR}
```

```bash:コマンド:
$ cat << EOF > ${FILE_CF_TEMPLATE}
{
     "AWSTemplateFormatVersion": "2010-09-09",
     "Resources" : {
       "${CF_RESOURCE_ID}" : {
         "Type" : "AWS::S3::Bucket",
         "Properties" : {
           "BucketName" : "${CF_BUCKET_NAME}",
           "WebsiteConfiguration": {
            "IndexDocument": "index.html",
            "ErrorDocument": "error.html"
           }
         }
       }
     },
     "Description": "${CF_DESCRIBE}"
}
EOF
```

テンプレートは上書き更新されるようなので、更新前の情報も含めておかないと、記述の無い部分は消えてしまいます。

JSONファイルを作成したら、フォーマットが壊れてないか必ず確認します。

```bash:コマンド:
   $ cat ${FILE_CF_TEMPLATE} | json_verify
```

```text:結果:
   JSON is valid
```


3.2. S3バケットの更新
---------------------

次に、実際にバケットの設定を更新します。

```bash:コマンド:
   $ aws cloudformation update-stack --stack-name ${CF_STACK_NAME} --template-body file://${FILE_CF_TEMPLATE}
```

```json:結果:
   {
       "StackId": "arn:aws:cloudformation:us-west-1:XXXXXXXXXXXX:stack/web-example-bucket/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
   }
```


3.3. スタックの状況確認
-----------------------

list-stacksサブコマンドで更新状況を確認します。

```bash:コマンド:
   $ aws cloudformation list-stacks \
     | jq -r --arg stackname ${CF_STACK_NAME} '.StackSummaries[] | select(.StackName == $stackname) | .StackStatus'
```

```text:結果:
UPDATE_COMPLETE
```

StackStatusが'UPDATE_COMPLETE'になっていれば更新は完了です。

それ以外が表示されている場合は、下記コマンドでFailedの文字が出ている前後を見て原因を調べます。

```bash:コマンド
$ aws cloudformation describe-stack-events --stack-name ${CF_STACK_NAME}
```


3.4. S3バケットのWebサイトホスティング設定確認
----------------------------------------------

```bash:コマンド:
   $ aws s3api get-bucket-website --bucket ${CF_BUCKET_NAME}
```

```json:結果:
   {
     "RedirectAllRequestsTo": {},
     "IndexDocument": {
         "Suffix": "index.html"
     },
     "ErrorDocument": {
         "Key": "error.html"
     },
     "RoutingRules": []
   }
```


4. S3バケットポリシーの設定
===========================

Webサイトコンテンツとして外部からアクセスできるように、S3バケットポリシーの設定を行います。
ここでは、新規スタックを追加してみます。


4.1. テンプレートの作成 (S3バケットポリシー作成)
------------------------------------------------

まず、CloudFormation用のテンプレートを作成します。

```bash:コマンド:
   $ FILE_CF_TEMPLATE="${CF_BUCKET_NAME}-bucketpolicy.json"
   $ CF_RESOURCE_ID='S3BucketPolicy'
   $ CF_DESCRIBE='CloudFormation web bucket policy'
```

```bash:コマンド:
$ cat << EOF > ${FILE_CF_TEMPLATE}
{
     "AWSTemplateFormatVersion": "2010-09-09",
     "Resources" : {
       "${CF_RESOURCE_ID}" : {
         "Type" : "AWS::S3::BucketPolicy",
         "Properties" : {
           "Bucket" : "${CF_BUCKET_NAME}",
           "PolicyDocument" : {
             "Version" : "2012-10-17",
             "Statement" : [
               {
                 "Action" : ["s3:GetObject"],
                 "Effect":"Allow",
                 "Principal": {
                   "AWS" : [
                     "*"
                   ]
                 },
                 "Resource":["arn:aws:s3:::${CF_BUCKET_NAME}/*"],
                 "Sid":"AddPerm"
               }
             ]
           }
         }
       }
     },
     "Description": "${CF_DESCRIBE}"
}
EOF
```

JSONファイルを作成したら、フォーマットが壊れてないか必ず確認します。


```bash:コマンド:
   $ cat ${FILE_CF_TEMPLATE} | json_verify
```

```text:結果:
   JSON is valid
```


4.2. S3バケットポリシーの作成
-----------------------------

実際にCloudFormationでS3バケットポリシーを作成してみましょう。

まず、スタック名を決めます。

```bash:コマンド:
   $ CF_STACK_NAME="${CF_BUCKET_NAME}-bucketpolicy" && echo ${CF_STACK_NAME}
```

```text:結果:
web-example-bucketpolicy
```

次に、実際にスタックを作成します。

```bash:コマンド:
   $ aws cloudformation create-stack --stack-name ${CF_STACK_NAME} --template-body file://${FILE_CF_TEMPLATE}
```

```json:結果:
   {
       "StackId": "arn:aws:cloudformation:us-west-1:XXXXXXXXXXXX:stack/web-example-bucketpolicy/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
   }
```


4.3. スタックの状況確認
-----------------------

list-stacksコマンドで、スタックの状況を確認してみましょう。

```bash:コマンド:
   $ aws cloudformation list-stacks \
     | jq -r --arg stackname ${CF_STACK_NAME} '.StackSummaries[] | select(.StackName == $stackname) | .StackStatus'
```

```text:結果:
CREATE_COMPLETE
```

StackStatusが'CREATE_COMPLETE'になっていれば作成は完了です。

それ以外が表示されている場合は、下記コマンドでFailedの文字が出ている前後を見て原因を調べます。

```bash:コマンド
$ aws cloudformation describe-stack-events --stack-name ${CF_STACK_NAME}
```


4.4. S3バケットポリシーの確認
-----------------------------

S3バケットにバケットポリシーが実際に設定されていることを確認してみましょう。

```bash:コマンド:
   $ aws s3api get-bucket-policy --bucket ${CF_BUCKET_NAME}
```

```json:結果:
   {
     "Policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"AddPerm\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::web-example/*\"}]}"
   }
```


5. コンテンツへのアクセス
=========================

最後に、ブラウザでS3のコンテンツにアクセスできることを確認してみましょう。

URLは下記のコマンドで確認できます。


```bash:コマンド:
   echo ${CF_BUCKET_URL}
```


6. 後始末1 (S3バケットポリシーの削除)
====================================

delete-stackサブコマンドの実習として、S3バケットポリシーのスタックを削除してみます。


6.1. スタックの一覧
--------------

まず、スタック一覧から削除するスタックのスタック名を確認します。

```bash:コマンド:
   $ aws cloudformation list-stacks \
     | jq '.StackSummaries[] | select(.StackStatus != "DELETE_COMPLETE" )'
```


6.2. 削除対象の確定
--------------

次に、削除するスタック名と、対象となるS3バケット名を特定します。

```bash:コマンド:
   $ S3_BUCKET_NAME="web-example"
   $ CF_STACK_NAME="web-example-bucketpolicy"
```


6.3. スタックの削除
--------------

実際に、スタックを削除してみます。

```bash:コマンド:
   $ aws cloudformation delete-stack --stack-name ${CF_STACK_NAME}
```

```bash:コマンド:
   (戻り値なし)
```


6.4. スタックのステータス確認
------------------------

list-stacksサブコマンドで削除状況を確認します。

```bash:コマンド:
   $ aws cloudformation list-stacks \
     | jq -r --arg stackname ${CF_STACK_NAME} '.StackSummaries[] | select(.StackName == $stackname) | .StackStatus'
```

```bash:コマンド:
   DELETE_COMPLETE
```

StackStatusが'DELETE_COMPLETE'になっていれば削除は完了です。

それ以外が表示されている場合は、下記コマンドでFailedの文字が出ている前後を見て原因を調べます。

```bash:コマンド
$ aws cloudformation describe-stack-events --stack-name ${CF_STACK_NAME}
```


6.5. ポリシーの確認
--------------

```bash:コマンド:
   $ aws s3api get-bucket-policy --bucket ${S3_BUCKET_NAME}
```

S3のバケットポリシーを別スタックで追加した場合、スタックを削除してもポリシーは変更ないようです。(詳細未確認)


6.6. コンテンツへのアクセス
--------------

コンテンツにも、引き続きアクセスできるようです。(詳細未確認)



7. 後始末2 (S3バケットの削除)
============================

最後に、後始末としてS3バケットを削除します。


7.1. スタックの一覧
--------------

まず、スタック一覧から削除するスタックのスタック名を確認します。

```bash:コマンド:
   $ aws cloudformation list-stacks \
     | jq '.StackSummaries[] | select(.StackStatus != "DELETE_COMPLETE" )'
```


7.2. 削除対象の確定
--------------

次に、削除するスタック名と、対象となるS3バケット名を特定します。

```bash:コマンド:
   $ S3_BUCKET_NAME='web-example'
   $ CF_STACK_NAME='web-example-bucket'
```


7.3. バケットの確認
--------------

バケットは中身が空でないと削除できないので、バケットにファイルがないか確認します。

```bash:コマンド:
   $ aws s3 ls s3://${S3_BUCKET_NAME}/
```

```bash:コマンド:
   2014-09-09 15:19:34 web-example
```


7.4. バケットの中身削除
------------------

S3バケット内にファイルが存在する場合は、削除します。

```bash:コマンド:
   $ aws s3 rm s3://${S3_BUCKET_NAME}/ --recursive
```



7.5. スタックの削除
--------------

実際に、スタックを削除してみます。


```bash:コマンド:
   $ aws cloudformation delete-stack --stack-name ${CF_STACK_NAME}
```

```text:結果:
   (戻り値なし)
```


7.6. スタックのステータス確認
------------------------

list-stacksサブコマンドで削除状況を確認します。


```bash:コマンド:
   $ aws cloudformation list-stacks \
     | jq -r --arg stackname ${CF_STACK_NAME} '.StackSummaries[] | select(.StackName == $stackname) | .StackStatus'
```

```bash:コマンド:
   DELETE_COMPLETE
```


StackStatusが'DELETE_COMPLETE'になっていれば削除は完了です。

それ以外が表示されている場合は、下記コマンドでFailedの文字が出ている前後を見て原因を調べます。

```bash:コマンド
$ aws cloudformation describe-stack-events --stack-name ${CF_STACK_NAME}
```


7.7. バケットの確認
--------------

削除対象のバケットが存在しないことを確認します。

```bash:コマンド:
   $ aws s3 ls s3://${S3_BUCKET_NAME}/
```

```text:結果:
A client error (NoSuchBucket) occurred when calling the ListObjects operation: The specified bucket does not exist
```

7.8. スタックの一覧
--------------

list-stacksサブコマンドで他に削除するものがないか確認します。

```bash:コマンド:
   $ aws cloudformation list-stacks \
     | jq '.StackSummaries[] | select(.StackStatus != "DELETE_COMPLETE" )'
```

おわり
=====================

この手順で、stackの作成、一覧、更新、削除ができるようになりました。
(あくまでも実習用なので実用的ではないですが;-)