CloudFormationでLaravel5.5が動く環境をEC2(Ubuntu18.04)上に作ったときのまとめ

GWだったので趣味で開発してるLaravelプロジェクトをEC2で動かすために必要な環境をCloudFormationで立ててみました。その時に必要となった知識をまとめておきます。

# 必要な環境

- OS
    - Ubuntu18.04

- 必要サーバ
    - EC2インスタンス1台

- インストールが必要なもの(ざっくり)
    - php7.2
    - php-fpm
    - composer
    - Laravelの動作に必要なPHPのライブラリ群
    - nginx

- 追加で必要なサービス
    - Elastic IP

- Security Groupの要件
    - ssh接続の許可
    - http接続の許可

# CloudFormationによるインスタンスの管理
CloudFormationには初期化コマンドを実行するための仕組みをpythonスクリプトで提供していますが、Ubuntu18.04にはデフォルトで備えられていないので自前インストールします。pipを使って下記のコマンドで実現できました。

```bash
sudo apt-get update
sudo apt-get -y install python-pip
pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
sudo cp -a /usr/local/init/ubuntu/cfn-hup /etc/init.d/cfn-hup
chmod u+x /etc/init.d/cfn-hup
sudo update-rc.d cfn-hup defaults
sudo service cfn-hup start
```

これにより

- テンプレート内でパッケージインストールなどをパラメータで指定して実行(cfn-init)
- cfn-initで実行したコマンドのステータスを見て通達(cfn-signal)
- テンプレート更新によるstack更新を検知するデーモン(cfn-hup)

が利用可能になります。他にもありますが、使ってないので触れません。

## cfn-init
cfn-init ヘルパースクリプトは、AWS::CloudFormation::Init キーからテンプレートメタデータを読み取り、それに応じて様々な処理を実行します。(公式ドキュメント抜粋)
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-init.html

実行コマンド例

```bash
sudo /usr/local/bin/cfn-init -v --stack "YourStackName" --resource WebApp --configsets Install --region "YourRegion"
```

### 設定の読み込み

CloudFormationの中に`Resources.WebApp.Metadata.AWS::CloudFormation::Init`という宣言を書き、その要素として実行コマンドを記述できます。
configSetsというパラメータに実行するtaskをまとめて指定することができるので、ここに必要なtaskを登録しましょう。

```json
"Resources": {
    "WebApp": {
        "Metadata": {
            "AWS::CloudFormation::Init": {
                "configSets": {
                    "Install": [
                        "taskName1",
                        "taskName2",
                        "..."
                    ]
                },
                "taskName1": {
                    "..."
                },
                "taskName2": {
                    "..."
                },
                "..."
            }
        }
    }    
}
```

taskに指定できるコマンドにはいくつか種類があるので今回用いたものを書いていきます。

公式ドキュメントはこちら
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-init.html

### packageインストール

aptコマンドを使ったパッケージインストールは下記のように記述すると必要なパッケージをインストールできます。

```json
"taskName": {
    "packages": {
        "apt": {
            "nginx": [],
            "php7.2": [],
            "php-fpm": [],
            "php7.2-mysql": [],
            "php7.2-zip": [],
            "php7.2-mbstring": [],
            "php7.2-dom": [],
            "php7.2-curl": [],
            "composer": []
        }
    }
}
```

### ファイル作成
例えばnginxの設定ファイルを作成したければ下記のような記述をすればパーミッションを指定した上で作成が可能です。

区切り文字を改行コードで指定しているため、複数行に渡って記述することができます。

```json
"taksName": {
     "files": {
        "/etc/nginx/nginx.conf": {
            "content": {
                "Fn::Join": [
                    "\n",
                    [
                        "your settings",
                        "write here"
                    ]
                ]
            },
            "mode": "000400",
            "owner": "root",
            "group": "root"
        }
    }
}

```

### コマンド実行
これは自由度が高いです。好きなコマンドを実行できます。

```json
"taskName": {
    "commands": {
        "makeConfDir": {
            "command" : "sudo mkdir -p /etc/nginx/common"
        }
    }
}
```

### プロセス起動

services キーを使用すると、インスタンスが起動されるときに有効化または無効化する必要のあるサービスを定義できます。(公式ドキュメント抜粋)

Linuxではsysvinitキーを指定することで初回起動時の振る舞いを指定できるようです。

用いたオプションの意味は下記のとおりです。
enabled: 起動時にサービスを自動的に開始させるかどうか
ensureRunning: cfn-init が終了した後でサービスを実行するかどうか
files: 読み込む設定ファイルのリスト

```json
"taskName": {
     "services": {
         "sysvinit": {
             "cfn-hup": {
                 "enabled": "true",
                 "ensureRunning": "true",
                 "files": [
                     "/etc/cfn/cfn-hup.conf",
                     "/etc/cfn/hooks.d/cfn-auto-reloader.conf"
                 ]
             }
         }
     }
}
```

### ログの場所
重要な情報が入ってるのでCloudFormationを使う時はこのログファイルを見ることになると思います。

```
# 詳細なログ
/var/log/cfn-init.log
# 結果だけ出力されるログ
/var/log/cfn-init-cmd.log
```

## cfn-signal
cfn-initの実行後のステータスを引数に与えて使うことで状態を追跡できるようになります。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-signal.html

ドキュメントの通り、下記のように使って、cfn-init実行後のステータス`$?`を渡して実行します。

```json

"UserData": {
    "Fn::Base64": {
        "Fn::Join": [
            "",
            [
                "#!/bin/bash -x\n",
                "# Install the files and packages from the metadata\n",
                "/opt/aws/bin/cfn-init -v ",
                "         --stack ",
                {
                    "Ref": "AWS::StackName"
                },
                "         --resource MyInstance ",
                "         --region ",
                {
                    "Ref": "AWS::Region"
                },
                "\n",
                "# Signal the status from cfn-init\n",
                "/opt/aws/bin/cfn-signal -e $? ",
                "         --stack ",
                {
                    "Ref": "AWS::StackName"
                },
                "         --resource MyInstance ",
                "         --region ",
                {
                    "Ref": "AWS::Region"
                },
                "\n"
            ]
        ]
    }
}
```

## cfn-hup
cfn-hupは２つの設定ファイルを用いて動作させます。
/etc/cfn/cfn-hup.conf: 変更の検知
/etc/cfn/hooks.d/cfn-auto-reloader.conf: hook処理の定義

/etc/cfn/cfn-hup.confのintervalは変更があったかを調べる周期なので、開発中は短くしておくと便利です(デフォルト15分)。
hookの設定ファイルにupdateされたらcfn-initを実行するように書いておきます。

```json
"files": {
    "/etc/cfn/cfn-hup.conf": {
        "content": {
            "Fn::Join": [
                "",
                [
                    "[main]\n",
                    "stack=",
                    {
                        "Ref": "AWS::StackId"
                    },
                    "\n",
                    "region=",
                    {
                        "Ref": "AWS::Region"
                    },
                    "\n",
                    "interval=5",
                    "\n"
                ]
            ]
        },
        "mode": "000400",
        "owner": "root",
        "group": "root"
    },
    "/etc/cfn/hooks.d/cfn-auto-reloader.conf": {
        "content": {
            "Fn::Join": [
                "",
                [
                    "[cfn-auto-reloader-hook]\n",
                    "triggers=post.update\n",
                    "path=Resources.WebApp.Metadata.AWS::CloudFormation::Init\n",
                    "action=/usr/local/bin/cfn-init -v ",
                    "         --stack ",
                    {
                        "Ref": "AWS::StackName"
                    },
                    "         --resource WebApp ",
                    "         --configsets Install ",
                    "         --region ",
                    {
                        "Ref": "AWS::Region"
                    },
                    "\n",
                    "runas=root\n"
                ]
            ]
        },
        "mode": "000400",
        "owner": "root",
        "group": "root"
    }
}
```

# 設定ファイルのサンプル

Laravelはcomposerを使っており、設定ファイルはjson形式なのでCloudFormationの設定ファイルもjsonに合わせています。
本当はコメント使えないですが、便宜的に追加してます。

SecurityGroupやElastic IPについては特に困ったことは無かったので説明しませんでしたが、このサンプルに含まれています。

```
{
   "Resources": {
        # ssh接続するための設定
        "SSHGroup": {
            "Type": "AWS::EC2::SecurityGroup",
            "Properties": {
                "GroupDescription": "Enable SSH access via port 22",
                "SecurityGroupIngress": [
                    {
                        "IpProtocol": "tcp",
                        "CidrIp": "0.0.0.0/0",
                        "FromPort": "22",
                        "ToPort": "22"
                    }
                ]
            }
        },
        # httpとhttps通信を許可する設定
        "httpGroup": {
            "Type": "AWS::EC2::SecurityGroup",
            "Properties": {
                "GroupDescription": "Enable http access via port 80",
                "SecurityGroupIngress": [
                    {
                        "IpProtocol": "tcp",
                        "CidrIp": "0.0.0.0/0",
                        "FromPort": "80",
                        "ToPort": "80"
                    }
                ]
            }
        }, 
        # IPアドレスを固定するために割り当てる
        "WebServerEip": {
            "Type": "AWS::EC2::EIP",
            "Properties": {
                "InstanceId": {
                    "Ref": "WebApp"
                }
            }
        },
        # 立ち上げるEC2インスタンスの設定
        "WebApp": {
            "Type": "AWS::EC2::Instance",
            "Metadata": {
                "AWS::CloudFormation::Init": {
                    "configSets": {
                        "Install": [
                            "Install",
                            "SetupNginx",
                            "StartNginx"
                        ]
                    },
                    "Install": {
                        # 必要なパッケージのインストール
                        "packages": {
                            "apt": {
                                "nginx": [],
                                "php7.2": [],
                                "php-fpm": [],
                                "php7.2-mysql": [],
                                "php7.2-zip": [],
                                "php7.2-mbstring": [],
                                "php7.2-dom": [],
                                "php7.2-curl": [],
                                "composer": []
                            }
                        },
                        # 初期化時に実行するコマンド
                        "files": {
                            "/etc/cfn/cfn-hup.conf": {
                                "content": {
                                    "Fn::Join": [
                                        "",
                                        [
                                            "[main]\n",
                                            "stack=",
                                            {
                                                "Ref": "AWS::StackId"
                                            },
                                            "\n",
                                            "region=",
                                            {
                                                "Ref": "AWS::Region"
                                            },
                                            "\n",
                                            # 開発中はチェック頻度短くしたほうが楽(デフォルト１５分)
                                            "interval=5",
                                            "\n"
                                        ]
                                    ]
                                },
                                "mode": "000400",
                                "owner": "root",
                                "group": "root"
                            },
                            # stackを更新した時に反映させるためのデーモンの設定
                            "/etc/cfn/hooks.d/cfn-auto-reloader.conf": {
                                "content": {
                                    "Fn::Join": [
                                        "",
                                        [
                                            "[cfn-auto-reloader-hook]\n",
                                            "triggers=post.update\n",
                                            "path=Resources.WebApp.Metadata.AWS::CloudFormation::Init\n",
                                            "action=/usr/local/bin/cfn-init -v ",
                                            "         --stack ",
                                            {
                                                "Ref": "AWS::StackName"
                                            },
                                            "         --resource WebApp ",
                                            "         --configsets Install ",
                                            "         --region ",
                                            {
                                                "Ref": "AWS::Region"
                                            },
                                            "\n",
                                            "runas=root\n"
                                        ]
                                    ]
                                },
                                "mode": "000400",
                                "owner": "root",
                                "group": "root"
                            }
                        },
                        # cfn-hupを初期起動させる
                        "services": {
                            "sysvinit": {
                                "cfn-hup": {
                                    "enabled": "true",
                                    "ensureRunning": "true",
                                    "files": [
                                        "/etc/cfn/cfn-hup.conf",
                                        "/etc/cfn/hooks.d/cfn-auto-reloader.conf"
                                    ]
                                }
                            }
                        }
                    },
                    # nginxの設定に必要なファイルやディレクトリを作成
                    "SetupNginx": {
                        # ここで作ったディレクトリはnginxの設定ファイルでincludeする必要がある
                        "commands": {
                            "makeConfDir": {
                                "command" : "sudo mkdir -p /etc/nginx/common"
                            }
                        },
                        "files": {
                            "/etc/nginx/nginx.conf": {
                                "content": {
                                    "Fn::Join": [
                                        "\n",
                                        [
                                            "your settings"
                                        ]
                                    ]
                                },
                                "mode": "000400",
                                "owner": "root",
                                "group": "root"
                            },
                            # サービス毎に分けて管理する
                            "/etc/nginx/sites-available/your_site.conf": {
                                "content": {
                                    "Fn::Join": [
                                        "\n",
                                        [
                                            "your_settings"
                                        ]
                                    ]
                                },
                                "mode": "000400",
                                "owner": "root",
                                "group": "root"
                            },
                            # php-fpmの設定ファイルを分けておく
                            "/etc/nginx/common/php_fastcgi.conf": {
                                "content": {
                                    "Fn::Join": [
                                        "\n",
                                        [
                                            "your_settings"
                                        ]
                                    ]
                                },
                                "mode": "000400",
                                "owner": "root",
                                "group": "root"
                            },
                            "/etc/nginx/common/general.conf": {
                                "content": {
                                    "Fn::Join": [
                                        "\n",
                                        [
                                            "your settings"
                                        ]
                                    ]
                                },
                                "mode": "000400",
                                "owner": "root",
                                "group": "root"
                            }
                        }
                    },
                    # 起動
                    "StartNginx": {
                        "commands" : {
                            "start": {
                                "command" : "sudo systemctl start nginx"
                            }
                        }
                    }
                }
            },
            "Properties": {
                # Ubuntu18.04のami
                "ImageId": "ami-0eb48a19a8d81e20b",
                "InstanceType": {
                    "Ref": "InstanceType"
                },
                "KeyName": {
                    "Ref": "KeyName"
                },
                "SecurityGroupIds": [
                    {
                        "Ref": "SSHGroup"
                    },
                    {
                        "Ref": "httpGroup"
                    }
                ],
　　　　　　　　　 # 最初に実行するコマンドの指定
                "UserData": {
                    "Fn::Base64": {
                        "Fn::Join": [
                            "",
                            [
                                "#!/bin/bash -xe\n",
                                "sudo apt-get update\n",
                                "sudo apt-get -y install python-pip\n",
                                # cfn-hupが使えるようにする
                                "pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n",
                                "sudo cp -a /usr/local/init/ubuntu/cfn-hup /etc/init.d/cfn-hup\n",
                                "chmod u+x /etc/init.d/cfn-hup\n",
                                "sudo update-rc.d cfn-hup defaults\n",
                                "sudo service cfn-hup start\n",
                                "# Install the files and packages from the metadata\n",
                                "sudo /usr/local/bin/cfn-init -v ",
                                "         --stack ",
                                {
                                    "Ref": "AWS::StackName"
                                },
                                "         --resource WebApp ",
                                "         --configsets Install ",
                                "         --region ",
                                {
                                    "Ref": "AWS::Region"
                                },
                                "\n",
                                # エラーハンドリング用
                                "# Signal the status from cfn-init\n",
                                "sudo /usr/local/bin/cfn-signal -e $? ",
                                "         --stack ",
                                {
                                    "Ref": "AWS::StackName"
                                },
                                "         --resource WebApp ",
                                "         --region ",
                                {
                                    "Ref": "AWS::Region"
                                },
                                "\n"
                            ]
                        ]
                    }
                }
            }
        }
    },
    # stack作成時にオプション選択するパラメータを指定
    "Parameters": {
        "KeyName": {
            "Description": "Name of an existing EC2 KeyPair to enable SSH access to the web server",
            "Type": "String",
            "Default": "your_key"
        },
        "InstanceType": {
            "Description": "WebServer EC2 instance type",
            "Type": "String",
            "Default": "t2.micro",
            "AllowedValues": [
                "t1.micro",
                "t2.nano",
                "t2.micro"
            ],
            "ConstraintDescription": "must be a valid EC2 instance type."
        }
    },
    # stackの状態を見た時に表示される内容
    "Outputs": {
        "WebServerIpAddress": {
            "Value": {
                "Fn::GetAtt": [
                    "WebApp",
                    "PublicIp"
                ]
            },
            "Description": "IP Address of WebServer"
        },
        "WebServerAvailabilityZone": {
            "Value": {
                "Fn::GetAtt": [
                    "WebApp",
                    "AvailabilityZone"
                ]
            },
            "Description": "AvailabilityZone of WebServer"
        },
        "SSHToWebServer": {
            "Value": {
                "Fn::Join": [
                    "",
                    [
                        "ssh -i /path/to/",
                        {
                            "Ref": "KeyName"
                        },
                        ".pem",
                        " ec2-user@",
                        {
                            "Fn::GetAtt": [
                                "WebApp",
                                "PublicIp"
                            ]
                        }
                    ]
                ]
            },
            "Description": "SSH command to connect WebServer"
        }
    }
}
```