そんな今日この頃でして、、、

コード書いたり映画みたり。努力は苦手だから「楽しいこと」を探していきたい。

Dockerを本番投入した話

このまま忘れてしまうのも勿体無いので記録に残しておこうかなと。

blue1st.hateblo.jp

技術系のネタは最近は別ブログの方でやってたけど、具体的なコードなんかは無いので今回はこっちで。

blue1st-tech.hateblo.jp

思い出した・思いついたら随時追記していく予定。


概要

フィーチャーフォンの時代からのサービスで、 無茶な拡張や教育不足や委託によるアレな開発状況によって、 コードも環境も技術的負債が山盛りの状態だった。

幸いにしてサーバを移設する機会に恵まれたので、 状況を改善するべく検討を進めた。

解決したかった問題

様々な部分において、環境ごとの差異が看過できない状態になっていた。

サーバによってインストールされているランタイムやモジュールのバージョンが異なることもあれば、 パスが異なることをコードにベタに記述してバージョン管理しないことにより対処している部分があったり、 そもそも環境によって同名のスクリプトの内容が書き換えられていたり。

バージョン管理ももはや正しく機能していなくて、必要なファイルだけ明示的に本番環境に上げるような綱渡りな運用になっていた。

おそらくリアルタイムでは、「急いで対応しなきゃいけないから仕方ない、後でちゃんと揃えよう」とかなんとか考えていたのかもしれないが、 往々にしてそういう意識は長続きしないもの。

結果として、どのサーバ環境での挙動が「正」なのかも分からない状態となっていた。

こうなってくると新たに環境を構築するのも困難だし、 リリース前の検証作業も不確実なものとなってしまう。

Dockerの導入

もちろんそれぞれ個別には対処法はあるにはあるんだけど、 それを全て長期的に完全に徹底していくことは困難なように思えた。

個人的には「気をつければ」とか「ちゃんと確認すれば」とかいった前提で成り立っているような運用というのは即ち無用にリソースを消費している状態なので、 今問題ないからといって看過すべきではなくすぐにでも改善すべきだと思っている。

そういったストレスを抱えながら開発運用していくよりも、 Dockerを用いた「可搬性ある形」を前提として使い方を習得してもらうほうが現実的で長期的なメリットもあると考え導入に踏み切ることにした。

(とはいえ、僕が使いたかったから理由を揃えて押し切ったという側面はちょっとある。 退社時期的にちょっと申し訳ない気もしている。)


さて、Docker本番導入における一つの大きな課題がマルチホストでのネットワークであるが、 幸いにして対象のサービスではアプリケーションサーバ間でやりとりする必要は無い。

なので、

  • あくまで個別のサーバでコンテナを個別に立ち上げるだけ
  • MySQLやMemcached、あるいはフロントのProxyとなるNginxなどは別サーバに普通に導入

という形をとった。

(インフラ担当の部署が別で、そういったところへの負担を減らしたかったという都合もある。)

Dockerイメージ

アプリケーションをコンテナ化するにしても、コードやモジュールを内部に持つか、外部からマウントするかといった選択の余地がある。

コンテナのサイズだったりビルドの速度だったりを考えるとマウントする形の方が合理的ではあるのだけれど、 今回主眼に置いているのが環境間の差異の吸収なので、 ビルド時に必要なモジュールなどはインストールしてしまい、コンテナ内にコードを含め動作のためのもの全てが収まる形をとった。


コードへの変更

Docker化するにあたって、いくつかコードに手を加えた。

考え方の基本は、いかにアプリケーションとホストの環境やその他のミドルウェアとの依存性を減らすかである。

オブジェクト指向において機能の疎結合化が求められるのと発想は同じで、これを進めていくことにより拡張や移植がしやすくなる。

環境依存の情報を環境変数から取得するように変更

Dockerイメージの形で持ち運んで様々な環境で起動する都合上、 例えばドメインみたいな環境に依存する情報は起動時に外部から渡すような記述に変更した。

元々も動作モードを示すproductiondevelopmentstagingなんかの環境変数の値を見て切り替えるような記述だったりはしたのだが、 後述するようにポート違いでいくらでも環境を量産できるという利点を考えると、 設定ファイル増量していくよりも具体的な値を渡すような形の方が利便性が高いように思えた。

パスの記述は固定化

一方でログファイルやキャッシュファイルの出力先などについては、逆に環境ごとに切り替えるような構造を無くした。

コンテナ内ではあくまで固定されたパスを用いるようにし、外面上は起動時にそれぞれの環境で適切な場所をマウントするようにした。

外部との接続もホスト名で固定化

MySQLやらMemcachedやらRedisと接続する必要があるわけだけど、 ここもパス同様にコンテナ内部では固定化してしまい、DNSもしくは起動時のオプションで指定させることで、 同一の記述を複数の環境で使いまわせるようにした。


開発環境について

自分でやる分には分かってるから良いのだけれど、開発メンバーは必ずしもDockerの習熟度が高いわけではない。

というわけで、なるべくそのあたりを意識しないで開発作業ができるように整備をした。

コードをマウントして起動

コードを変更するたびにイメージを再作成していたのでは効率が悪すぎる。

そこで、開発環境ではアプリケーションのパスの部分にホスト上のディレクトリをマウントし、 そのディレクトリ内にコードを置いて開発作業をしてもらうことにした。


Dockerが出始めの頃は、コンテナを軽量なVMと捉え、中に入って作業するようなモデルもみられた。(僕も一時期はそれをやってた)

だが、Dockerfileによる構成の管理や外部ツールとの連携を考えるならば、これは今では悪手だろう。

ツールの使い方を学習するための開発環境として使うのはアリな気はするが、 長期的にコードをいじるような現場ではDockerはDockerらしく使っておいた方が良いと思う。

Docker Composeの記述

諸々の起動オプションを毎回記述するのは流石にありえないので、Docker Composeを使用することにした。

blue1st.hateblo.jp

各人ごとのymlを記述してサーバに配布しておき、-fオプションでymlのパスを指定したコマンドのエリアスをbashrcに記載することで、 開発メンバー的には(例えばhoge=docker-compose -f target.ymlとして)

$ hoge up -d
$ hoge logs
$ hoge restart

みたいな形でコントロールできるようにした。

一時、実際に作業する(=ホスト側の)ファイルパスと動作上の(=コンテナ内での)ファイルパスが一致しないことによる混乱はあったものの、 それ以外は概ね問題なく受け入れてもらえたように思う。


一方で、モジュールの追加あたりではどうしてもビルドしてプルしてリスタートみたいな手順が必要なことを飲み込んで貰いづらく、 また実際に手順としても収まりが悪かった。

この辺は退職後に思いついたことだが、イメージのビルド時の作業と重複するが、 あえてアプリケーション起動時に不足しているモジュールをインストールするようなコマンドを追加することにより、 開発時の手間を減らせたのではないかと思った。

実際のデプロイの際にはその前段階のビルドでモジュールはインストールされているので、邪魔にはならないはず。

ビルドをCiにより自動化

Gitlabを用いていたのでGitlab Ciを導入し、 マスターブランチへのプッシュ時にイメージをビルドし、 ステージング環境を新たなイメージを用いて再起動するように設定した。

blue1st.hateblo.jp

※さすがに今回はDockerではなくサーバに直接導入

(本当は自動テストなんかも挟みたかったのだが、時間とかもろもろの都合で妥協)

これにより、開発メンバー的には

  1. 自分の開発環境で作業
  2. マージを他のメンバーに依頼
  3. 検証チームに依頼

という流れで、Dockerの存在を意識せずに作業できるようにした。


デプロイ

そんなわけで開発環境ではDocker Composeを用いていたので、 本番環境も同様に扱えるようにしたかったのだけど・・・

残念ながら当時はSwarmまわりが煩雑&CentOSと噛み合わない部分もあって断念。

いずれにせよDockerを用いる以上は一系統でgracefulな更新はできないので、 プロキシの切り替えを含めたAnsibleのPlaybookを記述し、 Blue-Greenデプロイの形をとることにした。

blue1st-tech.hateblo.jp

今はSwarmが組み込まれてもう少し扱い易くなってるらしいので、 もし機会があれば再挑戦してみたいところ。


当初はconsul-templateなんかも使って賢くやろうと思っていたんだけど、 期間が差し迫ってたり諸々の関係でリスクは減らそうと断念。

blue1st.hateblo.jp


本番環境でひっかかったところ

課題や対処なんか。

セキュリティとかを気にしないでよい検証環境まではサクッといけたんだけど、 実際に即した環境でやろうとすると思わぬところで引っかかった。

CentOS7系はどうにも相性が悪い

DockerがCentOS7に素で入るfirewalldではなくiptabelsを操作しようとしてコケてしまう問題が発生。

このへんはインフラさんの習熟度的な話もあり、iptablesを使う形に変更して対処。

たまにコンテナからのネットワークが繋がらなくなる→iptablesの再起動が問題だった

前まではちゃんと動いていたのに、なぜか突然コンテナ内からネットワークに繋がらなくなり、 Dockerプロセスを再起動すると解消するという状況が何度か起こった。

これはどうやらDockerプロセスが起動時にiptablesのフィルタルールを追記していたのを、 別件で何がしかをインストールしてiptablesを再起動した際にルールが消失してしまっていたのが原因なようだ。

知ってしまえば何のことは無いのだが、当時はすごく困惑した。

Blue-Greenの切り替え時にDBへのコネクション数が増大する

デプロイ時には一時的に平常時の2倍はコネクションが生じることになってしまうのだが、 アプリケーションサーバの台数とDBのスペック的に問題でこれが許容量を超えてしまう問題が発生した。

幸いにしてMySQLのタイムアウトは短めに設定してあったので、 AnsibleでのProxy操作のタスクを、 接続先を一括で切り替える形ではなく、 アプリケーションサーバを10等分して間にスリープを入れつつ順々に切り替えていき、 その間に古い方のコネクションがタイムアウトで勝手に切れてくれるように記述した。

Playbookの記述が結構カオスになってしまったし、デプロイで無駄に時間がかかるようになってしまった。

接続先を切り替えた後にユーザ側に出てない方のコンテナを殺して明示的にコネクションを切ればもう少し速やかに済ませられるかもしれないが、 それはそれでよりひどい記述になりそう。

単発のバッチ作業みたいなのをどうするか問題

本番環境のサーバに乗り込んでいって何かコードを書くなんていうのは保守上よろしくなくて、 そもそも今回のDocker化もそういったお行儀の良くない行為をなくそうという面があったんだけど・・・

とはいえ運用上そういった機会を全く無くせるわけではない。

このあたりは、スクリプト置き場的なディレクトリをマウントすることで対応している。


文章にしてしまうと非常にあっさりと順調に事が進んだかのような風に見えてしまうが、 もちろん移設にはそれなりに痛みを伴った。

コードのべた書きの箇所を探しては潰す作業は非常に面倒くさかったし、 モジュールのバージョンに依存した挙動の違いや記述方法の変更には毎回頭を悩ませた。

だがそれは移設においてのみならず、将来的な運用においても価値ある作業だったと思う。

投入してから最終出社日以降についても、 今のところは何か問題が生じたというような話も聞かないし、 何とか安定運用できているんじゃないかと思う。

次の会社では部署的にあんまりこの手の話題には関わらなそうだけど、 このジャンルの情報は興味あるとこだし追っていきたいなぁ。

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化

DevOps教科書

DevOps教科書

  • 作者: レン・バス,インゴ・ウェーバー,リーミン・チュー,長尾高弘
  • 出版社/メーカー: 日経BP社
  • 発売日: 2016/06/15
  • メディア: 単行本
  • この商品を含むブログを見る