えいのうにっき

むかしのじぶんのために書いています

Go言語で書いた Web アプリケーションの習作をサービス化して公開するところまでやってみた

もともと数ヶ月前から、Go言語によるWebアプリケーション開発 を読みながら Go での Webアプリケーション開発の勉強をしていた。

Go言語によるWebアプリケーション開発

Go言語によるWebアプリケーション開発

「実際に動くもの」を、「手を動かして作りながら学ぶ」のが僕は好きで、今回も同様、それを楽しんでやっていたのだけど、思いの外それっぽいものができあがってしまって。これをそのままローカルで動かすだけじゃおもしろくないな、もったいないな、と思ったので、それをサービス化して公開するところまでやってみた。

かんじんのアプリケーションは↓これ。Yukizuri と書いて「ゆきずり」と読む。

yukizuri.moshimo.works

ログインもない、ログも残らない、そんな「ゆきずり」の会話を楽しむチャットサービス、ということで。仕組み的には websocket を用いているので、(参加人数が増えたときには知らんけど)それなりにリアルタイムに会話できてちょっとおもしろい感じです。

僕が選んだアイコンと配色のせいか、期せずしてそこはかとなくエロい雰囲気が漂っている気がするけど、サービス自体は健全なものです。たぶん。

「もったいない」が主な動機ではあったけど、今回このサービス化・公開を通じて、以下のような知見が深まったように感じている。なんというか、またひとつ地力がついたような。

  • websocket 通信をともなうアプリケーションに関する基礎知識
  • 新しめのバージョンの fluentd と仲良くなる
  • Goアプリケーションのデーモン化手法
  • templateファイルも含めたシングルバイナリ化する方法
  • その他、様々な障害にもくじけないつよい気持ち

以下に、主に自分のための備忘録として、もう少し詳しく掘り下げた記録を残しておこうと思う。

サービス化 〜 公開までのおおまかなステップ

勉強の結果できあがった習作をサービス化し、公開にまでこぎつけるまで踏んだおおまかなステップは以下の通り。

  1. それっぽくなるようにデザインに手を入れる
  2. 機能の追加
  3. デプロイ先となるサーバの準備
  4. アプリケーションのデプロイの準備

以下、各ステップを少しだけ掘り下げてかいてみる。

1. それっぽくなるようにデザインに手を入れる

僕は、Webにおけるデザインというものにはからっきし向いていない。「努力したのか?」と言われるとその答えは「No」だけど、そもそもそこに時間を投資するよりもその他の分野に投資したい。というかんじである。

そんな僕の味方であり、僕の好きなサービスに Wrapbootstrap がある。今回に限らず、今までにも何度となくお世話になっている。今回もここで、いいかんじのチャットぽいUIを持つ bootstrap テンプレートを十数ドルで購入し、自分の習作に対してスタイルを当てた。

ロゴは Squarespace で生成したものを使った。余談だけど、自分のサービスのロゴを考えてる時間はいいものですね。

2. 機能の追加

Yukizuri には、元の書籍では実装されている機能を削ぎ落としたり、逆に追加したりしている機能がある。

  • 削いだ機能
    • ソーシャルログイン機能
    • イコン画像アップロード機能
  • 追加した機能
    • メンバーの入退室を知らせるシステムメッセージ
    • 参加済みメンバーリストの表示
    • アクセス元IPアドレスの表示
    • イコン画像のかわりに、identicon.js を用いてのアイコン表示
    • LTSV形式でのログの出力
    • など

ソーシャルログイン機能を削いだ理由は、「Yukizuri」なんて名前のサービスにソーシャルログインしたい人なんかきっといないだろうから。僕ならしない。アイコン画像のアップロードも、てめぇ浮かれてんじゃねぇぞって感じなんで取り除きました。

逆に機能追加した「メンバーの入退室を知らせるシステムメッセージ」とか「参加済みメンバーリストの表示」とかは、まぁそういうのがあったほうがわかりやすいし楽しいかなと思ったので追加した。「アクセス元IPアドレスの表示」とか「LTSV形式でのログの出力」とかは、どちらかというと自己保身のための機能追加で、まさかとは思うけど本当にこのサービス上でゆきずりの会話が交わされたらなんかヤバそうってことで、抑止力的な意味合いで。後述するけど、出力されたLTSVのログは fluentd の tail プラグインで舐めて BigQuery に送ってます。

f:id:a-know:20171226230420p:plain

f:id:a-know:20171226230454p:plain

もともとの実装に対してちょっとしたアレンジ実装を加えるのも楽しいですよね。

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 アプリケーションをデーモン化する - QiitaSupervisorで簡単にデーモン化 - 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>
view raw td-agent.conf hosted with ❤ by GitHub
gist.github.com

(すんげぇ適当に作った 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"
view raw metric-plugins.conf hosted with ❤ by GitHub
gist.github.com

[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"
view raw check-plugins.conf hosted with ❤ by GitHub
gist.github.com

あとは運用しながら追加したりしてみるよてい。

今回 Go で Webアプリケーションを作ったことによる、なにげに一番の収穫なことは、mackerel-plugin-gostats が自分のアプリケーションに対して使えるようになった、ということ。

github.com

f:id:a-know:20171226230956p:plain

こいつを使うためには、アプリケーション側にもそのための実装を少し加える必要がある。と言っても↓これくらいだけど。

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 を使った。

github.com

もともと 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"))

しれっと書いているけど、これ、最初はページが全然表示されなくて、でもその理由がさっぱりわかんなくてウンウン唸ってた記憶がある。こちら↓のエントリが突破口となり、無事解決できました。ありがとうございます!

tomi-ru.hatenablog.com

デプロイ・アプリケーションのリスタートをどうするか

僕はいままでアプリケーションのデプロイっていうとだいたい「ウチは 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 を触っている時間よりも、サービスを公開するための作業の時間の方が圧倒的に長くなってた。ただ、冒頭でも挙げたようなさまざまな学びが今回の作業を通じて得られたので、自分としてはやってよかったなという気持ちです。あとなによりも、年内に間に合ってよかった。

yukizuri.moshimo.works



follow us in feedly