LoginSignup
1
4

More than 3 years have passed since last update.

知識0からのDocker学習&活用 ~Blue-Green-Deployment~

Last updated at Posted at 2019-09-23

この記事は

Dockerを使って1台のマシンでBlue-Green-deploymentの環境を作ります。
同時に、Docker習得上で気が付いた点についてもまとめています。

少しnginx, rails, puma特有の内容が混ざります。

なぜBlue-Green-deploymentに行き着いたのか

Dockerが便利だよという話を聞いて色々調べたんですが
本腰入れるならdocker + kubernetesという結論に到達したためです。
中途半端にDockerを極めるくらいならサクッと活用を目指すのが
現状のDockerかなあと思っています。

構成

色々やり方はあると思いますが、今回はこんな構成にしました。

・nginx (proxy)
・rails + puma (app) ×2
これだけです。

インストールしたもの

Docker 19.03.2
Docker-compose 1.24.1
インストール方法は省略。OSとバージョン依存なので私も多分また公式見ます。

Dockerを使ってみての勘所

最初からリモートで動かそうとするのはダメ

オーケストレーションツールとの連携が印象的で、最初から運用環境での実行を考えていました。
が、Dockerは試行錯誤してうまくいくコマンドだけを整理して書いていく必要があります。

もしコンテナの立ち上げ中にエラーを出してもatatchやexecで途中から進めることは可能ですが、
コンテナを使い回すためには綺麗に書き直されたDockerfileやdocker-compose.ymlが必要です。
ですからまずは何度もやり直しができるローカル環境から始めるべきだと思います。
これに気がつくのに2日かかりました。

OSイメージはubuntuで

Docker hubの公式レポジトリでOS名の記載がないものはubuntuになっています。
リモートホストがcentOSなので、centOSの方がいいのかなと思いましたけど、
パッケージ管理コマンドをapt系に変更する以外影響はなく、問題なく動きます。
参考になるイメージも大体ubuntuベースなので、ubuntuが良いです。

軽量化にこだわってalpine linuxを使うという記事も多くありますが、
alpineになるとディレクトリをバインドしてホストOS側の機能を使う際に互換性がないです。
そうなるとかなり独自にエラーを吐きながらDockerfileを構築することになります。

これも気がつくのに2日かかりました。

Dockerfileとdocker-composeの使い分け

使い分けというか、さっくり使うレベルなら
極論ですがなるべくDockerfileは使わなくていいという結論に至りました。
というのはDocker-composeにできてDockerfileにできないことは多いのですが、
Dockerfileにできてdocker-composeにできないことがさほどないからです。

どちらも実際にやっていることは順番にdockerコマンドを呼び出しているだけです。

docker-composeからは
コンテナ間ネットワークの設定
コンテナ間での共通ディレクトリのバインド
といったコマンドが使えますが、Dockerfileにはありません。

Dockerfileにしかできないことは、シェルで実行するコマンドを複数書くことですが、
数行のコマンドであれば && で括ってしまえばdocker-composeでもできます。

なのでDockerfileに関しては全く触れません!
多分今後もあまりDockerfileは使わないでしょう!

なんなら自分でイメージを編集しない方が良い

暴論ですが、イメージを自分で作っているとDockerのいいところが失われていきます。
ホストOSのカーネルや他のコンテナと独立して環境を作れるのがDockerのいいところです。
例えばnginxのバージョンを上げようと思ったら、nginx最新版イメージに切り替えるだけ。
アプリケーションコンテナには依存ライブラリがないので、あれこれ弄る必要がないです。

イメージをいじくり回している暇があったら別のことをやったほうがいいですね。

手順

ディレクトリ構成

下図のようにディレクトリを切ります。

HostOS
docker-rails
├── docker-compose.yml
├── nginx
│   └── default.conf
├── testapp01
│   └── (Gemfileのみ必須)
└── testapp02
    └── (Gemfileのみ必須)

docker-compose.yml(rails用リハーサル)

bundle installで大体コケるので、一旦testapp01だけ作って試してみます。

command
cd testapp01
rails new testapp
docker-compose.yml
version: '3'
services:
  app01:
    image: "ruby:2.6"
    ports:
      - "3000:3000"
    volumes:
      - ./testapp01:/app
    working_dir: /app
    command: >
      bash -c "bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"

HostOSのtestapp01をコンテナの/appにバインドし、
working_dirでコマンドの実行ディレクトリにします。

command
cd ../
docker-compose up

docker hubのイメージを利用するので、docker-compose buildは不要です。
最初からupを使います。

なんかエラーが出ると思います。
今回はbundlerのバージョンが違うよというのと、
node.js入れてねというエラーが出ました。

前者はdocker-compose.ymlのコマンドを追加して解決します。

docker-compose.yml
version: '3'
services:
  app01:
    image: "ruby:2.6"
    ports:
      - "3000:3000"
    volumes:
      - ./testapp01:/app
    working_dir: /app
    command: >
      bash -c "gem install bundler:2.0.1 && bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"

後者はnode.js依存のgemをGemfileから削除して回避しました。
後で別途nodeのコンテナを立てて、webpacker使おうと思っているので。
turbolink使う方とかwebpackerの方はnode.jsをインストールするコマンドを追加しましょう。
もしかすると行が長くなってくるので、この辺で修正を多く入れる場合は
ちゃんとDockerfileを作った方がいいカモです。

その場合は、例えば./testapp01/DockerfileにDockerfileを作ったら

docker-compose.yml
version: '3'
services:
  app01:
    build: "./testapp01/Dockerfile"
    ports:
      - "3000:3000"
    volumes:
      - ./testapp01:/app
    working_dir: /app
    command: >
      bash -c "bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"

という感じになるはずです。
Dockerfile側は

./testapp01/Dockerfile
FROM ruby:2.6
RUN (some command necessary) &&\
    (another command necessary)

依存パッケージのインストールに必要なコマンドを入れてください。

なお、Dockerfileでbundle installを書いてもエラーを起こして停止します。
docker-composeのbuildでDockerfileのコマンドが呼び出され、完了後に次に進むので
Dockerfileのコマンド実行中はまだホストOSのディレクトリがバインドされておらず、
Gemfileが見つからないためです。

docker-compose upでコケなくなったら次に進みます。

railsアプリの準備

※rails固有の話なので関係のない人は飛ばしてください。

プロダクション環境で動くようにsecret_keyを発行します。

command
cd testapp01
bundle exec rake secret

secret keyをコピーしておきましょう。すぐに使います。

command
rails g controller tests
config/routes.rb
root "tests#show"
app/controller/tests_controller.rb
def show
  render plain: "This is app01."
end

app02も同じことを行います。app02は"This is app02."にしておいてくださいね。
当たり前ですがここを"This is app01."にすると後で切り替えの確認で困ります。

docker-compose.yml(本番)

ソース

SECRET_KEY_BASE: "your_key_base"の部分はrake secretで出力されたキーが入ります。

docker-compose.yml
version: '3'
services:
  webserver:
    image: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
  app01:
    image: "ruby:2.6"
    expose:
      - "3000"
    volumes:
      - ./testapp01:/app
    environment:
      VIRTUAL_HOST: localhost
      SECRET_KEY_BASE: "your_key_base"
    working_dir: /app
    command: >
      bash -c "gem install bundler:2.0.1 && bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"
  app02:
    image: "ruby:2.6"
    expose:
      - "3000"
    volumes:
      - ./testapp02:/app
    environment:
      VIRTUAL_HOST: localhost
      SECRET_KEY_BASE: "your_key_base"
    working_dir: /app
    command: >
      bash -c "gem install bundler:2.0.1 && bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"

networks:
  default:
    driver: bridge

コロンに注意

yaml文法のコロンとdocker-compose文法のコロンに要注意です。
yaml文法はコロンの後半角スペースが必要です。

    image: nginx

一方でdocker-compose文法のコロンは半角スペースを入れるとエラーになります。

    volumes:
      - ./testapp01:/app

expose vs ports

exposeとportsはどちらもポート解放に関する設定ですが、portsの方が色々できます。
こちらを参考にしました。
https://tkzo.jp/blog/difference-between-ports-and-expose-in-docker-compose/#ports_8211-3

今回の場合、appはproxyからしかアクセスしません。ですからexposeを使っています。
一応、ローカルで見る分には直接アクセスもできた方が便利かもしれません。
その場合はローカルホストからのみホストの13000番から直接アクセスできるようにして

    ports:
      - "127.0.0.1:13000:3000

としても良いかもしれません。
一方でproxyはホストOSの80番からproxyの80番へ接続させるためportsを使います。

    ports:
      - "80:80"

network

まず、dockerは初期状態で3つのネットワークを立ち上げています。
これは次のようにdockerコマンドで確認ができます。

command
docker network ls

NETWORK ID          NAME                   DRIVER              SCOPE
e0e37d582a18        bridge                 bridge              local
1ac914f6e912        host                   host                local
91508af4a972        none                   null                local

ネットワークにはID, 名前, ドライバがあります。
hostはホストOSを含むネットワーク、
bridgeはホストOSとは切り離されたコンテナ間を包括するネットワークです。

noneは単一コンテナのみのネットワークだそうです、(ネットワークとは一体?)
あくまで実装としてのネットワークってことでしょうが、ちょっと置いておきましょう。

今回はproxyだけがホストOSを通じてリクエストを受け取ることができれば良いので
bridgeを使っています。

なお、既存のネットワークを利用する場合には

networks:
  default:
    external:
      name: bridge

のようにしてもOKです。

nginx/default.conf

ヘッダーは理解せずにコピペしたので後で直すかもしれません。

nginx/default.conf
server {
    listen          80;

    server_name     localhost
    proxy_set_header    Host    $host;
    proxy_set_header    X-Real-IP    $remote_addr;
    proxy_set_header    X-Forwarded-Host      $host;
    proxy_set_header    X-Forwarded-Server    $host;
    proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;

    location / {
        proxy_pass    http://app01:3000;
#        proxy_pass    http://app02:3000;
    }

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

docker-composeで指定したapp01, app02が
ネットワーク内でホスト名として使えるようです。この関係性が便利ですね!

ちなみにですが、nginx/default.confが読み込まれるproxyの環境からみて
localhostはproxy自身になるようで、ホストOSを指すものではありません。
ですので

    location / {
        proxy_pass    http://localhost:3000;
    }

とか

    location / {
        proxy_pass    http://127.0.0.1:3000;
    }

とか

    location / {
        proxy_pass    http://0.0.0.0:3000;
    }

とか書いて、ホストOSの3000番ポートがappに繋がっていたとしても
appに繋がらず、かといってproxyも3000番は閉じているのでnginxがエラーになります。

コンテナ立ち上げ

ここもimageのみなのでbuild不要です。

command
docker-compose up

途中でDockerfileを書いた人だけ先にbuildしてupしてください。

動作確認

初期状態

ブラウザでlocalhostの80番へアクセスして、
This is app01.
が表示されていればOKです。

接続先切り替え

nginx/default.confのコメントアウトを切り替えます。
その後でproxyのシェルを1つ起動して、nginxをリロードします。

    location / {
#        proxy_pass    http://app01:3000;
        proxy_pass    http://app02:3000;
    }
docker exec -i -t <コンテナID> /bin/bash

nginx -s reload

この状態でlocalhostの80番へアクセスして、
This is app02.
が表示されていればOKです。

終わりに

これでローカルホストでの構築ができました。
ルートディレクトリのdocker-railsでgit initすれば丸ごとリモート環境へ移植できます。

アプリケーションをバージョンアップする際にはapp02側にリリースを行なって
nginxの接続先を変更すれば内容を変更することができます。
数日動かして問題がないかチェックしてOKであればapp01側にもリリースして
nginxの接続先を01に戻すようにすれば、良い感じのBlue-Green-Deploymentができます。

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4