Docker Swarmで画像処理apiを作りました。

https://qiita.com/advent-calendar/2018/excite
2日目の記事です。
エキサイトのニュースメディア技術で主にバックエンドを担当しています永野です。
約1年ぶりですが[この記事]( https://qiita.com/glassmonkey/items/04e6ec455c4217b36a07 )の続編です。Docker運用の知見がない弊社にて、ほぼ独学でコンテナ開発したものを約半年間Docker Swarmでサービスインしてみて運用感想をつづった記事となります。
画像処理機能の根幹部分は昨年の記事とあまり変わらないので今回は深くは扱いません。

![0_Hl-1BN46JLiTh8UM.png](https://qiita-image-store.s3.amazonaws.com/0/152938/b948b082-5f3d-2b26-7d7c-44e1a9dbd2d6.png)


# 何を作ったか
顔認識していい感じなサムネイルを自動する生成するapiを作りました。
Docker Swarmで稼働中です。ロジックはpythonで書きました。

## 変換例
例えば[弊社の記事](https://www.excite.co.jp/news/article/E1540107059902/)を例に挙げると下記のような変換を行います。
### 元画像
![trim.jpg](https://qiita-image-store.s3.amazonaws.com/0/152938/7de05a36-4904-c5d5-0db1-71785d69c40e.jpeg)
### 生成画像
![trim (1).jpg](https://qiita-image-store.s3.amazonaws.com/0/152938/1fe51b68-c07a-4cc9-f6dc-d7cd1e0d181c.jpeg)

# モチベーション
このapiを開発するにあたってコンテナ化しよう！！と思った理由は色々ありました。

-  変換apiなので状態を持つ必要がない。ステートレスな方が永続化など考えることが少ない。
- apiに求められる機能が画像変換だけなので、開発コストは高くない。事実去年のアドベントカレンダー執筆時点でほぼメインのロジックは完成していた。
- 社内にコンテナ開発の知見がない。やってみたい
- 開発だけでなく運用もやってみたい。

# なぜDocker Swarmか
## 背景
昨今のコンテナの運用だと[kubernetes (k8s)](https://kubernetes.io/case-studies/) がコンテナオーケストレーションツールとしては主流です。しかし弊社だと下記のような現状であり、k8sも導入となると学習コスト的にリリースが遅れることは必至でした。

 - プライベートクラウドであるOpenStackを利用しているが、アプリケーションエンジニアが基本のサーバー管理も行っている。
 - 社内全体でコンテナ運用の知見が乏しい
 
次点では[この記事]
(https://thenewstack.io/kubernetes-vs-docker-swarm-whats-the-difference/)にあるように、Docker単体でもマルチホストにコンテナオーケストレーションができるモードである[Docker Swarm](https://docs.docker.com/engine/swarm/)があげられると思います。
開発で使っていたdocker-composeの延長で利用できるとのことだったので、敷居が低そうなDocker Swarmを採用しました。

## Docker Swarmとは
Dockerが提供する。オーケストレーション機能です。必要なものはDockerさえインストールされていれば利用できます。1.12.0までは別ものだったようですが、それ以降は本体に取り込まれたみたいですね。
![0_Hl-1BN46JLiTh8UM.png](https://qiita-image-store.s3.amazonaws.com/0/152938/b948b082-5f3d-2b26-7d7c-44e1a9dbd2d6.png)
余談ですが
Docker=くじら　
Swarm=群れ
でこのアイコンはキュートで個人的に好きです。

## Docker Swarmの使い方
### 準備
[こちら](https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/) を参考に予めクラスタを作った状態にしておきます。各種作業はマネージャーノード上で行います。


### デプロイ
[こちら](https://qiita.com/tora470/items/5120d51f9dbab62d708e)が大変参考になります。
基本的にはswarm用のDocker Composeファイルを作ります。1行目のバージョン宣言でバージョン3で書けば動きますが、極力最新バージョンで書いたほうが便利です。

#### docker-compose.yml
```yml

version: "3.7"
services:
  db:
    image: postgres
    volumes:
      - data:/var/lib/postgresql/data
volumes:
  data:
    driver: mydriver
```

ただ、単体ホストで動かす想定のdocker-composeと違うこともそれなりにはあるので[公式の変更点]
(https://docs.docker.com/compose/compose-file/compose-versioning/#upgrading)は見ておくと吉です。

上記で作成したdocker-compose.ymlに対して下記のように実行すればhogehogeという名前でクラスタにアプリケーションを構成するコンテナ群がデプロイされます。
#### デプロイコマンド
```bash
$ docker stack deploy -c docker-compose.yml hogehoge
```
コンテナのスケーリングも予めこのように記載しておくことも出来たり自由度は高いです。ロードバランシングもよしなにやってくれます。

``` yml
deploy:
      replicas: 2
```

## 所感
今回DockerをDocker Swarmでプロダクション環境で運用した今回のチャレンジに対しての感想です。
Dockerをまともに触ったのは初めてだったので、それ踏まえた感じです。
マイナスポイントはぶっちゃけ、オンプレ環境特有の問題が大きかった感じはあります。

## プラスポイント
- どこでも開発が出来た点。
  - あまり褒められたことではないが、簡単な検証が会社以外でもできて結構捗った。 
- Docker特有の環境変数で環境依存の切り替えの考え方は、複数アプリケーションの環境依存要素を集約できるので定数管理が楽になった。
- アプリケーションレベルの開発環境の構築は楽になった。
  - 公式のイメージから構築を始めると新しい言語や構成に関しての敷居は低くなった。
- 汎用的なローリングアップデートの実現が容易。
  - 言語やアーキテクチャに左右されない汎用的なローリングアップデートが実現できたはず。
  - 副次的にブルーグリーンデプロイも実現
  - アプリケーションのバージョンアップがホストを止めずにコンテナデプロイで簡単にできるようになった。
- 学習コストはdockerコマンドにほぼ集約される。
  - dockerコマンドの延長なので、dockerコマンドさえ覚えてしまえば、コマンド体系が似ているのでその分学習コストが低い。
- Docker Composeでデプロイを書けるので、開発環境のほぼそのままでリリース状態まで持っていける。
  - ただし、プロダクション用と開発用は別のDocker Composeファイルを用意したほうがよき。
  - 開発はファイルをマウントしながら開発したいが、デプロイ時は不要なマウントはなくしてホスト依存を減らしておきたい点が大きかった。
- Dockerfileの作法が学べた 
  - Dockerファイルの開発時はビルド時間が短縮されるテクニックを模索した。
  - 今回扱ったpythonのコンテナでは、マルチステージビルドで開発とビルドを分けて、pipのビルドの短縮を行った。

```Dockerfile
#build用イメージ
FROM python:3.6 as build
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV DOCKER true
RUN apt-get update && \
    apt-get install -qy apt-utils && \
    apt-get install -qy build-essential && \
    apt-get install -qy cmake pkg-config && \
    apt-get install -qy libx11-dev && \
    apt-get install -qy libatlas-base-dev && \
    apt-get install -qy libgtk-3-dev && \
    apt-get install -qy libboost-python-dev && \
    apt-get -qy autoremove
COPY requirements.txt .
RUN pip install -r requirements.txt

#dev用のruntime。キャッシュをコピーしている。
FROM python:3.6-slim as dev
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV DOCKER true
RUN apt-get update && \
    apt-get install -qy libatlas-base-dev && \
    apt-get install -qy libboost-python-dev && \
    apt-get install -qy libglib2.0-0 && \
    apt-get install -qy libsm6 && \
    apt-get install -qy libxrender-dev && \
    apt-get install -qy libxext-dev && \
    apt-get install -qy libjpeg-dev && \
    apt-get -qy autoremove
COPY requirements.txt .
COPY --from=build /root/.cache /root/.cache
RUN pip install -r requirements.txt && rm -rf /root/.cache
COPY . .
  ```

## マイナスポイント
- オーバーエンジニアリング感
  - Docker Swarmを使うシチュエーションだと、GKEなどのマネージドなk8sを使うほうが理にかなってると感じた。
- Docker　Imageのサイズの肥大化対策
    - alpineはdebianとコマンド体系が違うかったりで画像処理系の依存周りが不明瞭だったのであきらめ。
    - 不要なファイルを消したりなど肥大化削減の工夫が求められた。

- プロダクションレベルでの運用情報の少ない。
  - 開発レベルの環境構築は楽だが、コンテナ監視などのツールがDocker Swarmはあまりソリューションが無い。(決定的にこれをつかっておくべきみたいなのが無さそう）
  - [こちらの記事] (https://qiita.com/tora470/items/5120d51f9dbab62d708e)でおすすめされた[portainer](https://github.com/portainer/portainer)を使ってはいます。ポチポチでコンテナのスケーリングができたり色々できるので結構便利。
  - k8sのistioとか？あまりサービスメッシュは理解できていない。。  
  - 各種publicクラウドもkubenatesはサポートしているがDocker Swarmはない 
  - k8sに比べると情報が少ない
- 事前に80番のサービスを作っていないと駄目だった。
  - [Dwangoさんの記事](http://ch.nicovideo.jp/dwango-engineer/blomaga/ar1353659)を参考にました。クライントipがデフォルトだと落ちないので、ホストの80番を専有するモードを起動しないといけなかった。
  - これをすると80番用のコンテナはローリングアップデートが出来なくなってしまった。
  - これもk8sだと問題ない？
  - ちなみに80番用のサービスには[nginx-proxy](https://github.com/jwilder/nginx-proxy)を利用しました。
  - ホストモードで起動する場合はportsの記述を詳細版で書けばOKです。

#### portsの記述の詳細版

```yml
ports:
  - target: 80
    published: 80
    protocol: tcp
    mode: host
```

- Dockerセキュリティの認識不足
  - ほぼ何も考えずに。。
  - [hadolint](https://github.com/hadolint/hadolint)などのチェックツールを利用ができてないので、今後利用する。
  - [最近のリリース](https://blog.docker.com/2018/11/introducing-docker-engine-18-09/)をみると結構セキュリティ周りを力いれてることがわかる。要チェック

- 弊社のOpen Stackとの相性がDockerと悪い。
    - 執筆時点での弊社のOpen Stack環境に[バグ](https://bugzilla.redhat.com/show_bug.cgi?id=1475460)があって、Dockerのブリッジモードがデフォルトだと使えない状態だった。mtuを指定する他なかった。mtuの数値は`ifconfig`などでeth0のNICを確認した。弊社環境は1400だったのでそれを明示的に設定。  

#### 弊社openstack環境下の必須のネットワーク設定(docker-composeの場合)  

```yaml
networks:
  local:
    driver: bridge
    driver_opts:
      com.docker.network.driver.mtu: 1400
```


#### daemon.jsonの場合

```json
{
    "mtu": 1400
}
```

# 感想
　今回のチャレンジのおかげで、バックエンドエンジニアとしては大きな武器を得ることができたなと実感しています。
　しかし、コンテナ開発に対して幻想を抱いていた感じはあったので、それも気づけて良かったです。
　特にアプリケーション開発時にコンテナで動かす前提の作りになってないと色々大変であることに気付かされました。ネットワークやホストに依存するロジックがあったりや、`PHP`と`gulp`のタスクランナーの処理が混在してたりで、コンテナ化の粒度がおかしくなってしまうケースがあったりと、既存のものをコンテナ化することは痛みが伴うことを覚悟が必要なんだと思い知らされました。
　長い視野で考えるとコンテナ化は選択肢として考えられるほうがいいと思うので、社内でDockerを引き続き布教していきたいです。


# 宣伝
実は11月初旬に[エキサイトニュース](www.excite.co.jp)はリニューアルしました。
<img width="1071" alt="スクリーンショット 2018-12-01 22.05.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/152938/ba25ab58-1850-1511-e830-f8718703ed27.png">
ここで使用されているサムネイルは全面今回作ったapiを利用しています。

サービスとしても、[fastly](https://www.fastly.com/)を使用して高速化に気をつけているので、日々の情報収集に利用してもらえると嬉しいです！！
fastlyに関してはこのアドベントカレンダーのどこかで同じニュースチームの先輩が紹介するので、そちらもぜひご覧ください。