Nginxを用いたDocker内で使えるロードバランサ設計

新卒2年目の梅川です。部屋の中が暑すぎて、例年より大分前倒ししてクーラーを解禁しました。皆様も熱中症にはお気をつけください。

さて、今回は弊社の広告事業で用いている管理画面を様々な事情で新しく作り直すことになったため、その時の開発環境用のDockerとNginxを用いたロードバランサ(以下LB)設計についてお話ししようと思います。

全体の方針

作り直すことになった管理画面ですが、全てのページを一度に移行するわけではなく、完成したページから徐々に移行する方針をとりました。 そして、移行過程で必要になってくる適切なパスの振り分けを、LBで管理することにしました。

ステージング環境や本番環境では、AWSのALBでパスの振り分けを行うことにしました。しかし、開発環境ではそうもいかないため、Nginxを用いて振り分けを行うことにしました。 また、新管理画面、旧管理画面、両管理画面共通で扱うデータベースおよびLBをそれぞれDockerのコンテナとして定義し、Docker Composeでまとめることで開発環境をセットアップしやすいようにしました。

構成

改めて今回作成したい環境について詳しく見ていきます。 作成したい環境は以下の図のようになります。

f:id:open8tech:20190520172118p:plain
構成図

管理画面は新旧どちらもRailsで作成します。今回の開発ではLBを介さずにアクセスしたいという要望が存在したため、80番ポートからLBにアクセスできる他、開発者が53000番・63000番ポートから、各管理画面へLBを介さず直接アクセスできるようにします。 では、ホームフォルダ上にcmc_projectというフォルダを作成し、そのフォルダ上で環境構築を行います(今回は説明に必要なファイルのみ記載)。 仮に古い管理画面のリポジトリ名をold_cmc_repo、新しい管理画面のリポジトリ名をnew_cmc_repodocker-compose.yml等を置くリポジトリ名をcmc_dockerとした時下記のようなフォルダ構成をとります。

cmc_project
├─ cmc_docker
│  ├─ docker-compose.yml
│  └─ load_balancer
│     ├─ Dockerfile
│     └─ nginx.conf
├─ old_cmc_repo
│  └─ Dockerfile
└─ new_cmc_repo
   └─ Dockerfile

データベースは作成済みのイメージを使いますが、それ以外はDockerfileを使って環境構築を行います。

それでは、セットアップ用のcmc_docker/docker-compose.ymlを見ていきましょう。

version: '3'
services:
  load_balancer:
    build: ./load_balancer/
    ports:
      - '80:80'
  old_cmc:
    build: ../old_cmc_repo/
    ports:
      - '53000:80'
    volumes:
      - ../old_cmc_repo/:/root/projects/old_cmc_repo
      - /root/bundle
      - /root/projects/old_cmc_repo/node_modules
  new_cmc:
    build: ../new_cmc_repo/
    ports:
      - '63000:80'
    volumes:
      - ../new_prod_repo/:/root/projects/new_cmc_repo
      - /root/bundle
  db:
    image: mysql:5.6.34
    command: "mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci"
    working_dir: /root/dump
    environment:
      MYSQL_ROOT_PASSWORD: 'root'
      TZ: Asia/Tokyo
    ports:
      - '3306'
    volumes:
      - mysql-data:/var/lib/mysql

volumes:
  mysql-data:
    driver: local

load_balancerのportsの部分を見ていただくと80:80という記述があります。これにより、ユーザーはブラウザ等からlocalhost:80にアクセスすることで、load_balancerコンテナのlocalhost:80にアクセスできるようになります。old_cmcコンテナ・new_cmcコンテナも同様にユーザーはlocalhost:53000localhost:63000にアクセスすることで各コンテナ内のlocalhost:80にアクセスできます。

LB用コンテナの構成

Docker Composeの準備が整ったところでload_balancerコンテナについて見ていきます。 まずは、Dockerfileから見ていきましょう。

#cmc_docker/load_balancer/Dockerfile
#最新の安定板を取得する
FROM nginx:1.15.12
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Nginxのイメージを取ってきて、/etc/nginx/nginx.confを置いた後、Nginxを起動しているのみの簡素なDockerfileです。 次に、nginx.confでどのようにパスを振り分けているのかを見ていきます(nginx.confの全容は最後に付録として載せています)。 今回は以下の図のように、/path_to_a/path_to_bのみnew_cmcコンテナへ振り分ける事とした場合です。

f:id:open8tech:20190520172109p:plain
振り分け図

まず下記の部分で、docker-compose.ymlで定義したコンテナ名をサーバーとして指定をしています。

  upstream old_cmc_stream {
    server old_cmc;
  }
  upstream new_cmc_stream {
    server new_cmc;
  }

コンテナ間はhttp://(コンテナ名):(ポート番号)で相互にアクセスできるようになっているため、このような書き方になります。 あとは完成したページはnew_cmcコンテナへ、そうでないものはold_cmcコンテナへ振り分けていきます。 単純な振り分けであれば、下記のようにlocationで指定することで行うことができます。

    location /path_to_a {
      proxy_pass http://new_cmc_stream/path_to_a;
    }
    location /path_to_b {
      proxy_pass http://new_cmc_stream/path_to_b;
    }
    location / {
      proxy_pass http://old_cmc_stream;
    }

しかし、Railsアプリケーションであれば/assetsも適切に振り分ける必要があります。 様々な解決法はありますが、お手軽にできそうだったためリファラを使った振り分けを今回は採用しました。 下記の記述を加えることで、リファラ/path_to_a/path_to_bであればvalid_referersとしてnew_cmcコンテナへ、それ以外はold_cmcコンテナへ振り分けるようになります。

    location /assets {
      valid_referers ~localhost/path_to_a*;
      valid_referers ~localhost/path_to_b*;
      if ($invalid_referer != "1") {
        proxy_pass http://new_cmc_stream;
      }
      if ($invalid_referer = "1") {
        proxy_pass http://old_cmc_stream;
      }
    }

これでLBは完成です。他のページも移行したい時には設定をnginx.confに加筆していくことで、スムーズな切り替えができます。

まとめ

今回はWebシステムを移行したい時の、DockerとNginxを用いた環境構築の一例を紹介させていただきました。 Nginxをきちんと触ったのは初めてで、実は構築にかなり苦戦したのですが、時間をかけて取り組んだおかげで理解がとても進みました。 また、Docker Composeで環境構築のために必要なものを全てひとまとめにしたおかげで、環境構築がとても楽になりました。 今回の記事が何らかの参考になれば幸いです。

付録

#cmc_docker/load_balancer/nginx.conf
user www-data;
worker_processes 4;
pid /run/nginx.pid;

events {
  worker_connections 768;
  # multi_accept on;
}
http {
  proxy_redirect off;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Host $host;
  proxy_set_header X-Forwarded-Server $host;
  proxy_set_header X-Real-IP $remote_addr;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;

  default_type application/octet-stream;

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

  gzip on;
  gzip_disable "msie6";

  proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=default:8m max_size=1000m inactive=24h;
  proxy_temp_path /var/cache/nginx/tmp;
  log_format main '$remote_addr - $remote_user [$time_local] "$request" '
  '$status $body_bytes_sent "$http_referer" '
  '"$http_user_agent" "$invalid_referer"';

  upstream old_cmc_stream {
    server old_cmc;
  }
  upstream new_cmc_stream {
    server new_cmc;
  }

  server {
    listen 80;
    server_name localhost;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    location /path_to_a {
      proxy_pass http://new_cmc_stream/path_to_a;
    }
    location /path_to_b {
      proxy_pass http://new_cmc_stream/path_to_b;
    }
    location /assets {
      valid_referers ~localhost/path_to_a*;
      valid_referers ~localhost/path_to_b*;
      if ($invalid_referer != "1") {
        proxy_pass http://new_cmc_stream;
      }
      if ($invalid_referer = "1") {
        proxy_pass http://old_cmc_stream;
      }
    }
    location / {
      proxy_pass http://old_cmc_stream;
    }
  }
}