最近,ニコニコ動画のスナップショット検索APIはバージョンアップしました.
今まで,ニコニコ動画の検索に関しては,
にお世話になっていました.
だけど,3月からニコニコ動画側がv1のAPI提供を終了したため,このgemではエラーが出るようになりました.
というわけで新API対応のgemを作りました.
続きを読むみなさんさようなら.
2016年が終わります.
個人的に痛々しいサービスを作り始めて,そろそろ3年経ちました.運用していると,バグもあるしバージョンアップも必要だし,たまにはリファクタリングや機能追加もしないと存続できないということがよくわかります.
に始まり,
https://twitter.com/aochan_prpr
https://twitter.com/hanazawa_sick
と,痛い声優botシリーズも横展開.
他にも,
twitterクライアントとか,
タスク管理サービスとか,
痛い声優情報サービスとか,
いろいろ増えてきました.
ここに来て, https://twitter.com/aochan_prpr
を閉じます.
長らくのご愛顧,ありがとうございました.
復活する予定は今のところありませんが,アカウントは削除しません.
続きを読むこの記事はCrowdWorks Advent Calendar 2016 2日目の記事です.
みなさんDockerのswarmモード,使ってますか? ここ最近変更が多くて,もうじき1.13が出ますね.
いろいろと便利機能が増えていますが,詳細についてはこちらで紹介されています. Docker 1.13の気になる変更点と新機能
CrowdWorksでは社内の検証用サーバをDockerで構築していて,その部分でswarmを活用しています. なお,この記事を書いている時点では1.13はまだリリースされておらず,これから書く話は全て1.12で構築しています.
アプリケーションが動く環境として,
くらいを作ることはあると思います.
CrowdWorksではそれに加えて,「社内のエンジニア以外に機能確認をしてもらうための環境」を,stagingとは別に用意していました. これは,
というような事情があります. 実際にこういう環境があると,「なんか手元だと鍵の設定とかめんどくさいなー」とか「assets:precompileされるとこれってどうなるんだろう」みたいな,微妙な部分の変更で手元で確認しにくいようなことを確認するときに,すごく便利になってきます.
元々そういう「検証用サーバ」は存在していたんですが,メンテがツライ.
以前はChefで構築していて,1環境に1EC2インスタンスを割り当てる形でした.ただ,Chefで作っていたとしても,本番のインフラ構成を変更するたびに,検証用サーバの方にもChefを流し直さないといけないのは当然ですよね.
ところが,本番では成功したのに,検証用だとちょっと環境が違ってChefが上手いこと流れないということはよくありました. また,Chefで設定されていた設定を手動で変更してしまっていて,既にChefを流し直せないサーバが生まれていました.
これをこのまま増やしていくのはツライ
というわけで新しく作り直すことにしました.
CrowdWorksが大きすぎました……単純にこれがクリティカルすぎて,もう自作するしかないという覚悟を決めていました.
なんといってもこの場合, イミュータブルになる というのが最も嬉しかった.
コンテナを終了してしまえば,中身を手動でいじっていても消えてしまいます. 新しく起動する際には,必ずDocker image通りのものしか起動されません.
Chefでもインスタンスごと作り直せばいい話ですが,それを始めると圧倒的にDocker imageをベースに起動した方が早い.
というわけで初期の段階でDockerにすることは決まりました.
Dockerのオーケストレーションツールはいくつか候補があります.
やりたいこととしては,
くらい.
CrowdWorks自体ではAWSをよく使っているので,ECSも候補ではあったんですが…….
swarmの良いところは,
というのが大きかったです.特にホスト名のところはECSだと解決するのが難しそうだなーと悩んでいたところでした.
これはオマケ程度の機能なのですが,やっぱりWebUIやAPIはあったほうがテンションあがります.
デプロイするたびに,swarm managerのホストに入ってコマンド叩いたり,シェルスクリプト実行したりするのは,あまりうれしくない……ということで基本的な操作をWebUIからできるようにしようという目標もありました.
ついでにAPIもあると,手元から叩けて更にテンション上がる!
こういうのはコードネームを付けると親しみやすいです. いろいろと候補を募って,yadockeriという名前になりました.
ちなみに名前の由来は,
Yet Another Docker Infra
です.絶妙にDockerが全部入るようにしておきました.これで「ヤドカリ」と呼んでいます. ホストにDockerコンテナ立てるヤドカリっぽさと,車輪の再発明感も漂う名前です.
だいたいこんな構成にします. ここでは,akiraという名前のサービスを作り,akira.y.example.comというURLでアクセスできるようにしたいと思います.
ユーザが増えて,たとえばtoruという名前でサービスを作りたい場合には,router以外の,akiraと名前のついているネットワークやコンテナセットが増えて,toruの分ができる,という形ですね.
ここではoverlay networkを使って,要求されたコンテナにリクエストを振り分けられるようにします.
まず,router用のDockerを作ります.
FROM nginx:stable-alpine COPY nginx.conf /etc/nginx/
単純なnginxコンテナを使います. ここに,
user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; # resolverが未定だとエラーになるのでDockerコンテナ内のresolv.confに書いてあるIPを決め打ちで書いてみる resolver 127.0.0.11 valid=5s; server { listen 80; set_real_ip_from '10.0.0.0/8'; real_ip_header X-Forwarded-For; proxy_set_header X-Forwarded-Scheme $http_x_forwarded_proto; proxy_set_header Host $host; server_name _; # 例えばakira.y.example.comだと$subdomain=akiraになる # api.akira.y.example.comの場合も$subdomain=akiraにproxyする if ($host ~* ([^.]+)\.y\.(dev\.)?example\.com$) { set $subdomain $1; } location / { proxy_pass http://$subdomain:8080; } } }
たとえば,akira.y.example.com というリクエストが来た場合には,http://akira:8080 にproxy_passされます.
そして,http://akira というリクエストは,swarmにより,akiraという名前で作ったserviceにリクエストを飛ばしてくれます.
そのため,後述するスタックを作るときに,akiraという名前で作ることにより,このrouterからakiraサービスにリクエストが飛ぶようになります.
そしてmanagerホスト内で,このDockerをbuildし,workerからでも参照できるようにECRにpushしてきます.
$ docker build -t hoge.ecr.ap-northeast-1.amazonaws.com/router . $ docker push hoge.ecr.ap-northeast-1.amazonaws.com/router $ docker network create --driver overlay --subnet=$ROUTER_SUBNET router $ docker service create --network router --mode global --with-registry-auth --name router -p 80:80 hoge.ecr.ap-northeast-1.amazonaws.com/router
こうしてrouterというnetworkの属しているrouterコンテナ(nginx)を動かしておきます.
後述するスタックはこのrouterネットワークに所属するようにしてやり,サービス名をホスト名と同名にしておけば,同じネットワーク内なので名前解決され,nginxからのリクエストが飛んで来るというわけです.
overlay networkはできたので,次は実際にCrowdWorksが動くコンテナです. CrowdWorksはRails以外にもmemcachedやElasticsearch等,複数のコンテナ群により一つのサービスを実現しています.このコンテナ群1セットをスタックと呼ぶことにします.
デプロイはdockerコマンドを叩くことが多いので,現状シェルスクリプトで実装されています.
この辺をgoで書き直そうと思ったんだけど,分量が多くて進んでない…….
スタックを作るあたりをちょっと紹介しましょう. routerは既に作ってある状態です.
# 周辺のミドルウェアのコンテナを作る create_stack() { # 使用するイメージをpullしなおす docker-compose -f docker-compose-yadockeri.yml pull # Docker Distributed Application Bundleという現状experimentalな機能を使って # composeのymlからswarmにデプロイするためのメタデータを生成 # .dabファイルはただのJSONなので何が出力されるかはファイルを見れば分かるけど # docker service createするための情報が入ってる # dabでは現状build命令に対応していないので、 # デプロイする度にビルドしなおさないといけないアプリケーション本体はここには含めない # 基本的に一度デプロイしたら変更しない周辺のミドルウェアに使用 docker-compose -f docker-compose-yadockeri.yml bundle -o ${STACK_NAME}.dab # dabファイルを元に新しいスタック用のoverlay networkとコンテナをデプロイ docker stack deploy --with-registry-auth $STACK_NAME } # 常駐プロセスのサービスを作る create_service() { local service=$1 local entrypoint=$2 # スタック内のネットワークはバックエンドのミドルウェア群との接続 # routerのネットワークはフロントエンドのブラウザからの通信に使ってる docker service create \ --network ${STACK_NAME}_default \ # akira_defaultみたいなネットワーク名を指定 --network router \ # ここで先程作ったrouterネットワークを指定 --replicas 1 \ --with-registry-auth \ --name $service \ # ここで指定される名前をakiraとすることで,http://akiraが名前解決される $IMAGE $entrypoint }
memcachedやElasticsearch等の周辺サービスのコンテナはdocker-composeをベースに作成します. ここではdabというファイルに変換していますが,1.13.0以降,docker-compose.ymlをそのまま使えるようになります.
WebUIは規模としてかなり小さいし(上記のshを実行してくれれば良い),phoenix(elixir)で作りました.
このphoenixアプリケーションも,Dockerに詰め込んで,yadockeriの一部としてswarmのserviceとして管理します.
phoenixのアプリケーションをDockerに乗せてデプロイするには,少しコツが要ります.
詳しくは, phoenixアプリケーションをDockerでデプロイする
にまとめてあります. ポイントとしては,config/prod.ex内で環境変数を使いたい場合には,ちょっと特殊な展開をしてやる必要があるということ.
config :sample_app, SampleApp.Repo, adapter: Ecto.Adapters.MySQL, username: "${DB_USER}", password: "${DB_PASSWORD}", database: "sample_app", hostname: "${DB_HOST}", port: 3306, pool_size: 5
という設定を作って,起動時にRELX_REPLACE_OS_VARS=true
という環境変数をセットします.
$ docker run --name sample_app \ -e DB_USER=hoge \ -e DB_PASSWORD=fuga \ -e DB_HOST=localhost \ -e RELX_REPLACE_OS_VARS=true \ sample_app:latest
これでアプリケーション実行時に環境変数を展開してくれます.
WebUIからスタックの操作をしたいため,前述のシェルスクリプトはphoenixアプリケーション内から叩く必要があります. 前述のシェルスクリプトのコマンドから分かる通り,スタックを操るコマンドは全てswarm manager上で実行する必要があるため,WebUIのコンテナだけは,swarm manager上で動くように制約を入れて起動します.
$ docker service create \ --name yadockeri \ -p 8080:8080 \ --replicas 1 \ --constraint "node.role == manager" \ # この指定によりmanager上で動くという制約が入る
WebUIを作る上でのキモはおそらくこの辺ですが…….
create_service()
等のコマンドはmanagerホスト上のdockerコマンドが必要になります.
そのため,WebUIからも,managerホスト上のdockerコマンドを叩ける状態にしておく必要があります.
しかし,WebUI自体もmanager上で動いているコンテナです.コンテナ内から親ホストのdockerコマンドを叩くという,ちょっとトリッキーなことをしてやる必要があります.
これを実現するために,
という準備をしておきます.
ENV DOCKER_CLIENT_VERSION=1.12.3 ENV DOCKER_API_VERSION=1.24 ENV DOCKER_COMPOSE_VERSION=1.8.1 RUN set -x && \ curl -fsSL https://experimental.docker.com/builds/Linux/x86_64/docker-${DOCKER_CLIENT_VERSION}.tgz \ | tar -xzC /usr/local/bin --strip=1 docker/docker RUN set -x && \ curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose && \ chmod +x /usr/local/bin/docker-compose
こんなDockerfileを書いておくと,Dockerのクライアントだけをコンテナ内に入れることができます.
あとは起動時のオプションとして,
$ docker service create \ --name yadockeri \ -p 8080:8080 \ --replicas 1 \ --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock,readonly=false
として,/var/run/docker.sockをマウントします.
まとめると,起動コマンドはこんな感じ.
docker service create \ --network router \ --with-registry-auth \ --name yadockeri \ -p 8080:8080 \ --replicas 1 \ --constraint "node.role == manager" \ --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock,readonly=false \ --mount type=bind,source=/home/devops,target=/srv,readonly=false \ --e DB_USER=hoge \ --e DB_PASSWORD=fuga \ --e DB_HOST=localhost \ --e RELX_REPLACE_OS_VARS=true \ crowdworks/yadockeri-web
このような状態になっていれば,WebUIからのデプロイでは,create_service()
等のコマンドを叩けば良いはずです.
というわけでelixirからは,
case System.cmd("sudo", ["-E", "stack.sh", "create", deployment.app_stack.name, deployment.branch]) do {result, 0} -> {:ok, result} {error, _} -> {:error, error} end
こんな形で呼び出せるようにしておきます.
sudoが必要なのは,dockerコマンドだからですね…….そこはグループで管理してもよいのですが,普通にsudoersに入れたほうがラクだったので.
これで無事にデプロイが走るようになりました.
ECSでは実現できないような,柔軟な構成でアプリケーションをデプロイできるようになりました. その分多少複雑ではありますが…….
今回,overlay networkで解決できたことが大きくて,これはかなりスマートな方法が取れたなぁと素直に思います.
WebUIを作ったのは,環境変数の設定を保存しておいて,デプロイ時に差し込めるようにしたかったりする意味もありました.が,環境変数設定まではまだたどり着いていません. dockerコマンド的には,envオプションで渡すだけなのですが…….
あと,1.13.0になってから作ったほうが,docker-compose周りや,env-fileオプションが実装されるので,もう少し楽になったかなぁと思います.
この記事はCrowdWorks Advent Calendar 2016 の2日目の記事でした。明日は@ganta の「CrowdWorksのDocker開発環境」の予定です。