ls /asapon/blog

基本tech、時々多趣味

Composeの起動順番をアプリケーション含め制御する

はじめに

Selenium環境をDocker内で完結させるの続きっぽい感じになっています。ですが、本記事のテーマである「起動順番の制御」は、上の記事を読まなくても理解できるよう心掛けて書きました。
例えば「続きっぽい」と感じられる要素は、実装時に書いたアプリケーション独自のものに留めています(rakeタスクやコンテナのサービスといったもの)。また、不要と思ったコード・コマンドは省略しました。

忙しい人向け

  • 起動順番を制御するには、ラッパースクリプトを用意すると良い
  • 方法は3種類ある
  • 特に大きな差はないのでお好みで

どんな問題が起こったのか

docker-compose up で立ち上げると、Selenium::WebDriverクライアントでエラーが発生しました。原因はSelenium::WebDriverクライアントを作るときに、Seleniumコンテナ内のChromeが立ち上がっていないことだった。
対策として、Chromeが立ち上がったあとにクライアントが作られるよう、依存関係を制御する必要があると分かった。

depends_on では駄目なのか

こういった依存関係の制御は、 depends_onlinks などのコマンドで制御できると思っていました。しかし実際は、サービスの起動順番しか面倒をみないようです。
つまり、コンテナ内のアプリケーションが実行可能かどうかまでは面倒を見てくれないということになります。
この仕様理由は、Compose の起動順番を制御で説明されています。以下は引用。

たとえば、データベースの準備が整うまで待つのであれば、そのことが分散システム全体に対する大きな問題になり得ます。プロダクションでは、データベースは利用不可能になったり、あるいは別のホストに移動したりする場合があるでしょう。 アプリケーションは、障害発生に対して復旧する必要があるためです。 データベースに対する接続が失敗したら、アプリケーションは再接続を試みるように扱わなくてはいけません。アプリケーションは再接続を試みるため、データベースへの接続を定期的に行う必要があるでしょう。

解決策

公式では

アプリケーションのコード上で解決する

ことが望ましいと説明しています。ただ、この処理を用意するのが面倒な場合は、「実行前にアプリケーションが立ち上がるのを待ってくれるラッパー」を用いることで対処できます。
ラッパーの準備は大きく二つに分かれます。

  • ツールを使う方法
  • 独自のラッパーを使う方法

ツールを使う場合

ラッパー用のツールは主に二つあります。

dockerize

dockerizeは、コンテナ内のアプリケーションの動作に関わる便利機能を提供してくれるツールです。その中でも今回は、アプリケーションがスタートするまで待機する機能を利用します。この機能では、ホスト・ポートだけでなく、HTTP(S)といった上位プロトコルも使ってヘルスチェックすることが可能です。

実装例

実装例は以下の流れになっています。

  1. dockerize をインストールする
  2. dockerize-wait オプションで実行する
    1. <protocol>://<host>:<port> の順番で指定する
  3. ヘルスチェックが通ったあと、引数のコマンドを指定する

Dockerfile

FROM ruby:2.7.0-alpine

# Alpine用のdockerizeをインストール
RUN apk add --no-cache openssl
ARG DOCKERIZE_VERSION="v0.6.1"
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
    && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
    && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz

docker-compose.yml

services:
  ruby:
    build: .
   command: ['dockerize', '-wait', 'tcp://chrome:4444', 'bundle', 'exec', 'rake', 'reserve']
    volumes:
      - .:/var/app
    depends_on:
      - chrome
  chrome:
    image: "selenium/standalone-chrome-debug:3.141.59"
    ports:
      - "4444:4444"
      - "5900:5900"
    volumes:
      - /dev/shm:/dev/shm

wait-for-it

wait-for-itはピュアなbashスクリプトで書かれています。そのため、依存関係を極力廃した作りになっています。また機能もシンプルになっており、扱いやすいものになっているのが特徴です。こちらのプロトコルtcpで固定です。

実装例

Dokerizeと違い、wait-for-itのREADMEには、イメージへの含め方が書いてありませんでした。そのため以下にあげる実装方法は、私が考え調べたものになります。

今回は後者の方法を使います。

  1. https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh を直接イメージにADDする
  2. wait-for-it.sh を実行する
  3. ヘルスチェックが通ったあと、引数のコマンドを指定する

Dockerfile

FROM ruby:2.7.0-alpine

ARG WORK_DIR="/var/app/"
WORKDIR ${WORK_DIR}

# alpineはbashのインストールが必要
RUN apk add --update --no-cache bash

# ルートディレクトリにスクリプトを置く
# ${WORK_DIR} 以下に置くと、ローカルがマウントするときファイルが消えてしまうため
ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh

docker-compose.yml

services:
  ruby:
    build: .
   command: ['/wait-for-it.sh', 'chrome:4444', '--', 'bundle', 'exec', 'rake', 'reserve']
    volumes:
      - .:/var/app
    depends_on:
      - chrome
  chrome:
    image: "selenium/standalone-chrome-debug:3.141.59"
    ports:
      - "4444:4444"
      - "5900:5900"
    volumes:
      - /dev/shm:/dev/shm

独自のラッパーを使う場合

ラッパースクリプトは自作することもできます。シェルスクリプトで、アプリケーションのヘルスチェックをするのが主です。
ヘルスチェックは、CLIでクライアントが呼び出せる場合

until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "postgres" -c '\q'; do

のように、シェルスクリプト内で直接コマンドを実行しても良いです(実装は公式ドキュメント参照)。
今回はSelenium::WebDriverクライアントを作ることができるか調べたいので、ヘルスチェック用のrakeタスクを用意し実行しています。

実装例

実装例は以下の流れになっています。

  1. ラッパースクリプトである wait-for-chrome.sh を実行する
  2. ヘルスチェック用のrakeタスクを実行し、エラーが発生したら数秒間スリープする
  3. ヘルスチェックが通ったら、引数で受けたコマンドを exec する

docker-compose.yml

services:
  ruby:
    build: .
    command: ['./wait-for-chrome.sh', 'bundle', 'exec', 'rake', 'reserve']
    volumes:
      - .:/var/app
    depends_on:
      - chrome
  chrome:
    image: "selenium/standalone-chrome-debug:3.141.59"
    ports:
      - "4444:4444"
      - "5900:5900"
    volumes:
      - /dev/shm:/dev/shm

wait-for-chrome.sh

# シェバンはコンテナのベースイメージに合わせる
# ex. alpineなら #!/bin/ash or #!/bin/sh

#!/bin/bash

set -e

cmd="$@"

until bundle exec rake health_check; do
  >&2 echo "chrome is unavailable - sleeping"
  sleep 3
done

>&2 echo "chrome is up - executing command"
exec $cmd

おわりに

他にも色々ラッパーはありそうですが、とりあえずここのやり方さえ抑えておけば、起動順番の制御は問題なさそうです。

参考

Selenium環境をDocker内で完結させる

はじめに

とある予約の申し込み作業が面倒になったので、ブラウザ自動操作のためSeleniumを採用しました。色々ローカルに入れるのもアレなので、Docker環境で完結させています。

構成

イメージ図は以下。

f:id:asaponseiten:20200113101912p:plain

docker-compose.yml

以下のようになります。

version: "3"

services:
  ruby:
    build: .
    volumes:
      - .:/var/app
    depends_on:
      - chrome
  chrome:
    image: "selenium/standalone-chrome-debug:latest
    ports:
      - "4444:4444"
      - "5900:5900"
    volumes:
      - /dev/shm:/dev/shm

Seleniumコンテナ

Standalone か Grid か

SeleniumHQ/docker-selenium公式イメージを使います。
この公式イメージは大きく、 Standalone タイプと Grid タイプに分かれます。テストのときに、複数のOS・ブラウザ環境が欲しいかどうかで決まります。
今回は、ただのバッチ処理(予約のための操作自動化)の用途でSeleniumを使うため、複数ブラウザ環境は必要ありません。 Standalone タイプのイメージを使います。

debug か否か

Standalone タイプでも更に、 debug か否か、タイプ分けすることができます。
debug とついているイメージは、VNCサーバーを起動することができます。VNCを使うことで、実際にブラウザが立ち上がって動作が完結するまで、処理の動きを目で追うことができます。
今回はselenium/standalone-chrome-debugを使いましたが、ここはお好みで良いと思います。
ちなみに、VNCサーバへのアクセスは

  1. vnc://localhost:5900 にアクセスする
  2. パスワード secret を打つ

で大丈夫です。

公式イメージの使い方メモ

docker rundocker-compose up といった起動コマンド時に、/dev/shm:/dev/shm とマウントする必要があります。

docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:3.141.59-yttrium

もしくは --shm-size=2g と、ホストのメモリ領域を使うために、フラグ立てをする必要があります。

docker run -d -p 4444:4444 --shm-size=2g selenium/standalone-chrome:3.141.59-yttrium

Rubyコンテナ

クライアントを実行できる環境があれば、問題ないです。

# Gemfile
gem 'selenium-webdriver'

selenium-webdriver gem を使う

参考リンクは以下になります。

またSeleniumコンテナにアクセスできるよう、localhost ではなく、 docker-compose.yml で指定したホスト名を使います。

url = 'http://chrome:4444/wd/hub'
@driver = Selenium::WebDriver.for(:remote,  url: url, desired_capabilities: :chrome)

wd/hub は、Selenium Hubコンソールへのリンクです。

疎通確認する

docker-compose run --rm ruby irb として確認(不必要な返り値は省略しています)。

irb(main):001:0> require 'selenium-webdriver'
irb(main):002:0> driver = Selenium::WebDriver.for(:remote,  url: 'http://chrome:4444/wd/hub', desired_capabilities: :chrome)
irb(main):003:0> driver.get('https://www.google.com/?hl=ja')
irb(main):004:0> driver.title
=> "Google"

おわりに

Seleniumはテストで使うことが主ですが、ブラウザ操作の自動化は、結構捗る範囲が広いと思います。これで予約が楽になれば良いなぁ。

新年あけおめ縦走してきました [後編]

つづきです。

前編はこちら

景信山〜小仏城山

f:id:asaponseiten:20200103143740j:plain f:id:asaponseiten:20200103143941j:plain

絶景を見ながら景信山をあとにします。

f:id:asaponseiten:20200103143820j:plain

台風の影響で小仏ルートが止まっています。登山前に調べた限りでも、復旧の見込みは立っていない模様。
陣場山〜高尾山は、至るところにエスケープがあるため挑戦しやすいのですが、バス停までの道が閉ざされているので、若干難易度があがっている気がします。

f:id:asaponseiten:20200103143857j:plain

通行止めルート。
ここから稜線特有のアップダウンが出てきます。足取りが遅くなっている方も増えてきたので、「次でちょっと休もう」と考え始めたときでした。

小仏城山

f:id:asaponseiten:20200103144016j:plain

小仏城山に着。高尾山から来た観光客も合わさり、なかなか賑やかになっていました。格好がジーパンなので露骨に分かりますね。 休憩がてら小腹が空いていたので、おでんを買おうと決めていました!

f:id:asaponseiten:20200103160136j:plain

ただのおでんでも、疲労と景色が最高のスパイスになって美味しい...

小仏城山〜高尾山

f:id:asaponseiten:20200103163523j:plain

こういう稜線良いですよね。整備されすぎてる感じもありますが。
丹沢の縦走もしたいなぁ。。

f:id:asaponseiten:20200103162953j:plain

展望台へ。丹沢山地がかっこいい!

f:id:asaponseiten:20200103162913j:plain

そしてとうとう五号路に到着!
この前後で長い階段があるので、気合入れて登りましょう。ここを乗り越えたらゴールはもう目の前です!

高尾山

f:id:asaponseiten:20200103163443j:plain

f:id:asaponseiten:20200103163705j:plain

高尾山に着きました。
頂上の混み具合は土日並だったので、あまり正月三が日という感じではありませんでした。

f:id:asaponseiten:20200103172055j:plain

休憩も取りましたが、だいぶ余裕を持ってゴールできた気がします。
ベースタイムは5時間でした。

f:id:asaponseiten:20200103163351j:plain

f:id:asaponseiten:20200103163606j:plain

f:id:asaponseiten:20200103163742j:plain

薬王院の混み具合を横目に見ながらお団子休憩。

帰りは一号路から、裏高尾ルートを通って、JR高尾駅に向かいました。

おわりに

新年早々、目標だった陣場山〜高尾山の縦走ができて最高です!
今年の夏は日本アルプスも行きたいと思っているので、身体鍛えて頑張ります!