AWS Beanstalk(Docker) 事始め

# 背景

ちょっとAWS上でサービスをデプロイしたいと思う時があります。本格的にデプロイするとするとCloudFormationでインフラ作ってCodeDeployなどででアプリをデプロイするという形になると思います。将来的に本番で使うならいざしらず、開発の初期や何かのテストという場合はやりたいことの割には大掛かりになりがちです。
そんな用途の時にはAWS Beanstalkがお手軽そうに感じたので、ちょっと調べてみることにしました。

# AWS Beanstalkって？

[公式ページ：AWS Elastic Beanstalk](https://aws.amazon.com/jp/elasticbeanstalk/)から引用します。

> AWS Elastic Beanstalk は、Java、.NET、PHP、Node.js、Python、Ruby、Go および Docker を使用して開発されたウェブアプリケーションやサービスを、Apache、Nginx、Passenger、IIS など使い慣れたサーバーでデプロイおよびスケーリングするための、使いやすいサービスです。

テストなどのお手軽用途だけでなく、本格的なデプロイにも使えそうです。

# Dockerfieが使える？

[Docker プラットフォームの使用](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/docker.html)によると、Dockerfileを使う構成も、docker-compose.ymlを使う構成も出来るようです。

# 今回の目標

- docker-composeを使用して、nginxと単純なwebサーバーを構築する
- AWSコンソールではなく、ローカルからのコマンドで操作する

# コマンドラインツール

[公式ページ：Elastic Beanstalk コマンドラインインターフェイス (EB CLI) の使用](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/eb-cli3.html)にある通り、ブラウザでのAWSコンソール以外でも管理できるようです。今回の目的に必要なのでこちらを使います。

## インストール

[公式Github：EB CLI Installer](https://github.com/aws/aws-elastic-beanstalk-cli-setup)に従って対応します。
対象OSはUbuntuです。

- Gitがない場合はinstallする必要があるようです。ただ自分の場合は既にインストールしているのでスキップします。

- 必須パッケージをインストールします

```bash
apt install build-essential zlib1g-dev libssl-dev libncurses-dev libffi-dev libsqlite3-dev libreadline-dev libbz2-dev
```

- gitリポジトリをクローンします

```bash
git clone https://github.com/aws/aws-elastic-beanstalk-cli-setup.git
```

- インストーラーを実行します

```bash
./aws-elastic-beanstalk-cli-setup/scripts/bundled_installer
```

- 環境変数をセットします

インストーラーのメッセージの最後に、パスを通すようにメッセージが表示されます。`.bash_profile`への追記は他の動作に影響しそうです。ebへのパスは良いとして、pythonの方は影響する確率高そうです。不安な場合は上手いこと分けたほうが良さそうです。今回は都度exportすることにします。

```bash
export PATH="/home/hogeuser/.ebcli-virtual-env/executables:$PATH"
export PATH=/home/hogeuser/.pyenv/versions/3.7.2/bin:$PATH
```

<details><summary>出てくるメッセージ</summary><div>

```console:hogeuserはUbuntuOSのユーザー名です
***************
5. Finishing up
***************
Success!

    Note: To complete installation, ensure `eb` is in PATH. You can ensure this by executing:

    1. Bash:

       echo 'export PATH="/home/hogeuser/.ebcli-virtual-env/executables:$PATH"' >> ~/.bash_profile && source ~/.bash_profile

    2. Zsh:

       echo 'export PATH="/home/hogeuser/.ebcli-virtual-env/executables:$PATH"' >> ~/.zshenv && source ~/.zshenv

   
    - NOTE: To complete installation, ensure `python` is in PATH. You can ensure this by executing:
   
      1. Bash:
   
         echo 'export PATH=/home/hogeuser/.pyenv/versions/3.7.2/bin:$PATH' >> /home/hogeuser/.bash_profile && source /home/hogeuser/.bash_profile
   
      2. Zsh:
   
         echo 'export PATH=/home/hogeuser/.pyenv/versions/3.7.2/bin:$PATH' >> /home/hogeuser/.zshrc && source /home/hogeuser/.zshrc
```
</div></details>

## ElasticBeanstalkプロジェクト作成

ElasticBeanstalkのコマンドラインツールを使用するには、`aws cli`が使える状態になっている必要があります。`aws configure`などを実行し、デプロイしたいAWSアカウントに対してawscliが使用出来るように、状況に応じた対応を行っておきます。
プロジェクトを作りたいフォルダ以下に移動して初期化します。

```bash
eb init
```

- リージョンを聞かれるので選択します。自分は`ap-northeast-1`なので、`9`を選択します。
- プロジェクト名を聞かれるので入力します。フォルダ名がデフォルトになってます。そのままにします。
- プラットフォームを選択します。PHPとかPythonとか選べます。今回はDockerを選択します。`3`を選択します。
- OSを選択します。AmazonLinux2以外が非推奨なので、`1`を選択します。
- CodeCommitとの連携が出来るようですが、今回は無視します。
- ssh用のキーペアの設定を選択します。既存のキーペアの番号を選択します。

# docker-compose構成

以下のようなフォルダ構成になるようにファイルなどを準備します。
appフォルダ配下のファイルは、[AWS公式ページ：チュートリアルおよびサンプル](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/tutorials.html)にあるdocker.zipから、アプリ用のファイルを抜き出しました。

```console
.
├── .elasticbeanstalk/  # eb init により生成されます
│   └── config.yml
├── .gitignore          # eb init により生成されます
├── Dockerrun.aws.json  # 新規作成します（詳細は後述）
├── app/
│   ├── Dockerfile      # サンプルからコピー
│   └── application.py  # サンプルからコピー 
├── docker-compose.yml  # 新規作成します
└── nginx/
         └── nginx.conf # 新規作成します
```

<details><summary>docker-compose.yml</summary><div>

```yml:
version: '3.7'
services:
  nginx:
    image: nginx
    ports:
      - 80:80
    volumes:
      - ./log/nginx/:/var/log/nginx/
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - app
    container_name: beanstalk-nginx
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    volumes:
      - ./log/app:/tmp/sample-app
    ports:
      - 8000:8000
    container_name: beanstalk-app
```
</div></details>

<details><summary>nginx.conf</summary><div>

```conf:
user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 768;
    # multi_accept on;
}

http {

    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    # server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # SSL Settings
    ##

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

    ##
    # Logging Settings
    ##

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    ##
    # Gzip Settings
    ##

    gzip on;
    gzip_disable "msie6";

    include /etc/nginx/sites-enabled/*;

    upstream app {
        server app:8000;
    }

    server {
        listen 80 default_server;
        server_name localhost;
        charset utf-8;

        location / {
           proxy_pass http://app/;
        }
    }
}
```
</div></details>

# ElasticBeanstalk設定ファイル（Dockerrun.aws.json）作成

本来、ElasticBeanstalk特有の設定を行うものだと思いますが、内容はdocker-compose.ymlの中身と一緒です。
UrlからするとまだV2ですが、[公式ページ：複数コンテナの Docker 設定](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create_deploy_docker_v2config.html)のページを参考に作成します。

<details><summary>Dockerrun.aws.json</summary><div>

```json:
{
  "AWSEBDockerrunVersion": "3",
  "volumes": [
    {
      "name": "app-log-vol",
      "host": {
        "sourcePath": "/var/app/current/log/app"
      }
    },
    {
      "name": "nginx-log-vol",
      "host": {
        "sourcePath": "/var/app/current/log/nginx"
      }
    },
    {
      "name": "nginx-conf-vol",
      "host": {
        "sourcePath": "/var/app/current/nginx/nginx.conf"
      }
    }
  ],
  "containerDefinitions": [
    {
      "name": "app",
      "image": "app",
      "essential": true,
      "memory": 128,
      "mountPoints": [
        {
          "sourceVolume": "app-log-vol",
          "containerPath": "/tmp/sample-app"
        }
      ]
    },
    {
      "name": "nginx",
      "image": "nginx",
      "essential": true,
      "memory": 128,
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 80
        }
      ],
      "links": [
        "app"
      ],
      "mountPoints": [
        {
          "sourceVolume": "nginx-log-vol",
          "containerPath": "/var/log/nginx"
        },
        {
          "sourceVolume": "nginx-conf-vol",
          "containerPath": "/etc/nginx/nginx.conf"
        }
      ]
    }
  ]
}
```
</div></details>


## volumesブロック

ホスト側パスのエイリアスを指定するもののようです。後にマウントで使用します。
プロジェクトルートからのパスを`/var/app/current/`に追記する形で記載します。

## containerDefinitionsブロック

コンテナ毎の詳細を記載します。以下その配下のブロックです。

### mountPointsブロック

先に指定したvolumesのエイリアスと、コンテナ内部のパスを結びつけます。docker-compose.ymlのvolumesブロックですね。
sourceVolume：ホスト側パスのエイリアス、containerPath：コンテナ内部のパス

### portMappingsブロック

ホスト側とコンテナ内部のポート結びつけです。docker-compose.ymlのportsブロックですね。
hostPort：ホスト側ポート、containerPortコンテナ内部ポート

### linksブロック

依存関係です。docker-compose.ymlのdepends_onブロックですね。

## ふと思ったこと

docker-compose.ymlの設定とDockerrun.aws.jsonの設定で不整合があったらどうなるのだろう？（未実験）

# ローカルで起動チェック

```bash:
eb local run
```

しかし、以下エラーが出てきます。

```console:
ERROR: ValidationError - The AWSEBDockerrunVersion key in the Dockerrun.aws.json file is not valid or is not included.
```

別件で試した時、こんなエラーも出てきました。

```console:
ERROR   Instance deployment: 'Dockerrun.aws.json' in your source bundle specifies an unsupported version. Elastic Beanstalk only supports version 1 for non compose app and version 3 for compose app. The deployment failed.
```

おそらくですが、docker-composeを使う時にはAWSEBDockerrunVersionを`3`にしなくてはならず、その時にはローカル実行は出来ないのだと思います。docker-compose.ymlがあるのだからdocker-composeを使ってローカルテストせよという事でしょう。

という事で、普通にdocker-composeを使ってローカルで動作確認します。

# AWS環境作成

必要なファイルをzip化してAWS上に環境を作成できます。以下コマンドを実行します。

```
eb create my-testenv
```

```console
2021-09-25 01:45:24    INFO    Application available at my-testenv.eba-XXXXXXXX.ap-northeast-1.elasticbeanstalk.com.
2021-09-25 01:45:25    INFO    Successfully launched environment: my-testenv
````
というメッセージが出てきて、デプロイ成功したようです。

# アクセスチェック

http://my-testenv.eba-XXXXXXXX.ap-northeast-1.elasticbeanstalk.com/
無事アクセスできました。

# 実際のリソース

起動している間にどんなリソースが生成されたかチェックします。

- EC2(t2.micro)※デフォルトVPCに作成されました。
- Auto Scaling グループ
- ELB(Classic)

手動でチェックしましたがCloudFormationみたく実際のリソースリストがAWSコンソールから見れるといいですね。
ドメイン名からすると、elsticbeanstalk用のR53からサブドメインが割り振られ、それがELBと結びついているという形と思われます。
また、docker-composeを使って２つのコンテナで構成しましたが、EC2は１つでした。AutoScalingの下で動いている以上、それぞれのインスタンスで分けて何か自動で判断するのはさすがに無理ですね。
本番で使用する際にはdocker-composeを使わず、処理内容に応じて１インスタンス１コンテナの構成にしてそれぞれの処理でAutoScalingするというのが望ましいと思います。今回のnginxなんて本来はCloudFrontに任せるべき処理ですし。

今回の使い方ではECRにイメージが作成されることも無い様でした。もちろんDockefile内の処理が多い場合には事前にイメージ作っておいたほうが良いですが、ちょっと試すぐらいなら手順が少なかったり変なゴミが発生しなかったりでよいと思います。

# AWS環境破棄

ec2インスタンスなどが起動したままだとお金がかかるので、テスト終了したら環境破棄します。

```
eb terminate my-testenv
```

# 最後に

標準的なwebサービス構成、ELB＋Autoscaling＋EC2（webサーバー）＋DBの形ならばかなり楽になりそうです。
また、今回の様にちょっとだけAWS内にサービス立てたいという時にも有効な選択肢かと思います。
もっとElasticBeanstalkで出来ることを追っておきたいと思えるサービスです。

# 更新履歴

2021-09-25：`Dockerrun.aws.json`の記述を全然していなかったので追記しました。