まえふり
久し振りに連絡を取り合った天文観測クラスタの先輩が講習会に講師で呼ばれるレベルになっていたので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でサービスを作りました。 本番で使うためにはPostgreSQLのデータをDBコンテナに貯めずに外だしするためにVOLUMEで土台のサーバーのディレクトリをマウントするとか、cronが定期的にちゃんと動いているのか、twitter/slackのサービスが止まっている時にどうするかなどなど必要な処理があると思います。 chefやansibleでサーバーを構成するのが実績もあって手堅いですが、Dockerのように動いた実績のあるものをそのまま持ち運べたり手軽にコピーできるのはDockerにしかない魅力です。 まずは自社サービスをオレオレサービス化や秘伝のタレ化させずに共有するところから始めると最初の一歩が踏み出せるかもしれません。
ではではLet’s Happy Hacking!
※アイキャッチ画像はDockerのサイトからお借りしました。