Elixir/Phoenix環境をElasticBeanstalk Custom Platformで構築してみた

# ElasticBeanstalk Custom Platformとは？

今までのElasticBeanstalkでは事前定義済みの環境にアプリケーションをデプロイすることで環境を構築していたが、2017/02/22の[リリース](https://aws.amazon.com/jp/about-aws/whats-new/2017/02/aws-elastic-beanstalk-supports-custom-platforms/)でCustom Platformを定義することが可能になった。

これまでもベースAMIをもとにカスタマイズした環境を利用することは可能だったが、AMIの管理(ビルドやバージョン管理、それらを利用したデプロイなど)は自力で行う必要があった。

今回リリースされたCustom PlatformはPackerを利用したAMIのビルドやバージョン管理、デプロイをマネージドサービスとして提供している。

なので、ためしにElixir/Phoenixを起動するCustom Platformを構築してみた。（`してみた`というレベルに見合わない時間がかかったけど）

副産物としてerlangのAmazon Linux用のrpmができた。[sonodar/erlang-rpm](https://github.com/sonodar/erlang-rpm)

作成したCustom Platformのテンプレートやプロビジョニングソースは[Github](https://github.com/sonodar/elasticbeanstalk-phoenix-platform)にあげておきました。
ソースの解説は一番最後に書いてあります。まずはCustom Platformを動かすところから。

# Phoenixアプリケーションのビルド方針

* Exrm(Elixirのビルドライブラリ)を利用したコンパイル済みバイナリのデプロイではなく、ソースをデプロイしてElasticBeanstalk(以降、EB)環境上でソースのビルドを行うものとする。
* DBおよびecto(Elixirのスキーママイグレーションライブラリ)は使わない構成とする
  * ecto使う事自体は簡単なのでIssueを追加しておいた
* brunch(Phoenixのアセットコンパイル用npmパッケージ)も使わない構成とする

# Custom Platformのビルド

## awsebcliのインストール

```bash
$ brew install awsebcli
```

## テンプレートソースのclone

```bash
$ git clone https://github.com/sonodar/elasticbeanstalk-phoenix-platform.git
$ cd elasticbeanstalk-phoenix-platform
```

## ElasticBeanstalk Custom Platformのビルド

以下のコマンドでローカルディレクトリに`.elasticbeanstalk/config.yml`ファイルが作成される。

```bash
$ ebp init -r ap-northeast-1

Enter Platform Name
(default is "elasticbeanstalk-phoenix-platform"):
```

この時点ではまだプラットフォームの作成は行われていない。

プラットフォームの作成は以下のコマンドで行う。
初回実行時にPacker実行用のElasticBeanstalk環境(`Custom Platform Builder`)も自動的に作成される。

```bash
$ ebp create
INFO: createPlatform is starting.
INFO: Initiated platform version creation for 'elasticbeanstalk-phoenix-platform/1.0.0'.
INFO: Creating Packer builder environment 'eb-custom-platform-builder-packer'.
INFO: Starting Packer building task.
INFO: Creating CloudWatch log group '/aws/elasticbeanstalk/platform/elasticbeanstalk-phoenix-platform'.
# ... 中略 ...
INFO: 'packer build' finished.
INFO: Successfully built AMI(s): 'ami-93a5fef4' for 'arn:aws:elasticbeanstalk:ap-northeast-1:xxxxxxxxxxxx:platform/elasticbeanstalk-phoenix-platform/1.0.0'
INFO: Creating CloudWatch log group '/aws/elasticbeanstalk/platform/elasticbeanstalk-phoenix-platform'.
INFO: Successfully built AMI(s): 'ami-93a5fef4' for 'arn:aws:elasticbeanstalk:ap-northeast-1:xxxxxxxxxxxx:platform/elasticbeanstalk-phoenix-platform/1.0.0'
INFO: Packer built AMIs: ami-93a5fef4.
INFO: Successfully created platform version 'elasticbeanstalk-phoenix-platform/1.0.0'.
```

作成された環境の確認。`Status`が`Ready`となっていればプラットフォームの作成が成功している。

```bash
$ ebp list
arn:aws:elasticbeanstalk:ap-northeast-1:xxxxxxxxxxxx:platform/elasticbeanstalk-phoenix-platform/1.0.0  Status: Ready
```

# 今回用のサンプルPhoenixプロジェクトの作成

Macの場合はおもむろに以下のコマンドを打つ。

```bash
brew install elixir
mix local.hex
mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
mix phoenix.new eb_phoenix --no-brunch --no-ecto
cd eb_phoenix
```

依存ライブラリをインストールするかどうか聞かれるので`y`応答

```
Fetch and install dependencies? [Yn] y
```

動作確認。以下のコマンドを実行して http://localhost:4000 にアクセス。

```bash
mix phoenix.server # 停止は Ctrl+C を2回
```

こんな画面が出れば成功。
![Hello-EbPhoenix.png](https://qiita-image-store.s3.amazonaws.com/0/17735/27dc283f-6355-47cb-c392-6b1a61b37d7b.png)

# サンプルアプリケーションを作成したCustom Platformで起動

`eb init`コマンドで初期化する。platform選択で`13) Custom Platform`を選択。

```bash
$ cd eb_phoenix
$ eb init -r ap-northeast-1
Select an application to use
1) Custom Platform Builder
2) [ Create new Application ]
(default is 2):

Enter Application Name
(default is "eb_phoenix"): eb-phoenix

Select a platform.
1) Node.js
2) PHP
3) Python
4) Ruby
5) Tomcat
6) IIS
7) Docker
8) Multi-container Docker
9) GlassFish
10) Go
11) Java
12) Packer
13) Custom Platform
(default is 1): 13

Select a platform.
1) elasticbeanstalk-platform-phoenix (Owned by: xxxxxxxxxxxx)
(default is 1): 1
Do you want to set up SSH for your instances?
(Y/n): Y

Select a keypair.
1) sonodar
2) [ Create new KeyPair ]
(default is 1): 1
```

`eb create`コマンドでEB環境を作成する。今回はお試しなのでシングル構成(`-s`オプション)

```bash
$ eb create -s
Enter Environment Name
(default is eb-phoenix-dev): eb-phoenix-dev
Enter DNS CNAME prefix
(default is eb-phoenix-dev): eb-phoenix-dev
Creating application version archive "app-84db-170327_132118".
Uploading eb-phoenix/app-84db-170327_132118.zip to S3. This may take a while.
Upload Complete.
Application eb-phoenix has been created.
Environment details for: eb-phoenix-dev
  Application name: eb-phoenix
  Region: ap-northeast-1
  Deployed Version: app-84db-170327_132118
  Environment ID: e-7fshvxwyad
  Platform: arn:aws:elasticbeanstalk:ap-northeast-1:xxxxxxxxxxxx:platform/elasticbeanstalk-platform-phoenix/1.0.0
  Tier: WebServer-Standard
  CNAME: eb-phoenix-dev.ap-northeast-1.elasticbeanstalk.com
  Updated: 2017-03-27 04:21:21.816000+00:00
Printing Status:
INFO: createEnvironment is starting.
INFO: Using elasticbeanstalk-ap-northeast-1-xxxxxxxxxxxx as Amazon S3 storage bucket for environment data.
INFO: Created EIP: 52.197.66.130
INFO: Created security group named: awseb-e-7fshvxwyad-stack-AWSEBSecurityGroup-1T9MF5U46H463
INFO: Waiting for EC2 instances to launch. This may take a few minutes.
INFO: Adding instance 'i-065353dcfcb113901' to your environment.
INFO: Successfully launched environment: eb-phoenix-dev
```

以上。ElasticBeanstalkのHTTPエンドポイントにアクセスしてみるとPhoenixの画面が見れる。

# Custom Platform テンプレートソース解説

TODO: あとで書く

## ポイント

* ElasticBeanstalkのデプロイシーケンスを理解する
* PhoenixアプリケーションのDaemon/Service化にはUpstartを利用
* ElasticBeanstalkのEnvironmentをパースしてファイルに出力
* 今回のためにErlangのrpmを作成した ([sonodar/erlang-rpm](https://github.com/sonodar/erlang-rpm))

# ハマったところ

## `ebp list`や`ebp delete`コマンドは`Custom Platform Builder`が起動していないとエラーになる。

`Custom Platform Builder`はCustom Platformのビルド時以外は不要のため、普段は環境を落としておいたほうがいいのだが、落とした状態で`ebp`コマンドを実行すると以下のように意味不明なエラーメッセージとともに失敗する。

```
ERROR: TypeError :: cannot concatenate 'str' and 'list' objects
```

## `ebp create`コマンドはIAMの`CreateRole`権限がないと失敗する

`ebp create`を初めて実行した際にPacker実行用環境である`Custom Platform Builder`が利用する`aws-elasticbeanstalk-custom-platform-ec2-role`というIAMロールも自動生成される。
CreateRole権限がないとエラーになるため権限のあるアカウントか、以下のポリシーをアタッチしておくこと。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "GrantCreateRole",
            "Action": [
                "iam:Create*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
```

それか、あらかじめ`aws-elasticbeanstalk-custom-platform-ec2-role`をEC2用のロール(インスタンスプロファイル付き)として以下のポリシーで作っておく。

```json:信頼関係
{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

```json:インラインポリシー(EB_Custom_Platform_Builder_Policy)
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PackerEC2Access",
            "Action": [
                "ec2:AttachVolume",
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:CopyImage",
                "ec2:CreateImage",
                "ec2:CreateKeypair",
                "ec2:CreateSecurityGroup",
                "ec2:CreateSnapshot",
                "ec2:CreateTags",
                "ec2:CreateVolume",
                "ec2:DeleteKeypair",
                "ec2:DeleteSecurityGroup",
                "ec2:DeleteSnapshot",
                "ec2:DeleteVolume",
                "ec2:DeregisterImage",
                "ec2:DescribeImageAttribute",
                "ec2:DescribeImages",
                "ec2:DescribeInstances",
                "ec2:DescribeRegions",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeSnapshots",
                "ec2:DescribeSubnets",
                "ec2:DescribeTags",
                "ec2:DescribeVolumes",
                "ec2:DetachVolume",
                "ec2:GetPasswordData",
                "ec2:ModifyImageAttribute",
                "ec2:ModifyInstanceAttribute",
                "ec2:ModifySnapshotAttribute",
                "ec2:RegisterImage",
                "ec2:RunInstances",
                "ec2:StopInstances",
                "ec2:TerminateInstances"
            ],
            "Effect": "Allow",
            "Resource": "*"
        },
        {
            "Sid": "BucketAccess",
            "Action": [
                "s3:Get*",
                "s3:List*",
                "s3:PutObject"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::elasticbeanstalk-*",
                "arn:aws:s3:::elasticbeanstalk-*/*"
            ]
        },
        {
            "Sid": "CloudWatchLogsAccess",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:logs:*:*:log-group:/aws/elasticbeanstalk/platform/*"
        }
    ]
}
```

[追記]
`arn:aws:iam::aws:policy/AWSElasticBeanstalkCustomPlatformforEC2Role`というマネージドポリシーがあるからそれを使えばいいみたいだ。内容は上記インラインポリシーと同じ。

## Custom Platform用のリポジトリとアプリケーション用のリポジトリは分けないといけない

Custom Platformテンプレート用のGitリポジトリとデプロイするソースのリポジトリは分けないといけない模様。
ソースリポジトリのサブディレクトリにCustom Platformビルド用のディレクトリを作成して`ebp init`などを実行すると以下のエラーになる。

```bash
$ ebp init
ERROR: This command is not supported for Application workspaces.
```

リポジトリの.elasticbeanstalk/config.ymlファイルに以下の記述があるので、ここを見ているようだ。

```yaml:.elasticbeanstalk/config.yml
global:
  # ... 略
  workspace_type: Platform
```

# 拡張ヘルスチェック・・・？

よく知らないのだけど、ElasticBeanstalkの拡張ヘルスチェック機能というのがある。PIDファイルを監視してくれるらしい。
PhoenixはPIDをファイルに吐いてくれない。
[ここ](http://qiita.com/kawakami_o3/items/d33bfb9e71364ee7a6ca)を参考にUpstartの定義ファイルを作った。

# 参考

* [AWSドキュメント(カスタムプラットフォーム)](http://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/custom-platforms.html)
* [高生産性、高信頼性、高速のElixir製Webアプリケーションフレームワーク、Phoenixを始める](http://dev.classmethod.jp/server-side/productive-reliable-fast-phoenix/)

# Dockerを使わない理由（あくまで個人的なイメージです）

* イメージの管理が面倒（Dockerイメージ用のCI/CDが必要）
* td-agentを使いたい場合、マルチコンテナ構成になって面倒
* Dockerボリュームのレイテンシが気になる
* インスタンスストア（エフェメラルディスク）を使いたい
* プラットフォームカスタマイズの頻度が高い
* EB on Dockerの場合、スケールはEC2単位？
  * ECSと違い、コンテナ単位でのスケールができない？
  * そもそも余剰リソースがなければDockerのスケールは意味がない