新卒2年目の梅川です。部屋の中が暑すぎて、例年より大分前倒ししてクーラーを解禁しました。皆様も熱中症にはお気をつけください。
さて、今回は弊社の広告事業で用いている管理画面を様々な事情で新しく作り直すことになったため、その時の開発環境用のDockerとNginxを用いたロードバランサ(以下LB)設計についてお話ししようと思います。
全体の方針
作り直すことになった管理画面ですが、全てのページを一度に移行するわけではなく、完成したページから徐々に移行する方針をとりました。 そして、移行過程で必要になってくる適切なパスの振り分けを、LBで管理することにしました。
ステージング環境や本番環境では、AWSのALBでパスの振り分けを行うことにしました。しかし、開発環境ではそうもいかないため、Nginxを用いて振り分けを行うことにしました。 また、新管理画面、旧管理画面、両管理画面共通で扱うデータベースおよびLBをそれぞれDockerのコンテナとして定義し、Docker Composeでまとめることで開発環境をセットアップしやすいようにしました。
構成
改めて今回作成したい環境について詳しく見ていきます。 作成したい環境は以下の図のようになります。
管理画面は新旧どちらもRailsで作成します。今回の開発ではLBを介さずにアクセスしたいという要望が存在したため、80番ポートからLBにアクセスできる他、開発者が53000番・63000番ポートから、各管理画面へLBを介さず直接アクセスできるようにします。
では、ホームフォルダ上にcmc_project
というフォルダを作成し、そのフォルダ上で環境構築を行います(今回は説明に必要なファイルのみ記載)。
仮に古い管理画面のリポジトリ名をold_cmc_repo
、新しい管理画面のリポジトリ名をnew_cmc_repo
、docker-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:53000
・localhost: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コンテナへ振り分ける事とした場合です。
まず下記の部分で、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; } } }