AWSCloudFormationでサーバーレス環境を立ち上げる

AWSでサーバーレス環境を立ち上げるというのは簡単にできるのですが、これを案件ごとに実行するのはあまりスマートではないのでルーチン化する方法を探しました。
色々探し回った結果、CloudFormationを使うという選択肢になったためその方法を記載します。 

## この記事で得られる知識
- CloudFormationで出来る初歩的な事がわかる
- CloudFormationを使ってS3バケットの環境を構築できるようになる

## イントロダクション
### 作業の流れ
1. CloudFormationテンプレート作成
2. S3バケットを作るリソースを追加
3. S3バケットのポリシーを追加
4. 公開URLを出力するように調整
5. スタック作成

### 構成（案）
最終的には下記のような構成にすることを考えていますが、今回はS3バケットを立ち上げるところまでです。
![deploy.png](https://qiita-image-store.s3.amazonaws.com/0/207486/93133b83-7358-32ba-3c80-f790f1bd1e88.png)

### 今回作成したソース
githubで公開しています。
https://github.com/websandbag/aws-serverless
今後随時更新していきますが、今回の作業は、[v0.1.1](https://github.com/websandbag/aws-serverless/releases/tag/v0.1.1)が対象です。　

### 参考資料
[CloudFormationの公式ドキュメント](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)

## CloudFormationテンプレート作成
AWSで様々なサービスを構成する場合、[CloudFormation](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation)というサービスを使います。
CloudFormationでは、「Template」と呼ばれる構成ファイルを自作し、それを元にAWSのサービスを構成して「Stack」を作成します。

![cloudformation_.png](https://qiita-image-store.s3.amazonaws.com/0/207486/2e291f5d-e6cf-a8cb-12b4-09768b071f61.png)


まずは、Templateを作成します。
TemplateはJSON形式か、YAML形式から選べます。今回はJSON形式で作成します。
ファイル名も任意ですが、今回はのcloudformation.jsonというファイル名にします。

  ``` json:cloudformation.json
{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Serverless base",
    "Parameters": {
        "EnvType": {
            "Description": "Enviroment type. Default is test.",
            "Default": "test",
            "Type": "String",
            "AllowedValues": [
                "prod",
                "test"
            ],
            "ConstraintDescription": "must specify prod or test."
        },
        "InstanceType": {
            "Description": "Enter t2.micro, m1.small, or m1.large. Default is t2.micro.",
            "Type": "String",
            "Default": "t2.micro",
            "AllowedValues": [
                "t2.micro",
                "m1.small",
                "m1.large"
            ],
            "ConstraintDescription": "must specify t2.micro, m1.small, or m1.large."
        }
    },
    "Conditions" : {
        "CreateProdResources" : {
            "Fn::Equals" : [
                {"Ref" : "EnvType"}, "prod"
            ]
        }
    }
}
```

構成について簡単に解説します。
詳細は、CloudFormationのドキュメントを参考にしてください。

``` json
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Serverless base",
```

まず、Templateのバージョンと概要を宣言します。

- AWSTemplateFormatVersionセクション: テンプレートの規格のバージョンを指定。2010-09-09が最新なので変更不要
- Descriptionセクション: テンプレートの説明。


``` json
"Parameters": {
    "EnvType": {
        "Description": "Enviroment type. Default is test.",
        "Default": "test",
        "Type": "String",
        "AllowedValues": [
            "prod",
            "test"
        ],
        "ConstraintDescription": "must specify prod or test."
    },
    "InstanceType": {
        "Description": "Enter t2.micro, m1.small, or m1.large. Default is t2.micro.",
        "Type": "String",
        "Default": "t2.micro",
        "AllowedValues": [
            "t2.micro",
            "m1.small",
            "m1.large"
        ],
        "ConstraintDescription": "must specify t2.micro, m1.small, or m1.large."
    }
},
```  

次に、Parametersセクションを定義します。
Parametersセクションは、CLIで構築するときに、引数として指定する事でスタック作成時に反映させる事が出来ます。
現在の状態ではまだ組み込めていませんが、次の事をするために使用します。

- EnvType: 本番とテスト環境の識別
- InstanceType: EC2インスタンスのサイズ指定

``` json
"Conditions" : {
    "CreateProdResources" : {
        "Fn::Equals" : [
            {"Ref" : "EnvType"}, "prod"
        ]
    }
}
```

最後に、リソースの作成有無についての状態用の識別子です。
リソース内で、Conditionという共通の属性があり、Conditionsセクションで指定した結果を挿入するときに使います。

## S3バケットを作るリソースを追加
次にResourcesセクションにS3バケットの構成を追加します。
リソース名は「S3Bucket」としています。

``` json:cloudformation.json
{
    "Resources": {
        "S3Bucket": {
            "Type": "AWS::S3::Bucket",
            "Properties": {
                "AccessControl": "PublicRead",
                "WebsiteConfiguration": {
                    "IndexDocument": "index.html",
                    "ErrorDocument": "error.html"
                },
                "Tags" : [
                    {
                        "Key" : "name",
                        "Value" : "S3Bucket",
                    }
                ]
            },
        },
    }
}
```

- Type: 追加するAWSのサービスを追加します。追加できるプロパティのタイプは[AWS リソースプロパティタイプのリファレンス](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)を参照してください。
- Properties: Typeで選択したタイプに付与する属性を選択してください。この記事[S3](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html)のリファレンスに順次ます。

### スタックを消してもバケットだけ残したい場合
スタックを削除すると、連動してS3のバケットも消えてしまうのですが、バケットだけ残して置きたいという時があります。
その場合は、リソースにDeletionPolicy属性を追加し「Retain」にします。

``` json
{
    "Resources": {
        "S3Bucket": {
            "Type": "AWS::S3::Bucket",
            "DeletionPolicy": "Retain"
        }
    }
}
```

### バケットの名前を定義する
Propertiesセクションに[BucketName](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html#cfn-s3-bucket-name)を追加する事で指定ができるようです。
もし、固有のバケットを作りたいのであれば上記を指定するやり方もあります。

``` json
{
    "Resources": {
        "S3Bucket": {
            "Type": "AWS::S3::Bucket",
            "BucketName" : "名前",
        }
    }
}
```

## S3バケットのポリシーを追加
この状態では、インターネットをバケットに引き込めないのでS3のポリシーを追加します。 
ポリシーの書き方については、[私のブログの記事](http://websandbag.hatenablog.com/entry/2017/12/16/014234)で触れましたので、よろしければこちらもご参考ください。

``` json:cloudformation.json
"S3BucketPolicy": {
    "Type": "AWS::S3::BucketPolicy",
    "Properties": {
        "PolicyDocument": {
            "Id": "MyPolicy",
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "PublicReadForGetBucketObjects",
                    "Effect": "Allow",
                    "Principal": "*",
                    "Action": "s3:GetObject",
                    "Resource": {
                        "Fn::Join": [
                            "",
                            [
                                "arn:aws:s3:::",
                                {
                                    "Ref": "S3Bucket"
                                },
                                "/*"
                            ]
                        ]
                    }
                }
            ]
        },
        "Bucket": {
            "Ref": "S3Bucket"
        }
    }
}
```

特筆すべきなのは、Fn::Joinの内容です。
[Fn::Join](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html)の部分については、区切り文字を使わず、第2引数の配列を結合するという意味です。
[Ref](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html)は、指定したリソースの値を返します。
S3の場合は、バケット名を返します。
他のリソースの場合は、[リソースの戻り値の例
](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html#w2ab2c21c28c65c25)を参照してください。


``` json
"Resource": {
    "Fn::Join": [
        "",
        [
            "arn:aws:s3:::",
            {
                "Ref": "S3Bucket"
            },
            "/*"
        ]
    ]
}
```

## 公開URLを出力するように調整
この状態でスタックを作成してもよいのですが、公開URLはAWSのコントロールパネルからしか確認できません。
そこで、CLIでCloudFormationにアクセスしたときにURLを返すように調整します。

``` json:cloudformation.json
"Outputs": {
    "WebsiteURL": {
        "Value": {
            "Fn::GetAtt": [
                "S3Bucket",
                "WebsiteURL"
            ]
        },
        "Description": "URL for website hosted on S3"
    },
    "S3BucketSecureURL": {
        "Value": {
            "Fn::Join": [
                "",
                [
                    "https://",
                    {
                        "Fn::GetAtt": [
                            "S3Bucket",
                            "DomainName"
                        ]
                    }
                ]
            ]
        },
        "Description": "Name of S3 bucket to hold website content"
    }
}
```

「WebsiteURL」がhttpプロトコルの場合のURLで、「S3BucketSecureURL」がhttpsプロトコルの場合のURLです。

また、テンプレートの中で[Fn::GetAtt](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html)という関数を使っています。
これは、次のようにリソース名と属性を指定して値を代入する事ができます。

``` json
{
    "Fn::GetAtt" : [ "logicalNameOfResource", "attributeName" ]
}
```  

[属性](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html#intrinsic-function-reference-getatt-attrib)については、公式ドキュメントにも記載されていますので必要に応じて取得してください。

## スタック作成
作成したテンプレートを元にスタックを作成します。
今回は、スタック名を「myteststack」にしています。

``` bash
$ cd (プロジェクトフォルダ)
$ aws cloudformation create-stack \
    --stack-name myteststack \
    --template-body file://cloudformation.json

```

実行すると、s3のバケットが作成され公開まで出来ます。
試しにAWSのコントロールパネルからS3のバケットを確認してみてください。

### 作成されたstackを確認する方法
Outputsセクションの設定をしていれば下記のコマンドでURLを確認する事が出来ます。

``` bash
$ aws cloudformation describe-stacks --stack-name myteststack

... 出力結果 ...

"Outputs": [
     {
         "OutputValue": "httpsのURL",
         "Description": "Name of S3 bucket to hold website content",
         "OutputKey": "S3BucketSecureURL"
     },
     {
         "OutputValue": "httpのURL",
         "Description": "URL for website hosted on S3",
         "OutputKey": "WebsiteURL"
     }
 ],

```

### 作成されたstackを削除
作成したスタックを削除する場合は次のコマンドを実行します。
リソースを残す設定をしていない場合は、作成されたリソースも一緒に削除されます。

``` bash
$ aws cloudformation delete-stack --stack-name myteststack
```

### スタックを更新
テンプレートを修正した場合は、次のコマンドでスタックを作り直す事が出来ます。

``` bash
$ aws cloudformation update-stack \
    --stack-name mystack \
    --template-body file://cloudformation.json
```

## Tips
### CloudFormationデザイナーとは？
GUIでパーツを繋げる事でテンプレートを直感的に作るツールです。
簡単な構成であればテンプレートを自作する必要はなく、このツールだけで作る事が可能です。

![design.png](https://qiita-image-store.s3.amazonaws.com/0/207486/f06fa146-e8fa-fbc1-0927-6d704c32d545.png)

### テストサーバーを一時的に構築できないか？？
(ここで言う、「テストサーバー」というのは、表示確認ではなくユニットテストを行うためのサーバーになります。)
運用環境の理想を言えば、テストケースを通してからデプロイできるのが一番望ましい環境になります。
例えば、PHPの場合は、一度、jenkinsサーバーのワークスペースにソースを展開し、PHPUnit等のテストライブラリでテストをした後、問題なければデプロイするような構成になるかと思います。
![deploy_2.png](https://qiita-image-store.s3.amazonaws.com/0/207486/96daef27-c1ba-9a50-058b-6b70117ce2b0.png)

これを、AWSの環境で行う場合は、[CodePipeline](https://aws.amazon.com/jp/CodePipeline/)に組み込んで、ビルドサーバーを作るという事になるかと思います。 
今回は実装しませんが、[ウォークスルー: テストおよび本稼働スタック用のパイプラインを構築する](
http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/continuous-delivery-CodePipeline-basic-walkthrough.html)というページで解説されています。

#### CodePipelineについて所感
料金は[CodePipelineの料金](https://aws.amazon.com/jp/CodePipeline/pricing/)によると1USD/1ヶ月となっています。 
実際には、CodeCommitやCodeDeployと行った他のツールと組み合わせたり、インスタンスやバケットを作ったりするので、増えると思いますし学習コストもかかるので安易に移行できないかもしれません。
ただ、案件数が少ない場合は、jenkinsサーバーをEC2インスタンスで稼働する費用を考えると運用コスト自体は抑えられるかもしれません。