もともと数ヶ月前から、Go言語によるWebアプリケーション開発 を読みながら Go での Webアプリケーション開発の勉強をしていた。
- 作者: Mat Ryer,鵜飼文敏,牧野聡
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/01/22
- メディア: 大型本
- この商品を含むブログ (3件) を見る
「実際に動くもの」を、「手を動かして作りながら学ぶ」のが僕は好きで、今回も同様、それを楽しんでやっていたのだけど、思いの外それっぽいものができあがってしまって。これをそのままローカルで動かすだけじゃおもしろくないな、もったいないな、と思ったので、それをサービス化して公開するところまでやってみた。
かんじんのアプリケーションは↓これ。Yukizuri
と書いて「ゆきずり」と読む。
ログインもない、ログも残らない、そんな「ゆきずり」の会話を楽しむチャットサービス、ということで。仕組み的には websocket を用いているので、(参加人数が増えたときには知らんけど)それなりにリアルタイムに会話できてちょっとおもしろい感じです。
僕が選んだアイコンと配色のせいか、期せずしてそこはかとなくエロい雰囲気が漂っている気がするけど、サービス自体は健全なものです。たぶん。
「もったいない」が主な動機ではあったけど、今回このサービス化・公開を通じて、以下のような知見が深まったように感じている。なんというか、またひとつ地力がついたような。
- websocket 通信をともなうアプリケーションに関する基礎知識
- 新しめのバージョンの fluentd と仲良くなる
- Goアプリケーションのデーモン化手法
- templateファイルも含めたシングルバイナリ化する方法
- その他、様々な障害にもくじけないつよい気持ち
以下に、主に自分のための備忘録として、もう少し詳しく掘り下げた記録を残しておこうと思う。
サービス化 〜 公開までのおおまかなステップ
勉強の結果できあがった習作をサービス化し、公開にまでこぎつけるまで踏んだおおまかなステップは以下の通り。
- それっぽくなるようにデザインに手を入れる
- 機能の追加
- デプロイ先となるサーバの準備
- アプリケーションのデプロイの準備
以下、各ステップを少しだけ掘り下げてかいてみる。
1. それっぽくなるようにデザインに手を入れる
僕は、Webにおけるデザインというものにはからっきし向いていない。「努力したのか?」と言われるとその答えは「No」だけど、そもそもそこに時間を投資するよりもその他の分野に投資したい。というかんじである。
そんな僕の味方であり、僕の好きなサービスに Wrapbootstrap がある。今回に限らず、今までにも何度となくお世話になっている。今回もここで、いいかんじのチャットぽいUIを持つ bootstrap テンプレートを十数ドルで購入し、自分の習作に対してスタイルを当てた。
ロゴは Squarespace で生成したものを使った。余談だけど、自分のサービスのロゴを考えてる時間はいいものですね。
2. 機能の追加
Yukizuri には、元の書籍では実装されている機能を削ぎ落としたり、逆に追加したりしている機能がある。
- 削いだ機能
- ソーシャルログイン機能
- アイコン画像アップロード機能
- 追加した機能
ソーシャルログイン機能を削いだ理由は、「Yukizuri」なんて名前のサービスにソーシャルログインしたい人なんかきっといないだろうから。僕ならしない。アイコン画像のアップロードも、てめぇ浮かれてんじゃねぇぞって感じなんで取り除きました。
逆に機能追加した「メンバーの入退室を知らせるシステムメッセージ」とか「参加済みメンバーリストの表示」とかは、まぁそういうのがあったほうがわかりやすいし楽しいかなと思ったので追加した。「アクセス元IPアドレスの表示」とか「LTSV形式でのログの出力」とかは、どちらかというと自己保身のための機能追加で、まさかとは思うけど本当にこのサービス上でゆきずりの会話が交わされたらなんかヤバそうってことで、抑止力的な意味合いで。後述するけど、出力されたLTSVのログは fluentd の tail プラグインで舐めて BigQuery に送ってます。
もともとの実装に対してちょっとしたアレンジ実装を加えるのも楽しいですよね。
3. デプロイ先となるサーバの準備
これなんだけど、最初は GAE でホストしたいなーと思ってた。んだけど、このサービスのキモである websocket が GAE の standard environment では使えないってことなので GAE は見送りに。
代わりのホスト先として、ちょっと前に無料になった GCE の f1-micro の無料枠があったので、ここにアプリケーションとミドルウェアを乗っけることにした。
docker でサービス運用したことがまだなかったので、もしや今回がその docker チャンスか...?と思ったけど、ちょっと年内にはやりきりたかったので、慣れた方法でプロビジョニングすることにした。具体的にいうと itamae。未経験だった Ansible をやってみるのもアリかー?と思って入門記事みたけど、ちょっともんにょりしたのでそれもやめてしまった。。
itamae でセットアップしたのはだいたい以下のような項目。
- hostname
- logrotate
- timezone
- rsync
- supervisor
- nginx
- td-agent
- mackerel-agent
- certbot(Let's Encrypt)
以下にポイントっぽく思えたところを特記する。
Go Webアプリケーションのデーモン化
恥ずかしい話、僕は、なぜだかよくわからないけど、「Webアプリケーションのデーモン化」ということに今までまともに向き合うことなく、ここまでやってこれてしまった。
なのでこのタイミングで1からのスタート、というかんじだったのだけど、supervisorを使えば目的は達成できそうだったので、サーバにsupervisord をインストールし、以下のような設定をして事なきを得た。
; for yukizuri
[program:yukizuri]
command=/var/www/yukizuri/app/yukizuri.bin -addr=":8080" -logging=true
autostart = true
startsecs = 1
user = root
redirect_stderr = true
stdout_logfile = /var/www/yukizuri/log/production.log
正確には、本当に事なきを得ているのかはわかってない気がするので、何かおかしいところがあったら教えてくださいお願いします。実行ユーザーはよくなさそう。
Supervisor を利用して Go アプリケーションをデーモン化する - Qiita や Supervisorで簡単にデーモン化 - Qiita などの記事が参考になりました。ありがとうございます。
nginx + websocket
nginx で websocket 通信をプロキシするには、Upgrade ヘッダと Connection ヘッダを指定する必要があるらしい。
# for websocket connection location /room { proxy_pass http://localhost:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_redirect default; }
(Go App との UNIX ソケット通信はやってない......、すみませんすみません :sweat: もしやるなら Golang で書いた Web アプリケーションを UNIX ドメインソケットで公開 - at kaneshin が参考になりそう!)
こちらの記事などを参考に上記のように設定したら、特に問題なく websocket 通信できるようになった。素敵。
あと、デフォルトでは websocket コネクションは60秒通信がない状態が続くと切断されてしまうっぽいのだけど、今回のようなアプリケーションだとそれだと困るので proxy_read_timeout 60m;
の指定を追加した。
server { listen 443 ssl; server_name yukizuri.moshimo.works; proxy_read_timeout 60m; ...
新しめのバージョンの fluentd と仲良くなる
今までにも別のサービスで fluentd は活用していたのだけど、そのバージョンは v0.12.x 系。今や v1.0 ということで(v1.0 おめでとうございます)、v0.14.x の頃から「やんなきゃなー」と思っていたところでもあったので、ここからは逃げずに取り組むことにした。
とはいっても大したことはしてなくて、今回の要件だと、「nginx やアプリケーションが出力するログを tail プラグインで舐めて BigQuery に送る」というもの。
例えば以下は、nginx のアクセスログを BigQuery に転送するための設定。覚悟を決めていたほどドラスティックな変化はなくて、「おっ?」と思ったのは buffer
inject
ディレクティブの指定くらい、だろうか。
<source> | |
@type tail | |
@id input_tail_nginx_access_log | |
format ltsv | |
time_format %Y-%m-%d %H:%M:%S %z | |
path /var/log/nginx/yukizuri.access.log | |
read_from_head true | |
rotate_wait 60 | |
pos_file /var/log/td-agent/yukizuri_access_log.pos | |
tag yukizuri.log.nginx | |
</source> | |
<match yukizuri.log.nginx> | |
@type bigquery | |
@id bigquery_nginx_access_log | |
method insert | |
<buffer time> | |
flush_interval 0.1 # flush as frequent as possible | |
buffer_queue_limit 10240 # 1MB * 10240 -> 10GB! | |
flush_thread_count 16 | |
timekey 1d | |
</buffer> | |
auth_method json_key | |
json_key /etc/td-agent/.keys/bq-credential-for-fluentd-jsonkey.json | |
project project-name | |
dataset yukizuri_nginx_log | |
auto_create_table true | |
table nginx_access_log_%Y%m%d | |
<inject> | |
time_key time | |
time_type string | |
time_format %s | |
</inject> | |
schema [ | |
{ "name": "time", "type": "timestamp" }, | |
{ "name": "local_time", "type": "string" }, | |
{ "name": "host", "type": "string" }, | |
{ "name": "forwardedfor", "type": "string" }, | |
{ "name": "req", "type": "string" }, | |
{ "name": "status", "type": "integer" }, | |
{ "name": "size", "type": "integer" }, | |
{ "name": "referer", "type": "string" }, | |
{ "name": "ua", "type": "string" }, | |
{ "name": "reqtime", "type": "float" }, | |
{ "name": "cache", "type": "string" }, | |
{ "name": "runtime", "type": "float" }, | |
{ "name": "vhost", "type": "string" }, | |
{ "name": "id", "type": "string" } | |
] | |
</match> |
(すんげぇ適当に作った conf なので、まずそうなところがあれば教えてください)
ドッグフーディングのための mackerel-agent
いちおう僕も Mackerel の中の人ってことで、サービス運営と同時に Mackerel のドッグフーディングもできたら一石二鳥で最高じゃんというかんじで呼吸をするかのごとくエージェントをセットアップ。
初期セットアップの段階で導入したプラグインを大公開しちゃう。
[plugin.metrics.linux] | |
command = "mackerel-plugin-linux" | |
[plugin.metrics.accesslog] | |
command = "mackerel-plugin-accesslog /var/log/nginx/yukizuri.access.log" | |
[plugin.metrics.conntrack] | |
command = "mackerel-plugin-conntrack" | |
[plugin.metrics.fluentd] | |
command = "mackerel-plugin-fluentd" | |
[plugin.metrics.gostats] | |
command = "mackerel-plugin-gostats -port=8080 -path=/api/stats" | |
[plugin.metrics.inode] | |
command = "mackerel-plugin-inode" | |
[plugin.metrics.nginx] | |
command = "mackerel-plugin-nginx -port=8888" |
[plugin.checks.supervisord-procs] | |
command = "check-procs --pattern=supervisord" | |
[plugin.checks.nginx-procs] | |
command = "check-procs --pattern=nginx" | |
[plugin.checks.fluentd-procs] | |
command = "check-procs --pattern=fluentd" | |
[plugin.checks.app-procs] | |
command = "check-procs --pattern=yukizuri.bin" | |
[plugin.checks.uptime] | |
command = "check-uptime --warning-under=600 --critical-under=120" | |
[plugin.checks.ntpoffset] | |
command = "check-ntpoffset -w=50 -c=100" |
あとは運用しながら追加したりしてみるよてい。
今回 Go で Webアプリケーションを作ったことによる、なにげに一番の収穫なことは、mackerel-plugin-gostats が自分のアプリケーションに対して使えるようになった、ということ。
こいつを使うためには、アプリケーション側にもそのための実装を少し加える必要がある。と言っても↓これくらいだけど。
import ( "net/http" "log" "github.com/fukata/golang-stats-api-handler" ) func main() { http.HandleFunc("/api/stats", stats_api.Handler) log.Fatal( http.ListenAndServe(":8080", nil) ) }
websocket 通信の SSL/TLS 対応
もともとは習作のアプリケーションとはいえ、Let's Encrypt での https 対応もちゃんとやっている。ただそれにより、websocket 通信もそれに対応させる必要がでてきた。
最初こそ全くわからなくてパニくってしまったが、なんのことはない、クライアント側での実装を、
socket = new WebSocket("ws://" + location.host + "/room");
といったものから、
socket = new WebSocket("wss://" + location.host + "/room");
とするだけでよかった。拍子抜けだった。
APIキーなどの credential 情報の itamae でのセットアップ
mackerel-agent のAPIキーや、BigQueryへのログ転送のための credential など、どうしても秘匿情報を扱う必要がある。今回は itamae を使っているということで、sorah さんの itamae-secrets を使った。
もともと Chef の encrypt-databag に馴染みがあったということもあるのだろうけど、迷うことなくスッと使えてとてもよかった。ありがとうございます。
4. アプリケーションのデプロイの準備
template ファイルも含めたアプリケーションのシングルバイナリ化
今回の Go Webアプリケーションでは template を利用しており、サーバにデプロイするバイナリに template ファイルの内容も含める必要があった(開発時はルートディレクトリでバイナリを起動するから template ファイルも相対パスでうまいこと参照できるけど、本番環境となるとそうはいかない、みたいな理由)。
結論からいうと、go-assets(go-assets-builder)を使って template ファイルのコンテンツを返してくれるような Go のコードを生成、
$ go-assets-builder --package=main templates/ > templates.go
ハンドラ実装の中では以下のように書くことで呼べる。
f, err := Assets.Open(filepath.Join("/templates/yukizuri.html"))
しれっと書いているけど、これ、最初はページが全然表示されなくて、でもその理由がさっぱりわかんなくてウンウン唸ってた記憶がある。こちら↓のエントリが突破口となり、無事解決できました。ありがとうございます!
デプロイ・アプリケーションのリスタートをどうするか
僕はいままでアプリケーションのデプロイっていうとだいたい「ウチは capistrano におまかせしているんですよ」という箱入り息子状態だったので、「それ以外の方法でデプロイだと......?」みたいな感じだったのだけど、まぁここはそんなに頑張るところじゃないよね頑張らなくていいよね、ってかんじで、以下のような方法でデプロイ・起動をしてみている。
$ go-assets-builder --package=main templates/ > templates.go $ GOOS=linux GOARCH=amd64 go build -o yukizuri.bin $ rsync -a --backup-dir=./.rsync_backup/$(LANG=C date +%Y%m%d%H%M%S) -e ssh ./* yukizuri.moshimo.works:/var/www/yukizuri/app $ ssh yukizuri.moshimo.works sudo supervisorctl restart yukizuri
素朴すぎる気がするので、お仕事でやる場合には真似しないほうがよさそう。実装側でも、Graceful な restart を実現するためのケアまでは今回はできなかった。チャット中にデプロイしちゃうと、まさにゆきずり、ってかんじになりそう。
おわり
まさにこんなかんじで、気がついたら、Go Web App を触っている時間よりも、サービスを公開するための作業の時間の方が圧倒的に長くなってた。ただ、冒頭でも挙げたようなさまざまな学びが今回の作業を通じて得られたので、自分としてはやってよかったなという気持ちです。あとなによりも、年内に間に合ってよかった。