2019-03-05

雑にワンライナーのシェルスクリプトをHTTPサーバーとしてデプロイする

[追記]

執筆後にGAE-FEがGCEと同様の値段がかかる&&最小インスタンス数が1以上であることから無茶苦茶お金がかかることに気づいたため、この記事はなかったことになりました。

以下本文

自分はシェルスクリプトが好きなので、いろいろなことをシェルスクリプトでやるのですが、たまにシェルスクリプトがHTTPサーバーであってほしいときがあります。

話は変わって最近AtCoderをやろうかなと思っているのですが、AtCoderにはABC、AGC、ARCの3種類の定期開催のコンテストがあります(参考: AtCoder Grand Contest (AGC) について:suda_k0のブロマガ - ブロマガ)。しばらくABCに参加しようかなと考え、公式のGoogle Calendarを見ると、

Calendar Screenshot

ABC以外も混ざっている!!

ABC(AtCoder Beginner Contest)の予定は表示しておきたいけど、他のAGC(AtCoder Grand Contest)などの参加する予定のないものは表示しておきたくないのでどうにかフィルタしたいところです。

ところでGoogleカレンダーのカレンダーはiCal形式でインポート/エクスポートできます。自分はiCal形式を前に少し触ったことがあったので、シェルスクリプトでフィルタをしてみようと思いやってみたのが以下です。

curl https://calendar.google.com/calendar/ical/atcoder.jp_gqd1dqpjbld3mhfm4q07e4rops%40group.calendar.google.com/public/basic.ics \
  | sed -e '/BEGIN:VEVENT/{:l N; /END:VEVENT/!bl; /\(Beginner\|ABC\)/!d;}'

1行目のcurlで取ってきているURLが先の画像でお見せしたAtCoder公式のカレンダーのiCalです。これをsedでいい感じに"Beginner"や"ABC"を含むものだけにフィルタします。雑いですが個人用なのでこれで十分です。

これで現時点でのABCのみのカレンダーを作ることができましたが、今後も自動的にアップデートしてほしいものです。このシェルスクリプトにURLを与えて、URLが叩かれるたびに新しいカレンダーが出力されるようになれば完璧なのに… ここまでが前置きです。

やりたいこと

  1. HTTPサーバーを立っている
  2. リクエストが来るとシェルスクリプトが実行され、標準出力がレスポンスで返される

これをできるだけ楽に作りたい。

検討したサービス

一番に思いつく方法はEC2やGCEなどのインスタンスをまるごと借りるタイプのものですが、お金のない学生なので選択肢に挙がりません。

次にAWS Lambdaでシェルスクリプトが動く、みたいな話があったのを思い出しましたが、個人のプロジェクトはGCPで統一しているので管理コストなども考え今回は除外させていただきました… すみません(AWSに慣れている方は一番の選択肢になるかもしれないです)。

同様にGoogle Cloud FunctionsやFirebase Functionsでシェルスクリプトが動いてほしいという希望を持ちましたが、残念ながら対応していないようでした。

そして今回採用したのがGoogle App Engine(GAE) Flexible Environment(FE)です。FEは使ったことがなかったのでどれだけ楽にできるのか不安なところがありましたが、慣れればまあそこそこ頭を使わずにできそうな感じだったので紹介します。

GAE-FEでシェルスクリプトを実行するHTTPサーバーを立てる

シェルスクリプトの公式のランタイムはありませんので、カスタムランタイムとしてDockerイメージをデプロイすることにします。というわけでまずHTTPサーバーとシェルスクリプトを含むDockerfileを書きます

FROM alpine
RUN apk add sed bash curl coreutils busybox-extras
COPY index.cgi /www/cgi-bin/index.cgi
RUN chmod 700 /www/cgi-bin/index.cgi
CMD httpd -h/www -p$PORT -f

サーバーにはBusybox httpdを使用しました。BusyboxがもともとAlpineに入っていた(といいつつ busybox-extras を別途入れる必要があります)のと、そこそこ楽そうだったからです。あとsedやcoreutilsを入れているのはGNUのコマンドが使いたかったからです。シェルスクリプトに必要なコマンド類はこれらに加えてfindとか入れておけばだいたいのスクリプトで足りるイメージがあります。

これに伴いシェルスクリプトをCGI化する必要があります。index.cgiとして以下のように記述しました。

#!/usr/bin/env bash
echo 'Content-Type: text/calendar'
echo
curl https://calendar.google.com/calendar/ical/atcoder.jp_gqd1dqpjbld3mhfm4q07e4rops%40group.calendar.google.com/public/basic.ics \
  | sed -e '/BEGIN:VEVENT/{:l N; /END:VEVENT/!bl; /\(Beginner\|ABC\)/!d;}'

まあContent-Typeの記述が増えただけです。こいつらを同じディレクトリに置いて docker build . とすればなんとかなります。 docker run -e PORT=8080 <image-id> でテストしておきましょう。

面倒だなと思ったのが、なぜか httpd がCtrl-Cで死んでくれないことです。ローカルで立ち上げると普通に死ぬんですけどね…(詳しい方教えてください mm)

テストできたら以下のapp.yamlを脳死で書いて

runtime: custom
env: flex

デプロイしましょう。

$ gcloud app deploy

GAEにデプロイしたことがない方はこのあたりが参考になるかもしれません: Testing and Deploying Your Application  |  Custom runtimes for the App Engine flexible environment  |  Google Cloud

あとは表示された"Target URL"を叩けば動きます。

Deployment Screenshot

Result Screenshot

(画像は編集しています)

このURLをGoogleカレンダーに設定して… うまくいきました!

今回使ったソース: gadgets/abccal at master · acomagu/gadgets

感想

このDockerfileとapp.yamlは使い回せるので、次回以降雑にシェルスクリプトをデプロイする機会があれば爆速デプロイが可能なんじゃないかと思っています。でもワンライナーくらいどこかにペッと貼り付けたら動いてほしいなあ…

まあDockerだと使えるコマンドも選べるしローカルでテストできるしそこは間違いなく良いところですね。とりあえずカレンダーがうまくいってよかったです。