Apache Mesos / Marathon を本番で運用するための5つのTips

> この記事は[VASILY DEVELOPERS BLOG](http://tech.vasily.jp/)にも同じ内容で投稿しています。よろしければ他の記事もご覧ください。

こんにちは。
インフラエンジニアの光野です。

先日の[ブログ記事](http://tech.vasily.jp/entry/iqon-crawler-by-docker-and-mesos-and-marathon)でご紹介したとおり、弊社のクローラーはDockerコンテナ化されています。このコンテナはApache MesosとMarathonのクラスタ上で動いています。

先日の記事はクローラーシステム全体を取り扱いましたが、本記事ではMesos/Marathonを導入するにあたって必要だった設定について「〜したい」という形で紹介いたします。
Tips集として導入や検討の参考にしていただければ何よりです。

記事中の用語については先頭の[前提知識・用語まとめ](#first)にまとめています。また、Tipsは各見出しごとに独立させていますので、お好きな部分を参照ください。

## シリーズ一覧

* 新クローラーシステムの全体観
  * [Docker / Apache Mesos / Marathon による3倍速いIQONクローラーの構築](http://tech.vasily.jp/entry/iqon-crawler-by-docker-and-mesos-and-marathon)
* クラスタへのデプロイについて
  * [Production deployment of the Docker container with Marathon by Tatsuro Mitsuno](https://speakerdeck.com/kotatsu360/production-deployment-of-the-docker-container-with-marathon)
* クラスタ構築時のTips
  * 本記事

## Tips一覧

* MesosのTips
  * [Tips 1. ホストポートをコンテナに割り当てたい（Mesos編）](#mesos_hostport)
  * [Tips 2. プライベートサブネット環境下でMesos UIを使いたい](#mesos_web_ui)
* MarathonのTips
  * [Tips 3. ホストポートをコンテナに割り当てたい（Marathon編）](#marathon_hostport)
  * [Tips 4. タスクでUserDefinedNetworkを使いたい](#marathon_udn)
  * [Tips 5. タスクの宣言をWeb APIで行いたい](#marathon_web_api)


<a name="first"></a>
# 前提知識・用語まとめ
本記事で使う用語や、コマンドを簡単にまとめます。
なお、Apache MesosとMarathon自体については先日の[ブログ記事](http://tech.vasily.jp/entry/iqon-crawler-by-docker-and-mesos-and-marathon)で触れておりますので説明を省略します。

## 登場する用語

| 名称 | 概要 |
| --- | --- | 
| Mesosクラスタ | Apache Mesosのマスタとスレーブから成る。zookeeperによって管理される |
| Mesosマスタ | Apache Mesosのマスタノード。スレーブに対してタスクを投入する。Web UIもここにある。 |
| Mesosスレーブ | Apache Mesosのスレーブノード。クラスタのリソースを担う。|
| Mesosエージェント | Mesosスレーブの各ノードで動くデーモン。起動オプションによってMesosでできることが変わる。|
| タスク | Apache Mesosで実行する処理。シェルコマンドからコンテナまでなんでもよい。本記事中では`docker run`されるもの。|

## ソフトウェアのバージョン

* Ubuntu 16.04.1 LTS
* Apache Mesos 1.1.0
* Marathon 1.4.1

## Mesos / Marathonの起動

Ubuntuの場合は、[Mesosphare社](https://mesosphere.com/)がパッケージ化してくれており、
リポジトリを追加することで`apt-get`を使ってインストールが可能です。

```bash

# master
sudo apt-get install mesos marathon
sudo systemctl start mesos-master
sudo systemctl start marathon

# slave
sudo apt-get install mesos
sudo systemctl start mesos-slave
```

初期設定については以下の記事がとても参考になります。リポジトリについても記載されています。
なおOSバージョン毎に存在するパッケージが若干異なるためご注意下さい。

[How To Configure a Production-Ready Mesosphere Cluster on Ubuntu 14.04 | DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-configure-a-production-ready-mesosphere-cluster-on-ubuntu-14-04)

## Mesosエージェントの設定方法
1. 起動時にオプションとして与える
   * `mesos-agent --resources=ports:[80-80, 31000-32000]`
2. 設定ファイルに記述する
   * オプション名 = ファイル名
   * ファイルの内容 = 引数
   * `echo 'ports:[80-80, 31000-32000]' > /etc/mesos-slave/resources`

本記事では2の方法を使っています。

ref. [Apache Mesos - Configuration](http://mesos.apache.org/documentation/latest/configuration/)

```bash
sudo systemctl restart mesos-slave
```

なお、リスタートに失敗するようであればlatestディレクトリを削除して下さい。

```bash
sudo rm -rf /var/lib/mesos/meta/slaves/latest
```

Mesosマスタはzookeeperを介して各スレーブを識別しており、その情報がlatestディレクトリに記録されています。
記録されている情報と新しい設定が食い違うとリスタートに失敗するため、latestを削除し新しいスレーブとして認識させます。

ref. [Apache Mesos - Slave Recovery in Apache Mesos](http://mesos.apache.org/blog/slave-recovery-in-apache-mesos/)

## Marathonのタスク宣言
Marathonには大きく3つのタスク宣言方法がありますが、編集の手段が異なるだけで最終的なリクエストは同じJSONです。

1. Web UIで宣言する
2. Web UIのJSONモードで宣言する
3. Web APIで宣言する

本記事でも文中にMarathon用のJSONを記述します。
ただ、説明に必要な部分だけを抜粋しているため、コピー&ペーストでは動作しません。

# MesosのTips
<a name="mesos_hostport"></a>
## Tips 1. ホストポートをコンテナに割り当てたい（Mesos編）
実行するタスクによってはホストのポートを専有したいことがあるかもしれません。
その場合、予めMesosエージェントに起動オプションを与えておく必要があります。
デフォルトで`[31000-32000]`が専有可能ですが、これに80番を追加する場合は次のように指定します。

```bash
echo 'ports:[80-80, 31000-32000]' > /etc/mesos-slave/resources
sudo systemctl restart mesos-slave
```
<a name="mesos_web_ui"></a>
## Tips 2. プライベートサブネット環境下でMesos UIを使いたい
MesosはWeb UIを持っており、ここからクラスタやタスクの状況、またタスクごとのサンドボックスを確認することができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/62601/95279050-7c90-ea7c-43c5-db0caade70d0.png)

サンドボックス内には、fetch済みのファイルやログファイルがあるためデバッグ時に大変便利です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/62601/2ebe9952-86e5-2735-7a12-daefcce866d0.png)


この情報は、Web UIがMesosスレーブに対して直接リクエストを行い収集しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/62601/d23f6883-9d2c-3006-8f29-be80808fe66a.png)


一方、AWSのベストプラクティスに従うとMesosクラスタはプライベートサブネットに構築されることが多いと思います。
実際に、弊社のMesosクラスタは次の構成になっています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/62601/d368c4b0-a58d-6d15-ca41-2528264d8a97.png)


MesosのWeb UIでサンドボックスの中を確認するためには、手元からプライベートサブネットにあるMesosスレーブへアクセスする必要があります。
間にELBとnginxによるプロキシを挟みこれを解決します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/62601/56f9e1a7-82de-0263-7f50-e73af4709b15.png)


### Mesos Config
Web UIがスレーブにアクセスする場合、その問い合わせはMesosエージェントに起動オプションとして与えられたホストネームに対して行われます。

まず、nginxで扱いやすいユニークな名前を設定してください。

```bash
echo "$(hostname).mesos-slave.xxxxx.yyyyy" > /etc/mesos-slave/hostname
sudo systemctl restart mesos-slave
```

xxxxx / yyyyyは、適宜ご自身で所有されているドメインへ読み替えてください。

### DNS Record

ELBに対するAliasレコードとして`*.mesos-slave.xxxxx.yyyyy`を設定してください。

### nginx

nginxを使って、特定のルールに基づくホストネームから名前解決を行い、
プライベートサブネットに存在する各スレーブへプロキシします。
なお、コメントにもありますが、5051ポートはMesosエージェントが利用するためnginxは別ポートでListenしています。

```conf
server {
    set_real_ip_from   10.0.0.0/8;
    real_ip_header     X-Forwarded-For;

    # [NOTE] 5051はMesosエージェントがbindする
    # ELBでポートを変えて送信。
    # Web UI -> 5051 ELB -> 15051 nginx -> 5051 Mesosスレーブ
    listen 15051;
    server_name .mesos-slave.xxxxx.yyyyy;

    location / {
        # [NOTE] Route53
        resolver 10.0.0.2;

        # [NOTE] 定期的に名前解決を行えるようにsetする
        # 直接書くとnginx restartでしか名前解決が行われない
        if ($host ~* (.*)\.mesos-slave\.xxxxx\.yyyyy) {
            set $mesos_slave_server "$1.YOUR_AWS_REGION.compute.internal";
        }

        proxy_pass http://${mesos_slave_server}:5051;
    }
}
```

ここではRoute53をVPC内のprivate DNSとして使っています。

# MarathonのTips
<a name="marathon_hostport"></a>
## Tips 3. ホストポートをコンテナに割り当てたい（Marathon編）
Marathonにおいて特定のホストポートを専有するタスクはスケジューリングに制約を与えることから非推奨になっています。
とはいえ実行するタスクによってはホストのポートを専有したいことがあるかもしれません。
この場合、Marathonでタスクを宣言する際にオプションを与える必要があります。

### ポートを割り当てる（入門編）
まずは公式ドキュメントに従って単純に設定します。

```json
{
  "container": {
    "type": "DOCKER",
    "docker": {
      "network": "BRIDGE",
      "requirePorts": true,
      "portMappings": [
        { "containerPort": 3000, "hostPort": 80, "protocol": "tcp"}
      ]
    }
  }
}
```

1. requirePortsをtrueに設定
2. portMappingsでhostPortを0ではない値に設定

hostPortで指定するポート番号は、予め[Mesosエージェントに起動オプション](#mesos_hostport)で許可されている必要があります。　
これらは、公式ドキュメントの[トラブルシューティング](https://mesosphere.github.io/marathon/docs/host-port.html)にわかりやすくまとめられています。

### ポートを割り当てる（実践編）
入門編の内容で、ポートを割り当てる事自体は完了です。実践編ではデプロイ時の問題を解決します。

```json
{
  "container": {
    "type": "DOCKER",
    "docker": {
      "network": "BRIDGE",
      "requirePorts": true,
      "portMappings": [
        { "containerPort": 3000, "hostPort": 80, "protocol": "tcp"}
      ]
    }
  },
  "constraints": [
    ["hostname", "UNIQUE"], // あるタスクがMesosスレーブあたり高々1コンテナになるようにする
  ],
  "upgradeStrategy": {
    "minimumHealthCapacity": 0, // デプロイ時、旧コンテナ数が0になることを許容する
    "maximumOverCapacity": 0 // デプロイ時、旧タスクをkillしてから新タスクをrunする
  } 
}
```

`requirePorts`と`hostPort`に加えて、`constraints`と`upgradeStrategy`を設定しています。

Marathonはタスクを更新する際、`upgradeStrategy`とに基づいてタスクを[ローリングリスタート](https://mesosphere.github.io/marathon/docs/deployments.html#rolling-restarts)してくれます。
新しいタスクが何らかのバグで起動しない場合も、古いタスクがそのまま動き続けるため安全です。
しかし、既存のタスクがホストポートを専有するタスクの場合、新しいタスクはポートをバインドできずデプロイが必ず失敗するという状況に陥ります。

その為、`constraints`を使ってタスクのスケジューリングに制約を与えた上で、`upgradeStrategy`を変更して旧タスクが無い状況を作り出すことで問題を回避します。
幸いにも弊社で運用されているポートを専有するタスクは、最悪瞬断しても良いという類のものでした。
もし、瞬断が許されない条件で動かす場合は、別の工夫が必要になります。

<a name="marathon_udn"></a>
## Tips 4. タスクでUserDefinedNetworkを使いたい
DockerのUDNを使いたい場合は、3箇所の宣言が必要です。

```json
{
  "container": {
    "type": "DOCKER",
    "docker": {
      "network": "USER"
    }
  },
  "ipAddress": {
    "networkName": "mesos_slave_host_nw"
  },
  "ports": []
}
```

1. `network`にUSERを指定
2. `ipAddress`にUDN名を指定
   * `docker network create`したときの名前
3. `ports`に空配列を指定

空配列を明示的に指定しないと、エラーになります。
portsはオプショナルな項目のため、ついつい忘れがちです。ご注意ください。

<a name="marathon_web_api"></a>
## Tips 5. タスクの宣言をWeb APIで行いたい
Marathonは整理された[Web API](https://mesosphere.github.io/marathon/docs/rest-api.html)をもっています。
Web APIには[コンソール](https://mesosphere.github.io/marathon/api-console/index.html)も用意され、各パラメータの詳細を確認することが可能です。

Web APIはRESTfulに設計されており、状況に応じてPOST/PUT/PATCH/DELETEを使い分けます。

* POST: 新タスクの宣言
* PUT: 既存タスクの更新 / もし既存タスクがなければ作成される
* PATCH: 既存タスクの更新（1.4.1時点で`/v2/apps`以下のエントリポイントのみ）
* DELETE: タスクの削除

操作したいリソースに対するエントリポイントさえ分かれば自然に利用できるかと思いますが、
その中で`/v2/apps`に対するPUTについては注意が必要です。

```bash
curl -XPUT -H "Accept: application/json" -H "Content-type: application/json" <Mesosマスター>/v2/apps/ -d@app.json
```

PUTはとても便利でPOST/PATCHの両方を兼ねてくれるのですが、
[Marathon 1.4系](https://github.com/mesosphere/marathon/releases/tag/v1.4.0)からPATCHのように振る舞うPUTについてDeprecatedになりました。

後方互換性を守るため、1.4.1時点ではPUTとPATCHに挙動の差はありません。ただし、次のバージョン（おそらく1.5.0）で変更されるという宣言がされています。

```
For backward compatibility, we will not change this behaviour, but let users opt in for a proper PUT.
The next version of Marathon will use PATCH and PUT as two separate actions.
```
将来のバージョンアップを考えると、PATCHを積極的に利用するのが望ましいです。

余談ですが、1.4.0には「PATCHのように振る舞うPUTがすべてエラーになる」という不具合がありUIも一部動作しません。そのためアップデートの際には1.4.1以降を選択下さい。

# おわりに
Apache MesosとMarathonを本番運用するにあたって必要になるであろう内容をTipsの形でご紹介いたしました。

Apache MesosとMarathonは実際に運用している情報が少なく、発生する問題に対しては自力で解決する必要があります。
とはいえ、両者とも機能そのものは豊富ですし、なにより公式の情報がとても丁寧に整備されています。
そのため、ドキュメントさえ読み込めば大抵のことはフレームワーク上で解決できるというのが、実際に運用してみての感想です。

今後もまた問題が発生するとは思いますが、都度ドキュメントとにらめっこして解決して解決していこうと考えています。