[JAWS-UG CLI] CloudFormation:#3 S3でwebサイトホスティング (独自ドメイン)

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

複数のテンプレートを利用して、S3上に独自ドメインでの静的webサイトを構築します。




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

* S3へのフルアクセス権限
* http://qiita.com/tcsh/items/2ae34c849322f9cc9584  の手順でCloudFormationのテンプレートを置くためのS3バケットを作成済であること。



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

0.1. 公開用バケット名の決定
---------------------------

S3バケット名を決めておいてください。独自ドメインで公開する場合は、公開するホスト名にしておく必要があります。

```bash:コマンド:
   $ S3_BUCKET_NAME='www.example.jp'
```


0.2. 共通テンプレート用バケット名の決定
------------------------------------

http://qiita.com/tcsh/items/2ae34c849322f9cc9584  の手順で作成した、CloudFormationのテンプレートを置くためのS3バケット名を指定します。

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

```text:結果
   s3-us-west-1.amazonaws.com/cf-example
```

このURLは、個別テンプレートから共有テンプレートを呼び出すときに利用します。



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

個別テンプレートを置くためのディレクトリを作成します。

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


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

共通テンプレートを置くためのディレクトリを作成します。ここにあるテンプレートをS3バケットに転送します。

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




1. 共通テンプレート作成
=======================


1.1. web siteバケット用テンプレート
-----------------------------------

CloudFormationテンプレートを作成します。

```bash:コマンド:
   $ FILE_CF_TEMPLATE_BUCKET='cf-s3-bucket-website.template'
```

```bash:コマンド:
$ cat << EOF > ${FILE_CF_TEMPLATE_BUCKET}
{
     "AWSTemplateFormatVersion" : "2010-09-09",

     "Parameters" : {
       "BucketName" : {
         "Type" : "String",
         "Default" : "",
         "Description" : ""
       }
     },

     "Resources" : {
       "S3Bucket" : {
         "Type" : "AWS::S3::Bucket",
         "Properties" : {
           "BucketName" : { "Ref" : "BucketName" },
           "WebsiteConfiguration": {
             "IndexDocument": "index.html",
             "ErrorDocument": "error.html"
           }
         }
       }
     },

     "Description" : "website bucket template",

     "Outputs" : {
       "S3Bucket" : {
         "Value" : { "Ref" : "S3Bucket" }
       }
     }

}
EOF
```

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


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

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

validate-templateサブコマンドで、簡単な検証ができます。

```bash:コマンド:
   $ aws cloudformation validate-template --template-body file://${FILE_CF_TEMPLATE_BUCKET}
```

```json:結果:
   {
     "Description": "website bucket template",
     "Parameters": [
         {
             "DefaultValue": null,
             "NoEcho": false,
             "Description": null,
             "ParameterKey": "BucketName"
         }
     ],
     "Capabilities": []
   }
```


1.2. web siteバケットポリシー用テンプレート
-------------------------------------------

CloudFormationテンプレートを作成します。

```bash:コマンド:
   $ FILE_CF_TEMPLATE_BUCKETPOLICY='cf-s3-bucketpolicy-website.template'
```

```bash:コマンド:
$ cat << EOF > ${FILE_CF_TEMPLATE_BUCKETPOLICY}
{
     "AWSTemplateFormatVersion" : "2010-09-09",

     "Description" : "website bucket-policy template",

     "Parameters" : {
       "BucketName" : {
         "Type" : "String",
         "Default" : "",
         "Description" : ""
       }
     },

     "Resources" : {
       "S3BucketPolicy" : {
         "Type" : "AWS::S3::BucketPolicy",
         "Properties" : {
           "Bucket" : { "Ref" : "BucketName" },
           "PolicyDocument" : {
             "Statement" : [{
               "Action" : "s3:GetObject",
               "Effect" : "Allow",
               "Principal" : { "AWS": "*" },
               "Resource" : {
                 "Fn::Join" : [
                   "",
                   ["arn:aws:s3:::", { "Ref" : "BucketName" }, "/*" ]
                 ]
               },
               "Sid":"AddPerm"
             }]
           }
         }
       }
     },

     "Outputs" : {
       "S3BucketPolicy" : {
         "Value" : { "Ref" : "S3BucketPolicy" }
       }
     }

}
EOF
```

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

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

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

validate-templateサブコマンドで、簡単な検証ができます。

```bash:コマンド:
   $ aws cloudformation validate-template --template-body file://${FILE_CF_TEMPLATE_BUCKETPOLICY}
```

```json:結果:
   {
     "Description": "website bucket-policy template",
     "Parameters": [
         {
             "DefaultValue": null,
             "NoEcho": false,
             "Description": null,
             "ParameterKey": "BucketName"
         }
     ],
     "Capabilities": []
   }
```


2. 共通テンプレート転送
=======================

作成した共通テンプレートを、共通テンプレート用S3バケットに転送します。

2.1. 共通テンプレートのテンプレート用バケットへの転送
---------------------------

テンプレートを、S3バケットに転送します。


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

```text:結果
upload: ./cf-s3-bucket-website.template to s3://cf-example/cf-s3-bucket-website.template
upload: ./cf-s3-bucketpolicy-website.template to s3://cf-example/cf-s3-bucketpolicy-website.template
```

2.2. 共通テンプレートの確認
---------------------------

テンプレートが転送されたことを確認します。

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

```text:結果:
2014-09-22 16:48:48        671 cf-s3-bucket-website.template
2014-09-22 16:48:48        997 cf-s3-bucketpolicy-website.template
```


3. 個別テンプレート作成
=======================

個別案件用のテンプレートを作成します。
先程作成した2つの共通テンプレートを呼び出して利用します。(チェーン実行)


3.1. 個別テンプレート作成 (S3バケット)
--------------------------------------

CloudFormationテンプレートを作成します。

```bash:コマンド:
   $ FILE_CF_TEMPLATE="${S3_BUCKET_NAME}-bucket.template"
   $ S3_DESCRIBE="${S3_BUCKET_NAME} website bucket"
```

```bash:コマンド:
$ cat << EOF > ${FILE_CF_TEMPLATE}
{
     "AWSTemplateFormatVersion" : "2010-09-09",

     "Resources" : {
       "Bucket" : {
         "Type" : "AWS::CloudFormation::Stack",
         "Properties" : {
           "TemplateURL" : "https://${CF_BUCKET_URL}/${FILE_CF_TEMPLATE_BUCKET}",
           "Parameters" : {
             "BucketName" : "${S3_BUCKET_NAME}"
           }
         }
       },

       "BucketPolicy" : {
         "Type" : "AWS::CloudFormation::Stack",
         "Properties" : {
           "TemplateURL" : "https://${CF_BUCKET_URL}/${FILE_CF_TEMPLATE_BUCKETPOLICY}",
           "Parameters" : {
             "BucketName" : "${S3_BUCKET_NAME}"
           }
         }
       }
     },

     "Outputs" : {
       "S3Bucket" : {
         "Value" : { "Fn::GetAtt" : ["Bucket", "Outputs.S3Bucket"]}
       },
       "S3BucketPolicy" : {
         "Value" : { "Fn::GetAtt" : ["BucketPolicy", "Outputs.S3BucketPolicy"]}
       }
     },

     "Description": "${S3_DESCRIBE}"
}
EOF
```

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

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

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


4. バケットの作成
=================


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

スタック名を定義します。

```bash:コマンド:
   $ CF_STACK_NAME="s3-bucket-`echo ${S3_BUCKET_NAME} | sed 's/\./-/g'`"; echo ${CF_STACK_NAME}
```


```text:結果:
   s3-bucket-www-example-jp
```

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/s3-bucket-www-example-jp/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
   }
```


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

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.3. スタックの内容確認
-----------------------

作成されたスタックの内容を確認します。

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

```json:結果:
{
    "TemplateBody": {
        "AWSTemplateFormatVersion": "2010-09-09", 
        "Outputs": {
            "S3Bucket": {
                "Value": {
                    "Fn::GetAtt": [
                        "Bucket", 
                        "Outputs.S3Bucket"
                    ]
                }
            }, 
            "S3BucketPolicy": {
                "Value": {
                    "Fn::GetAtt": [
                        "BucketPolicy", 
                        "Outputs.S3BucketPolicy"
                    ]
                }
            }
        }, 
        "Description": "www.example.jp website bucket", 
        "Resources": {
            "BucketPolicy": {
                "Type": "AWS::CloudFormation::Stack", 
                "Properties": {
                    "TemplateURL": "https://s3-us-west-1.amazonaws.com/cf-example/cf-s3-bucketpolicy-website.template", 
                    "Parameters": {
                        "BucketName": "www.example.jp"
                    }
                }
            }, 
            "Bucket": {
                "Type": "AWS::CloudFormation::Stack", 
                "Properties": {
                    "TemplateURL": "https://s3-us-west-1.amazonaws.com/cf-example/cf-s3-bucket-website.template", 
                    "Parameters": {
                        "BucketName": "www.example.jp"
                    }
                }
            }
        }
    }
  }
```


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

実際にS3バケットが作成されていることを確認しましょう。

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

```text:結果:
   2014-09-09 15:19:34 www.example.jp
```


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

作成されたS3バケットのバケットポリシーを確認しましょう。

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

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

上記の内容になっていれば、外部からコンテンツに対するアクセスを許可しています。



4.6. webサイトホスティングの設定
-----------------------------

作成されたS3バケットのwebサイトホスティング設定を確認しましょう。

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

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

上記の内容になっていれば、外部からのアクセスに対してコンテンツを返すことができます。


4.7. Outputsの確認
-----------------

describe-stacksサブコマンドでOutputsの出力を確認できます。

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

```json:結果:
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-west-1:XXXXXXXXXXXX:stack/s3-bucket-www-example-jp/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 
            "Description": "www-example.jp website bucket", 
            "Tags": [], 
            "Outputs": [
                {
                    "OutputKey": "S3Bucket", 
                    "OutputValue": "www-example.jp"
                }, 
                {
                    "OutputKey": "S3BucketPolicy", 
                    "OutputValue": "s3-bucket-www-example-jp-BucketP-S3BucketPolicy-SD37C6TE1IZA"
                }
            ], 
            "StackStatusReason": null, 
            "CreationTime": "2014-09-22T08:03:04.997Z", 
            "StackName": "s3-bucket-www-example-jp", 
            "NotificationARNs": [], 
            "StackStatus": "CREATE_COMPLETE", 
            "DisableRollback": false
        }
    ]
}
```

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

サンプルコンテンツをS3バケットに転送します。


5.1. コンテンツの取得
---------------------

GitHubからコンテンツを取得します。

```bash:コマンド:
   $ cd ${HOME}/tmp/
   $ git clone https://github.com/opelab/jawsug-cli-sample-web.git && cd jawsug-cli-sample-web
```


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

取得したコンテンツをS3バケットに転送します。

```bash:コマンド:
   $ aws s3 sync . "s3://${S3_BUCKET_NAME}/" --exclude ".git*"
```


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

S3バケットにコンテンツファイルが転送されたことを確認します。

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

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


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

ブラウザで下記のURLにアクセスして、コンテンツが見えることを確認します。

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

```text:結果:
http://www01.opelab.jp.s3-website-us-west-1.amazonaws.com/
www01.opelab.jp.s3-website-us-west-1.amazonaws.com
```


残作業
=======

今回は、以下の作業についてはやってないです。

* ログ用バケット
 * 作成
 * ACL設定
 * ログアーカイブ設定

* コンテンツ用バケット
 * ログ保存設定



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

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


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

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

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


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

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

```bash:コマンド:
   $ S3_BUCKET_NAME='www.example.jp'
   $ CF_STACK_NAME='s3-bucket-www-example-jp'
```


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

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

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

```bash:コマンド:
   2014-09-09 15:19:34 www.example.jp
```


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" )'
```