FANCOMI Ad-Tech Blog

株式会社ファンコミュニケーションズ nend・新規事業のエンジニア・技術ブログ

Docker冪等性の日

まえふり

久し振りに連絡を取り合った天文観測クラスタの先輩が講習会に講師で呼ばれるレベルになっていたのでfacebookを覗いたらラヴジョイ彗星というのを知ったy_yamadaです。

ラヴジョイ彗星はテリー・ラヴジョイさんが発見した5個の彗星で、その中で2014年8月17日に発見された「C/2014 Q2」が昨年末から今年年初にかけて現在手軽に観測できているようです。最近の発見にしては地球に最接近した時には等級4という比較的明るいので首都圏在住の方でも郊外へ出かければ肉眼で観測できるかもしれません。宇宙はロマンですね。

ところで、天体観測クラスタで話題なのは彗星ですがIT業界でここ最近話題といえば冪等性でしょう。 何度試しても同じ結果が得られる冪等性はテストやデプロイの再現性にとても役立ちます。 そして冪等性で注目が集まっているのがDockerです。視点の問題ですが厳密に言えば冪等ではないなどなど議論はあるようです。

Dockerがどういうものか極論すると、XenやVirtualBoxのような仮想化された環境を提供するミドルウェアです。 もう少し詳細な話として、Dockerはコンテナという単位で仮想化(のようなもの)を行います。XenやVirtualBoxのように仮想化の対象をハードウェア丸ごとエミュレートしてそこにOSをインストールする訳ではなくコンテナのプロセスを他のコンテナから排他、隠蔽します。ハードウェアをエミュレートする訳ではなく実際はただのプロセスだというこの特徴により他の仮想化/エミュレーターとは違って実行の無駄が無く軽く速く動作します。この仕組みはFreeBSDに昔からあるJailという仕組みによく似ています。 また、DockerではUnionFS(AUFSなど)のファイルシステムを採用しており、これによって親ファイルシステムにレイヤーを提供して親のファイルシステムにあるファイルはそのまま利用できてなおかつ新規のファイル作成は親や他のコンテナからは独立して存在することができます。もちろん、親の特定のディレクトリをマウントして通常通り上書きすることもできます。

 

サービス構成

という訳でDockerを使ってサービスを作ってみます。

・サービス名
ピーチボット twitterでピーチクパーチクしゃべっているのをslackへさらにしゃべるということでピーチク(peachiku)としゃべる(speech)の語感が良かったのでpeachbotにしました。

・内容
twitterから特定のツイートを取得してslackへ投稿する。

・構成
AWS EC2 CentOS 6.6 Docker(PostgreSQLコンテナ + Rubyコンテナ) twitter API slack Incoming WebHooks

 

前提

土台のサーバーはこうです。

# cat /etc/redhat-release
CentOS release 6.6 (Final)

 

Dockerの準備

Dockerを利用するために土台のサーバーに色々用意します。

 

EPELのリポジトリーを登録する

# yum install wget -y
# wget http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
# rpm -Uvh remi-release-6.rpm epel-release-6-8.noarch.rpm
# sed -i -e "s/enabled=1/enabled=0/" /etc/yum.repos.d/epel.repo

 

Dockerをインストールする

Dockerをインストールします。
土台のサーバーがCentOS 6.x系なのでrpm名がdocker-ioですが、CentOS 7.xではrpm名がdockerだった気がします。

5行目でコンテナの起動、OSの名前&バージョンを表示しています。
6行目で次の項目以降で必要なファイルを置く場所を作成しています。

# yum install docker-io -y --enablerepo=epel
# chkconfig docker on
# chkconfig --list | grep docker
# service docker start
# docker run centos cat /etc/redhat-release
# mkdir ~/docker-files

 

DBコンテナ

今回はPostgreSQLを提供する「DBコンテナ」、Rubyとその他を提供する「rubyコンテナ」を作ります。 まずはDBコンテナを作成します。

Dockerのコンテナイメージをすべて自前で作成することもできますが今回提供するPostgreSQLは特殊な設定はないのでCentOSが提供している設定をgithubから取得して使っています。

5~18行:twitterからどこまでのツイートを取得したのかの保存するためのテーブルを準備しています。
19~21行:上記のSQLを実行する手順を追加しています。
22~24行:コンテナイメージの作成、コンテナの実行、テーブルの作成を行っています。

# mkdir ~/docker-files/postgres
# cd ~/docker-files/postgres
# git clone https://github.com/CentOS/CentOS-Dockerfiles.git
# cd ~/docker-files/CentOS-Dockerfiles/postgres/centos6
# cat << _EOT_ > twi_configs.sql
CREATE TABLE twi_configs (
    id SERIAL PRIMARY KEY,
    config_key text,
    config_value text,
    created_at timestamp with time zone DEFAULT now()
);
INSERT INTO twi_configs (config_key,config_value) VALUES ('since_id','553366914391478274');
_EOT_
# cat << _EOT_ > create_table.sh
#!/bin/sh

su --login - postgres --command "psql  dockerdb -f /twi_configs.sql"
_EOT_
# sed -i -e '/^ADD \.\/postgres_user/a ADD ./create_table.sh /create_table.sh' Dockerfile
# sed -i -e '/^ADD \.\/postgres_user/a ADD ./twi_configs.sql /twi_configs.sql' Dockerfile
# sed -i -e '/^RUN chmod +x \/postgres_user.sh/a RUN chmod +x /create_table.sh' Dockerfile
# docker build -rm -t y_yamada/postgres:centos6 .
# docker run -d -p 15432:5432 --name postgres y_yamada/postgres:centos6
# docker exec postgres /create_table.sh

 

rubyコンテナ

次はrubyコンテナイメージを作成します。
細かく言うと以下の環境を構築しています。
・ruby 2.1系のインストール
・rsyslogのインストール
・crondのインストール
・cronの登録

ベースのコンテナイメージはtcnksmさんのものを利用させていただきました。ありがとうございます。

 

ベースのイメージを取得

rubyコンテナ用のDockerfileを書く前にベースのコンテナイメージを先に取得しておきます。 コンテナイメージをビルドする時に自動で取得してはくれますが、サイズ/履歴の多いコンテナイメージは取得に時間がかかることが多いので先に取得しておきます。

# docker pull tcnksm/centos-ruby:2.1

 

Dockerfile定義

rubyコンテナ用の定義ファイルです。

内容はコメントの通りで難しくないと思います。 ある意味一番重要なのはsupervisordを使わずにベタで動かすところでしょうか。 そのentrypoint.shについての項目でも記載しますがサービスの複数実行はそれでいいかなあと最近は思っています。

# cat << _EOT_ > Dockerfile
FROM tcnksm/centos-ruby:2.1

# ユーザー追加
RUN useradd peachbot
RUN runuser -l peachbot -c 'mkdir -p /home/peachbot/bin'

# 設定ファイルの一時保管場所を作成
RUN runuser -l peachbot -c 'mkdir -p /home/peachbot/tmp'

# ruby env
ADD ruby.sh /etc/profile.d/

# syslog
RUN yum install rsyslog -y
RUN chkconfig rsyslog on
#RUN service rsyslog start

# バンドルをインストール
RUN     mkdir -p /home/peachbot/app
ADD     Gemfile /home/peachbot/app/
WORKDIR /home/peachbot/app/
RUN     bundle install
RUN     chown -R peachbot:peachbot /home/peachbot/app

# cron
RUN yum install cronie-noanacron -y
RUN yum remove cronie-anacron -y
RUN sed -i -e 's/^session    required   pam_loginuid.so/#session    required   pam_loginuid.so/' /etc/pam.d/crond
RUN chkconfig crond on
ADD crontab.peachbot /home/peachbot/tmp/
RUN runuser -l peachbot -c 'crontab /home/peachbot/tmp/crontab.peachbot'
#RUN service crond start

#
ADD entrypoint.sh /home/peachbot/bin/entrypoint.sh
RUN chmod 755 /home/peachbot/bin/*.sh

# 権限をpeachbotへ変更
RUN chown -R peachbot:peachbot /home/peachbot/bin
RUN chmod 755 /home/peachbot/bin/*.sh
RUN chown -R peachbot:peachbot /home/peachbot/tmp

# 起動
#CMD /bin/bash
#CMD ["/usr/bin/supervisord"]
ENTRYPOINT ["/home/peachbot/bin/entrypoint.sh"]
_EOT_

 

ruby.shの中身

# cat << '_EOT_' > ruby.sh
export GEM_HOME=/usr/local/bundle
export PATH=/usr/local/bundle/bin:$PATH
_EOT_

 

取ってくるgem

# cat << '_EOT_' > Gemfile
source 'https://rubygems.org'
gem 'pg'
gem 'twitter'
gem 'json'
gem 'fluent-logger'
_EOT_
# cat << '_EOT_' > Gemfile.lock
_EOT_

 

crontab.peachbotの中身

10分ごとにツイートを取得します。 最近のtwitterはAPI上限を超えるとすぐにAPI利用停止されるので、ごめんなさいメールを送るのが嫌な方は大人のマナーに従って抑え目に使いましょう。

パスワードをベタに書くのは好ましくないですが今回は作り込む時間が無かったのでここはそのまま書いています。ruby.shと同じ方法でできるはずです。

# cat << '_EOT_' > crontab.peachbot
HOME=/home/peachbot
PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/peachbot/bin
SHELL=/bin/bash
DB_ENV_POSTGRES_USER=dockeruser
GEM_HOME=/usr/local/bundle
DB_ENV_POSTGRES_DB=dockerdb
DB_PORT_5432_TCP_ADDR=172.17.0.136
DB_ENV_POSTGRES_PASSWORD=password
DB_PORT_5432_TCP_PORT=5432

*/10 * * * * cd /home/peachbot/bin ; /usr/local/bin/ruby /home/peachbot/bin/twi_crawler.rb 1>> /home/peachbot/bin/log 2>> /home/peachbot/bin/err
_EOT_

 

実アプリ本体

ツイートの取得、slackへの投稿を行う処理の本体です。 今回はわれらがカリスマtwitter-erであるヤナティのツイートを取得して自分のチームのslackチャンネルへ投稿しています。これでヤナティのツイートをしっかり追えますね。

ツイート取得、slack投稿自体は簡単な処理なので公式やもろもろのサイトのものと比較してご理解いただけると思います。

注目点は取得したツイートの場所の取得、更新です。DBコンテナを使うのもこのためです。 twitterでは検索APIも利用制限があります。同じ条件の検索を何度も行うと利用回数制限内であってもAPI利用が停止されてしまいます。なので、取得したツイートのIDを保存し次回にその場所からの検索で利用しています。 処理は67,86-87行目です。

# cat << '_EOT_' > twi_crawl.rb
#!/usr/local/bin/ruby

require 'rubygems'
require 'twitter'
require 'pg'
require 'json'
require 'time'
require 'net/http'
require 'uri'
require 'json'

def slack_push(text)
  data = {
    'username' => 'peachbot',
    'text' => text
  }
  # https://じぶんのちーむ.slack.com/services/new
  request_url = 'Incoming WebHooksするためのURL'
  uri = URI.parse(request_url)
  http = Net::HTTP.post_form(uri, {"payload" => data.to_json})
end

def dbg_out(msg)
    print "#{msg}\n"
end

TW_CONSUMER_KEY        = "ひみつのこんしゅーまーきー"
TW_CONSUMER_SECRET     = "ひみつのこんしゅーまーしーくれっと"
TW_ACCESS_TOKEN        = "ひみつのあくせすとーくん"
TW_ACCESS_TOKEN_SECRET = "ひみつのあくせすとーくんしーくれっと"

# 認証
twClient = Twitter::REST::Client.new do |config|
  config.consumer_key        = TW_CONSUMER_KEY
  config.consumer_secret     = TW_CONSUMER_SECRET
  config.access_token        = TW_ACCESS_TOKEN
  config.access_token_secret = TW_ACCESS_TOKEN_SECRET
end

# 検索したい条件
word = 'from:ankeiy' # 検索したいワード

connection = PG::connect(:host => ENV['DB_PORT_5432_TCP_ADDR'], :port => ENV['DB_PORT_5432_TCP_PORT'],
                         :user => ENV['DB_ENV_POSTGRES_USER'], :password => ENV['DB_ENV_POSTGRES_PASSWORD'], :dbname => ENV['DB_ENV_POSTGRES_DB'])

begin

  # プレースホルダーを準備する
  get_twi_configs = <<-EOS
    SELECT * FROM twi_configs
    WHERE config_key=$1
  EOS
  connection.prepare('GET_TWI_CONFIG', get_twi_configs)

  set_twi_configs = < 'recent', :since_id => since_id)

# 取得ツイートを繰り返し処理する
results.attrs[:statuses].reverse_each do |tweet|

  status_id   = tweet[:id]
  screen_name = tweet[:user][:screen_name]
  user_name   = tweet[:user][:name]
  tweet_text  = tweet[:text]

  msg = "\nhttps://twitter.com/#{screen_name}/status/#{status_id}\n@#{screen_name} #{user_name}:\n#{tweet_text}\n"
  slack_push("#{msg}")

  since_id = tweet[:id]
  connection.exec_prepared('SET_TWI_CONFIG', ['since_id', since_id])

end

dbg_out("since_id: #{since_id}")

ensure
  connection.finish
end

dbg_out('finished @ twi_crawler.rb')
exit(0)

 

entrypoint.shの中身

Dockerfileの定義でも少し触れた内容を掘り下げます。 Dockerではコンテナの起動時に1つのプロセスしか実行できません。ですがプロセス1つのためにコンテナを複数用意するのはしんどいのでもろもろ詰め込んだコンテナを作成するのが多いと思います。 そういう要望のためにマルチプロセス起動のためにpythonアプリであるsupervisordがよく利用されていると思います。 今回もsupervisordを利用する予定だったのですが、以下のブログを見つけてあまりにもお手軽だったのでこういう記載になりました。 supervisordは必要か?

7行目:docker runで渡された変数を一般ユーザーでも使えるように/etc/profile.dに初期化スクリプトで保存しています。

# cat << '_EOT_' > entrypoint.sh
#!/bin/sh

service rsyslog start
service crond start

echo -e "export DB_ENV_POSTGRES_USER=$DB_ENV_POSTGRES_USER\nexport DB_ENV_POSTGRES_PASSWORD=$DB_ENV_POSTGRES_PASSWORD\nexport DB_PORT_5432_TCP_ADDR=$DB_PORT_5432_TCP_ADDR\nexport DB_PORT_5432_TCP_PORT=$DB_PORT_5432_TCP_PORT\nexport DB_ENV_POSTGRES_DB=$DB_ENV_POSTGRES_DB" > /etc/profile.d/db-env.sh

while true
do
    sleep 10
done
_EOT_

 

ビルド

長々と書いてきましたがやっとビルドです。 残りあと少し!

# docker build --rm -t y_yamada/ruby:2.1 .

 

実行

できあがったコンテナイメージから実行します。 -eで環境変数でDB情報を渡しています。

# docker run -e "DB_ENV_POSTGRES_USER=dockeruser" -e DB_ENV_POSTGRES_PASSWORD=password -e DB_ENV_POSTGRES_DB=dockerdb -d --link postgres:db --name ruby y_yamada/ruby:2.1

docker_org

 

あとがき

ざっくりとDockerでサービスを作りました。 本番で使うためにはPostgreSQLのデータをDBコンテナに貯めずに外だしするためにVOLUMEで土台のサーバーのディレクトリをマウントするとか、cronが定期的にちゃんと動いているのか、twitter/slackのサービスが止まっている時にどうするかなどなど必要な処理があると思います。 chefやansibleでサーバーを構成するのが実績もあって手堅いですが、Dockerのように動いた実績のあるものをそのまま持ち運べたり手軽にコピーできるのはDockerにしかない魅力です。 まずは自社サービスをオレオレサービス化や秘伝のタレ化させずに共有するところから始めると最初の一歩が踏み出せるかもしれません。

ではではLet’s Happy Hacking!

※アイキャッチ画像はDockerのサイトからお借りしました。