[JAWS-UG CLI] S3:#6 バケット更新 (CloudFormation)

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

CloudFormationを利用して、S3上に静的ウェブホスティング(独自ドメイン)用のバケットを構築、運用します。

今回は、S3:#5 (http://qiita.com/tcsh/items/de8676d836ffe611cc58) で作成したピュアなS3バケットにWebホスティングの公開に必要な各種設定を追加します。

Web公開準備のときの設定更新作業として行なうことを想定しています。



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

* S3へのフルアクセス権限
* 独自ドメインを運用している。
  * 独自ドメイン取得手順: http://qiita.com/tcsh/items/597e7644949bfe4ab405
  * 独自ドメインを持っていない場合は、example.comやexample.jpのサブドメインを利用してみてください。(Route53への登録は可能なようです。継続的に利用できるかは未確認)

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

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

更新するバケットを決めます。

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

Note
このハンズオンでは、ログバケット名が”{コンテンツバケット名}-log”であることを前提にしています。

0.2. 変数の定義
-----------------


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

0.3. リージョンの決定
---------------------

更新するバケットが存在するリージョンをAWS_DEFAULT_REGIONとして設定します。


```bash:コマンド:
$ export AWS_DEFAULT_REGION=`aws s3api get-bucket-location --bucket ${S3_BUCKET_NAME} --output text`; echo ${AWS_DEFAULT_REGION}
```

Note
このハンズオンでは、ログバケットがコンテンツバケットと同じリージョンにあることを前提にしています。

0.4. リポジトリへの移動
-------------------------

リポジトリが存在する場合、作業場所としてリポジトリディレクトリに移動します。

```bash:コマンド(例):
$ cd ~/_work/aws/project-01
```


1. テンプレート更新
===================

CloudFormationテンプレートを編集します。

ここではS3バケット(設定なし)に、以下の設定を追加します。

* ログバケット
  * ACL (ログ書き込みを許可)

* コンテンツバケット
  * Webサイトホスティング
  * バケットポリシー (Webアクセスを許可)
  * アクセスログ保存


```json:変更前:
{
  "AWSTemplateFormatVersion": "2010-09-09",

  "Description": "website bucket (www.example.com)",

  "Resources": {
    "LogBucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "BucketName": "www.example.com-log"
      }
    },
    "WebBucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "BucketName": "www.example.com"
      }
    }
  }
}
```

```text:修正部分:
10c10,11
<         "BucketName": "www.example.com-log"
---
>         "BucketName": "www.example.com-log",
>         "AccessControl": "LogDeliveryWrite"
16c17,39
<         "BucketName": "www.example.com"
---
>         "BucketName": "www.example.com",
>         "WebsiteConfiguration": {
>           "IndexDocument": "index.html",
>           "ErrorDocument": "error.html"
>         },
>           "LoggingConfiguration": {
>              "DestinationBucketName": {"Ref": "LogBucket"},
>              "LogFilePrefix": "logs"
>         }
>       }
>     },
>     "WebBucketPolicy": {
>       "Type": "AWS::S3::BucketPolicy",
>       "Properties": {
>         "Bucket": "www.example.com",
>         "PolicyDocument": {
>           "Statement": [{
>             "Action": "s3:GetObject",
>             "Effect": "Allow",
>             "Principal": { "AWS": "*" },
>             "Resource": "arn:aws:s3:::www.example.com/*"
>           }]
>         }
```


```json:変更後:
{
  "AWSTemplateFormatVersion": "2010-09-09",

  "Description": "website bucket (www.example.com)",

  "Resources": {
    "LogBucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "BucketName": "www.example.com-log",
        "AccessControl": "LogDeliveryWrite"
      }
    },
    "WebBucket": {
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "BucketName": "www.example.com",
        "WebsiteConfiguration": {
          "IndexDocument": "index.html",
          "ErrorDocument": "error.html"
        },
          "LoggingConfiguration": {
             "DestinationBucketName": {"Ref": "LogBucket"},
             "LogFilePrefix": "logs"
        }
      }
    },
    "WebBucketPolicy": {
      "Type": "AWS::S3::BucketPolicy",
      "Properties": {
        "Bucket": "www.example.com",
        "PolicyDocument": {
          "Statement": [{
            "Action": "s3:GetObject",
            "Effect": "Allow",
            "Principal": { "AWS": "*" },
            "Resource": "arn:aws:s3:::www.example.com/*"
          }]
        }
      }
    }
  }
}
```

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


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

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

invalidとエラーが表示された場合は、jsonlintでどこが壊れているか調べます。


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

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

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



2. スタック(バケット)更新
==========================

スタックを更新して、S3バケットの設定を変更します。

2.1. スタック(S3バケット)更新
-----------------------------


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

```json:結果:
{
  "StackId": "arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/route53-example-com-hostedzone/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
```

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

スタックの更新状況を確認します。


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

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

StackStatusが’UPDATE_COMPLETE’になっていれば作成は完了です。 (30秒から1分程度かかるようです。)

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


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


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

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


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

```json:結果:
{
  "TemplateBody": {
      "AWSTemplateFormatVersion": "2010-09-09",
      "Description": "website bucket (www.opelab01.opelab.jp)",
      "Resources": {
          "WebBucket": {
              "Type": "AWS::S3::Bucket",
              "Properties": {
                  "LoggingConfiguration": {
                      "DestinationBucketName": {
                          "Ref": "LogBucket"
                      },
                      "LogFilePrefix": "logs"
                  },
                  "BucketName": "www.opelab01.opelab.jp",
                  "WebsiteConfiguration": {
                      "IndexDocument": "index.html",
                      "ErrorDocument": "error.html"
                  }
              }
          },
          "LogBucket": {
              "Type": "AWS::S3::Bucket",
              "Properties": {
                  "AccessControl": "LogDeliveryWrite",
                  "BucketName": "www.opelab01.opelab.jp-log"
              }
          },
          "WebBucketPolicy": {
              "Type": "AWS::S3::BucketPolicy",
              "Properties": {
                  "PolicyDocument": {
                      "Statement": [
                          {
                              "Action": "s3:GetObject",
                              "Resource": "arn:aws:s3:::www.opelab01.opelab.jp/*",
                              "Effect": "Allow",
                              "Principal": {
                                  "AWS": "*"
                              }
                          }
                      ]
                  },
                  "Bucket": "www.opelab01.opelab.jp"
              }
          }
      }
  }
}
```

3. ログバケット確認
=====================

ログバケットの設定を確認します。

3.1. バケットACLポリシー確認
-------------------------------

LogDeliveryに対して、”WRITE”と”READ_ACP”が許可されていることを確認します。


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

```json:結果:
{
  "Owner": {
      "DisplayName": "XXXXX",
      "ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "Grants": [
      {
          "Grantee": {
              "DisplayName": "XXXXX",
              "ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
          },
          "Permission": "FULL_CONTROL"
      },
      {
          "Grantee": {
              "URI": "http://acs.amazonaws.com/groups/s3/LogDelivery"
          },
          "Permission": "WRITE"
      },
      {
          "Grantee": {
              "URI": "http://acs.amazonaws.com/groups/s3/LogDelivery"
          },
          "Permission": "READ_ACP"
      }
  ]
}
```

4. コンテンツバケット確認
==========================

4.1. Webサイトホスティング設定の確認
-------------------------------------

Webサイトホスティングの設定が反映されていることを確認します。


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

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

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

バケットポリシーの設定が反映されていることを確認します。


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

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

4.3. アクセスログ保存設定の確認
-------------------------------

アクセスログの保存設定が反映されていることを確認します。


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

```json:結果:
{
  "LoggingEnabled": {
      "TargetPrefix": "logs",
      "TargetBucket": "www.example.com-log"
  }
}
```

5. コミット
=============

問題無くスタックの更新が完了していれば、ここで一旦テンプレートをコミットしておきましょう。


```bash:コマンド:
$ hg commit -m "update website bucket configuration (${S3_BUCKET_NAME})."
```

ウェブサイトエンドポイントの保存
===============================

コンテンツ転送後に確認するためのエンドポイントをメモしておきます。


```bash:コマンド:
$ WEB_ENDPOINT="${S3_BUCKET_NAME}.s3-website-${AWS_DEFAULT_REGION}.amazonaws.com" && echo ${WEB_ENDPOINT}
```

```text:結果:
www.example.com.s3-website-ap-northeast-1.amazonaws.com
```

完了
======

S3でのウェブサイトホスティングの構築が完了したので、Route53にコンテンツバケットへのエイリアスレコードを登録します。 http://qiita.com/tcsh/items/abe87b236e50d29f08e6


おまけ
=======

サンプルコンテンツを転送しておくと、下記のURLでコンテンツ確認ができます。

```bash:コマンド
ENDPOINT="${S3_BUCKET_NAME}.s3-website-${AWS_DEFAULT_REGION}.amazonaws.com" && echo ${ENDPOINT}
```

転送手順: http://qiita.com/tcsh/items/70694da357c1738c6bf8#2-1