Docker MachineでMacからVPS上のDockerへアプリをデプロイしよう


# はじめに

[Docker Machine](http://docs.docker.jp/machine/overview.html)を使ったアプリのデプロイにて、VPSや自サバを対象とするケースの記事が少ない印象だったので本記事を書きました。

Docker Machineの機能を利用することで普段使いのローカルPCから直接(sshログイン無しに)VPSや自サバ上へDockerを構築、イメージとコンテナを管理、アプリをデプロイすることができます。

Docker Machineでのリモート化による嬉しい点には以下があります。

* ローカルからできるので、対象のDockerホストサーバにログイン→ソースやイメージをPull、する必要がない。
* サードパーティのデプロイツールに頼らずDockerの標準機能だけでアプリがデプロイできる。
* 複数のDockerホストを扱うとき必要があるとき、すべてローカルから操作できる。

本記事では個人開発サービスやスタートアップサービスアプリなどのデプロイを前提とし、1つのConoha VPS上にDocker Composeでデプロイするまでを記載します。KubernetesやDocker Swarmを使った複数ホストが対象のクラスタリング利用は含まれていません。

# 前提・環境

## Mac (ローカル側)

MacOS: High Sierra 10.13.6  
Docker for Mac: Docker Engine, Docker Machineのバージョンはこのスクショ画像を参照
 <img width="582" alt="スクリーンショット 2018-11-25 16.43.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/91562/f3c376ba-a003-340b-8e81-8b80071e1d4c.png">

## Conoha VPS (サーバホスト側)

OS: Ubuntu 18.04 LTS (Bionic Beaver)


# SSH公開鍵認証ログインできるユーザーを作成して設定

本記事ではrootユーザではVPSへ直接ログインできないことを前提とします。そのため、Docker Machineを経由して対象ホストへログインするためのSSH公開鍵認証のログインができるユーザーが必要です。

[こちらの記事](https://qiita.com/ir-yk/items/af8550fea92b5c5f7fca)などを参照することでユーザ作成できます。

# Generic DriverでVPS上にDockerホストを構築

以下のスクリプトを実行することでVPS上にDockerがインストールされ、1つのMachineとして構築されます。

```shell:create_docker_machine.sh
#!/bin/bash

IP=対象VPSのIPアドレス
USER=username
KEY=~/.ssh/id_rsa
NAME=conoha1 # Dockerホストサーバへの任意の名前

docker-machine create \
  --driver generic \
  --generic-ip-address=$IP \
  --generic-ssh-user $USER \
  --generic-ssh-key $KEY \
  $NAME
```

今回はDockerホストとなるサーバがVPSなので`--driver generic`を指定します。
ちなみに、[公式ドキュメント](http://docs.docker.jp/machine/drivers/toc.html)を見ると、AWS、GCP、Azure、Degital Ocean、OpenStackとクラウドサービスごとにドライバーが用意されていることがわかります。

## 【注意】 visudoで設定を変更しないとエラーになる。

上記の`docker-machine`コマンドを実行すると下記のようなエラーとなり失敗するかと思われます。

```
Running pre-create checks...
Creating machine...
(conoha1) Importing SSH key...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with ubuntu(systemd)...
Error creating machine: Error running provisioning: ssh command error:
command : sudo hostname conoha1 && echo "conoha1" | sudo tee /etc/hostname
err     : exit status 1
output  : sudo: no tty present and no askpass program specified
```

どうやらdocker-machineがVPSホストに入ってsudo実行しようとしてもパスワード入力が必要なので処理を続けられないようです。
ググってみると、[こちらのissue](https://github.com/docker/machine/issues/1569)で同様の話があがっており、最終的に[こちらのgenericドライバーに関するドキュメント](https://github.com/vmware/docker-machine/blob/master/docs/drivers/generic.md#sudo-privileges)の更新に反映されています。

> This user has to have password-less sudo privileges. If it's not the case, you need to edit the sudoers file and configure the user as a sudoer with NOPASSWD.

つまり、対象ユーザはsudo実行時にパスワード入力を必要としない設定にする必要があります。

私の場合、下記のように、sudoグループのユーザーはsudo実行時にパスワード入力不要の設定にしています。(この設定は当然、ユーザーを乗っ取られた場合、好き放題サーバを操作されてしまうので、設定した際にはユーザのログインしっぱなしなどを決してしないようにする必要があるでしょう。)

```sh:visudo
# Allow members of group sudo to execute any command
# %sudo ALL=(ALL:ALL) ALL # original setting
%sudo   ALL=(ALL) NOPASSWD:ALL
```

# 対象リモート先Dockerホストを操作できるかを確認

はじめにVPS上のDockerをMackから操作できるか確認しましょう。

## コンテキスト指定

以下のコマンドで`docker`コマンドがVPS上のDockerを指すように指定します。

```shell_session
$ NAME=conoha1 # docker-machine createで指定したホスト名
$ eval $(docker-machine env $NAME)
$ env | grep DOCKER # 指定した環境変数を確認
```

`docker-machine env $NAME`は実行するとわかりますが、環境変数のexportをしているのでevalに食わせることで適用されます。
これでリモート先と繋がります。`docker image ls`などを実行するとMac上のDockerではなくVPS上に配置されたDockerであることが確認できると思います。

## コンテキストを解除

操作し終わったら以下のコマンドでコンテキストを解除しましょう。解除するとローカル上のDockerが対象に戻ります。

```shell_session
$ eval $(docker-machine env -u)
```

# MacからDocker Composeでアプリをデプロイ

Docker ComposeでVPS上のDockerにアプリをデプロイしてみましょう。

本記事ではMySQLを利用するGo言語のAPIサーバアプリケーションを例とします。

## サンプルアプリについて

ファイル構成はこのような感じです。

```shell-session:アプリ構成
.
├── Dockerfile
├── db
│   └── init
│       └── init.sql
├── deploy.sh
├── docker-compose.prod.yml
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
└── scripts
    └── create_docker_machines.sh
```

Macのローカル上で`docker-compose build`=>`docker-compose up`をするとDockerでアプリケーションが立ち上がる前提とします。

なので本記事では`Dockerfile`と`main.go`のコードの詳細は割愛します。`main.go`は環境変数を参照してMySQLに接続しHTTPサーバを立ち上げるアプリだとみなしてください。

## `docker-compose.prod.yml`で本番環境用上書き設定

普段のローカル開発では`docker-compose.yml`のみを利用しますが、VPS環境(本番環境とします)にDocker Composeでデプロイする際は`docker-compose.prod.yml`という別ファイルを用意し`docker-compose.yml`の内容を上書きすることで環境の差異を補いましょう。

上記のサンプルアプリの例ではそれぞれ以下のようになります。

```yaml:docker-compose.yml
version: '2'
services:
  api:
    build: .
    ports:
      - "5000:5000"
    depends_on:
      - db
    links:
      - db
  db:
    image: mysql:5.7.22
    ports:
      - "3306:3306"
    volumes:
      - "./db/init:/docker-entrypoint-initdb.d"
    environment:
      MYSQL_ROOT_PASSWORD: localmysqlpass
      MYSQL_DATABASE: superappdb
```

```yaml:docker-compose.prod.yml
version: '2'
services:
  db:
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: prodsecurepass
volumes:
  mysql_data:
    driver: local
```

mysqlイメージの使い方に関しては[公式](https://hub.docker.com/_/mysql/)を参照。

prodの方のファイルで`db`サービス(MySQL)のvolumeと環境変数を上書き変更しています。

注意点としてvolumeはDocker Machineでの操作であっても、ローカル(Mac)側ではなくリモート先(VPS)のホスト上のリソースがvolumeとなりコンテナにマウントされます。
上記の`docker-compose.prod.yml`の場合、Docker Composeのトップとして[data volume](https://docs.docker.com/storage/volumes/)を明示的に定義しているので、実行するとリモート先ホスト内にvolumeが作成されデータが永続化されます。

## ビルド&デプロイ用スクリプトを用意してデプロイ

デプロイ用のスクリプトを用意しましょう。上記のアプリ構成にある`deploy.sh`がそれにあたります。
内容は以下になります。

```sh:deploy.sh
#!/bin/bash

set -eu

export DEPLOY_DOCKER_MACHINE="conoha1" # 設定したDocker Machineの名前
export DEPLOY_SERVICE="api" # ビルド対象のDocker Composeサービス

# Change docker context to production environment
eval $(docker-machine env $DEPLOY_DOCKER_MACHINE)

# Build, and then Deploy
docker-compose build --no-cache $DEPLOY_SERVICE
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --no-deps -d $DEPLOY_SERVICE

# Reset docker context
eval $(docker-machine env -u)
```

`docker-compose -f docker-compose.yml -f docker-compose.prod.yml up --no-deps -d ` のように`-f`オプションを指定することでprod設定で上書きされ、アプリがリモート先ホストで立ち上がります。

`./deploy.sh`でMacからデプロイ完了です！

# 参考

* https://qiita.com/tukiyo3/items/02ad056f851cb845b1d4
* https://qiita.com/mochizukikotaro/items/0f7227e8f02c4ab34e6d
* https://docs.docker.com/v17.09/machine/drivers/generic/