write.kogu

Google App EngineとGoでブログ風CMSを自作する

プログラミング GCP

Gopher on engine by kogu CC BY 3.0

Google App Engine(GAE)はGoogleの提供するPaaSです。2016年半ば現在のGoogle App Engine - write.koguでは、その歴史と現状を紹介しましたが、このサイトはそのGAEとGo言語を使ってブログ風のCMSを自作し運用しています。

こういう用途ではGAEのコンセプトであるスケールアウトをあまり活かせませんが、PaaSの恩恵を受けつつ、極めて低コストで運用できます。趣味で作るCMSであり、まだまだ低機能で小規模ですが、GAEの勉強として取り組みやすいお題でもあり、自作のCMSの構成などをご紹介します。

目次

なぜ自作するのかto TOC

わざわざ自作する最大の理由は、今のGAEに適した既存のプロダクトが無いためです。

GAEが盛り上がった頃には、幾つか作り込まれたプロダクトもありました。しかしどれもメンテナンスされておらず、現状のGAEへの対応は不十分です。また2010年頃の、ごてごてとしたブログ像が前提になっています。言語もPythonもしくはJavaで、当時まだ使えなかったGoのプロダクトは当然ありません。

PHPが使えるようになったことで、WordPressなどの著名なブログエンジンも使えるようになりましたが、それらはデータストレージにRDBMSを使い、ローカルのファイルシステムへの書き込みも前提としています(Wordpressの場合、ファイルシステム等のGAE対応を行うプラグインはあります)。せっかくGAEを使うのだから、Standard EnvでDatastoreを使いたいですし、他のGCPのサービスも活用し、GAEの仕様変更にも対応していける、そんなCMSを求めていました。

既存のブログエンジンがあまりに重厚だという点も、理由のひとつです。たとえばトラックバックのような廃れた機能のためのコードは一切不要ですし、コメントをCMSで扱うつもりもありません。極端に言えばいわゆるブログらしいフル機能など不要で、私たち自身がじっくりと物書きできれば十分。そして問題や不足が出た時、すぐに手を入れられる見通しの良さが確保できれば何よりでした。

比較的最近公開されているGAEに対応したCMSも、あるにはあります。たとえば以下の記事で紹介されているknightso/caterpillarは、積極的なGAE/Goのユーザーである合同会社ナイツオさんのプロダクトです。

ページテンプレートの更新にGAEのデプロイが必要などユーザーは選びますが、一通り機能の揃ったGAE/Go用の貴重なCMSです。Datastoreでのモデルの表現や静的ファイルの取り扱いなど、汎用的なCMSを作る上でとても参考になります。しかし、記事という大きな括りで投稿し主に時系列で提示するブログ風、のCMSとして手を入れるのは、なかなか手間がかかります。

こういった事情と、また自分たちの学習のために、再発見でも再発明でも良いからGAE/Goでブログ風のCMSを作ることにしました。

GAEとGoの構成to TOC

Webアプリケーションフレームワークto TOC

Goは標準ライブラリが充実している言語です。標準ライブラリに含まれるnet/httpパッケージでも、Webアプリケーションは十分作れます。しかし、ルーティングやミドルウェアなどを模索していると、どうしてもフレームワークが欲しくなります。

Goには有名なWAFが色々とあり、今でも新しいものが登場しています。それらの中でRevelやMartiniなど古いものを除外した結果、goji(+gorilla), echo, Ginあたりが候補になりました。Ginの評価が高いですが、どれもGoらしいフレームワークで、決定的に秀でたものはありません。

write.koguでは現在、labstack/echoを使用しています。当初はzenazn/gojiを使用していましたが、やや不足を感じていて、試したechoがしっくりきたため移行しました。コンテキスト周りが柔軟で、きちんとGAEを考慮している点も気に入っています。

echoはver.2になって、valyala/fasthttpへの対応など大きな見直しが行われました(LabStack Echo v2 Released -)。とはいえ、移行の解説を含めたサンプル付きのドキュメントがそれなりにあるので、対応は簡単です。echo.Context.Form()がecho.Context.FormValue()に変わるとか、echo.HeaderContentTypeのように定数名が変わるとか、荒削りが理由の変更は面倒でしたが、今回の見直しで今後は安定するはずです。

Datastoreto TOC

記事などのデータ永続化には全て、Datastore(Cloud Datastore)を使用しています。CDN併用のブログ風CMSにスケールアウトは大して必要ありませんが、GAEでは、やはりまずDatastoreを活用すべきです。

DatastoreはBigtableをベースにしつつ、Bigtableには無いクエリやトランザクションなどを追加したKVSです。RDBMSのような正規化がむしろ悪手だったり、そもそもリレーションシップによるモデリングがそぐわなかったりします。KVSに触れたことがあっても、EntityGroup, Key, Indexなどに混乱するかも知れません。また、一貫性に”Strong”だの”Eventual”だのと、RDBMSを小さな用途で使っていれば隠蔽されていたような概念も突き付けられます。こういった壁はあっても、フルマネージドで凄まじくスケーラブルなKVSが、大きな無料枠付きで使えるのはとても魅力的です。

Datastoreの操作には、全てmjibson/goonというライブラリを使用しています。goonはDatastoreのラッパーであり、GetしたEntityをmemcacheサービスに自動でキャッシュしてくれます。また冗長な記述も省けるため、GoからこのCMSの他でも、Datastoreを操作する際には必ずgoonを使っています。

goonがどのように楽をさせてくれるかは、以下の記事にまとまっています。

モデルto TOC

現時点では、記事、ファイル、タグを取り扱っています。

DatastoreではKeyの設計が重要ですが、記事やファイルにはKeyNameとして、単にランダムな文字列を採用しています。ただしUUIDでは冗長なため、URLセーフな6桁の文字列をランダムに生成して使っています。Keyはユニークである必要がありますが、その衝突は比較的低コストで検出できるため、新規の割り当て時には衝突しない文字列が出来るまで繰り返すようにしています。なおタグのKeyNameは、タグ名そのものを使用しています。

記事のモデルが最も大きく複雑です。このサイトは、じっくり読める長い記事をじっくり書いて提供しようと始めたため、記事のデータ量がDatastoreの単一Entityのサイズ上限1MBを超える可能性があります。そこで記事の本文はstringではなく、gzipで圧縮したbyteとして保存しています。入出力時にModelで自動的に圧縮や展開を行うのため扱いは楽ですし、そもそもDatastoreでは長いstringをクエリで扱えず、検索したければSearchサービスの利用が必要なため、特に不便はありません。ただしCloud Consoleなどで中身が直接確認できなくなるデメリットはあります。

タグによる記事の抽出は、記事側にstringのリストを持たせているだけで済ませています。リストはクエリで簡単に拾い上げられるので、いわゆるタグ機能のような実装は簡単です。ただしタグ側から見た該当記事件数は、Datastoreの一貫性の問題で取得するタイミングの制御が必要です。Datastoreでクエリを実現しているのは、Entityが持つ値を列記したIndexであり、このIndexの更新がEntityの更新と同時に行われる保証が無いためです。そのため記事保存と同時にタグによるクエリを実行した場合、誤った件数が取得される可能性があります。そこでタグの該当記事数取得は、Task Queueを用いて、記事の操作から数秒遅延させています。

後述しますがファイルのEntityは、Google Cloud Storageに置くファイルのメタデータを持つだけのものです。各ファイルの権利をきちんと取り扱えるよう、著作者やライセンスのプロパティを設けている程度です。

Datastoreのモデリングでは、Keyに親子構造を与えて形成するEntity Groupが非常に重要ですが、このCMSではカスタムのEntity Groupを作っていません。Entity Groupの重要性は主に一貫性のコントロールにありますが、ブログ風のCMSに強い一貫性は不要だったためです。Datastoreの一貫性やEntity Groupについては、apps-gcpの、次の記事が非常に分かりやすいです。

Datastoreの仕組み 〜Consistencyについて〜 - apps-gcp

扱うモデルが単純で少ないため、全体は非常に単純です。Filter・Order・Limitを手軽に指定できる汎用的なクエリ関数、ページング用のクエリ関数などは用意していますが、それ以外は基本的なCRUDのみです。

Services(旧Modules)とVersionsto TOC

GAEのServices(旧Modules)は、アプリケーションをモジュールに分けて構成する機能です。この自作CMSでは、記事の投稿などを行うモジュールと、公開されるフロントを別モジュールとしています。もうひとつAPI用のモジュールも分けていますが、現時点では限定的にしか使っていません。

モジュールを分けるメリットは色々あります。モジュール単位に機能を作り、アクセスコントロールやログ、デプロイもその単位で行われます。また各モジュールごとにインスタンスのオプションを指定できるため、単なるコンテンツ配信のフロントは最小性能、画像のリサイズも行うモジュールはメモリの大きい物、といった使い分けもできます。たとえばいわゆる管理画面を持つようなアプリケーションなら、まず分けておいた方が無難です。

Versionsは、デプロイにバージョンを指定できる機能です。指定したバージョンは、同時に復数が有効な状態となり、どれを標準とするか、あるいはどのバージョンにどれだけのトラフィックを振り分けるかが設定できます。これにより、リリース候補のバージョンを”rc”と固定したり、あるいは”v1”, “v2”, “v3”などシリアルなバージョンを順に標準バージョンとするGreen Blue Deploymentなどに使えます。またバージョン間のトラフィックの振り分けを任意の割合にして、ABテストのようなことも行えます。

自作CMSでは主に、Green Blue Deployment用でVersionsを使っています。またVersionsは各Serviceごとに保持できるため、自分たちが使えれば十分な管理画面側のモジュールでは大雑把な指定をし、フロント側のモジュールでは細かく振っています。

これらを組み合わせて、マイクロサービス志向の構成も可能です。以下の公式ドキュメントでは、マイクロサービスを意識したServicesとVersionsの構成が紹介されています。

ビューとテンプレートエンジンto TOC

基本的にサーバーサイドでHTMLのレンダリングまで行っています。クライアントサイドでのレンダリングも一部ありますが、非常に限定的です。フロント側は静的コンテンツ主体でCDNも使うため、そのままサーバーサイドでのレンダリングで良いと考えています。管理画面では、今のところ記事の編集で少しjQuery等を使っている程度です。しかしファイル管理と記事編集の両立など、クライアントサイドでレンダリングした方が扱いやすい箇所も多いので、いずれVueを導入しようか検討しています。

HTMLやXMLをレンダリングするテンプレートエンジンには、flosch/pongo2を使っています。pongo2はGoでDjangoライクな記法をサポートするテンプレートエンジンです。元々Pythonでjinja2、PHPではTwigを常用していたため、記法の習得コストはほぼありませんでした。Goの標準ライブラリにもテンプレートエンジンはありますが、レンダリング対象がXMLやJSでもそのまま使えて、細かい制御も可能なpongo2は重宝します。このCMSでは、sitemapのXML生成や、管理画面用のJSONの生成などにも利用しています。またpongo2はカスタムフィルターにも対応しているため、タイムスタンプの変換等、ビュー用の加工がそれを用いています。

ファイル用ストレージto TOC

記事で使用するファイルは、Google Cloud Storageにアップロードしています。GCSにはGo用のAPIが用意されており、GAE/Goからも、とても簡単に扱えます。

復数ファイルをまとめて扱いやすくしたかったため、ドラッグアンドドロップでプレビューできる簡単なJavaScriptを書いています。これとjQueryを使って、メタデータ用のフォームなども生成し、アップロードを行っています。

アップロードしたファイルは、GAEのインスタンスで受け取り、リサイズとサムネイルの生成を行います。GAE/GoにもImages Go APIという、オンザフライでリサイズや切り抜きを行い、Googleのエッジキャッシュで配信してくれる機能があります。しかしロックインの低減等を考え、外部のCDNで扱いやすいよう自前でリサイズしています。

画像の操作はGoの標準ライブラリでも可能ですが、リサイズのアルゴリズムが豊富で分かりやすいnfnt/resizeを利用しています。ただし画素数の大きな画像の場合、GAEの最も安価で低機能なインスタンスではメモリが不足します。そこでひとつ上のクラスを指定していますが、本来記事の編集だけなら過剰なコストのため、いずれは独立した画像処理用のモジュールへの切り出しを考えています。

ファイルはGCSにアップロードしますが、同時に、権利者やライセンス、タグなどのメタデータをDatastoreに保存しています。特に権利関係をちゃんと扱いたかったため、ライセンスとそのURL、著作権者とそのURLなどを必須としています。

GCSにはこのサイト用のバケットを用意し、独自ドメインを割り当てています。これを外部のCDN(CloudFlare)でキャッシュし、配信しています。

アクセス制御to TOC

管理画面側へのアクセス制御は、完全にGoogleアカウント任せです。

GAEでは他のGCPのアクセス制御同様、Googleアカウントによる管理が可能です。当然あやしいログインの通知や二段階認証などの機能も標準で使えます。これらは自前での実装が非常に手間がかかるため、Googleアカウントに任せてしまえる恩恵は大きいです。またCloud Consoleでの管理が可能なのも嬉しいです。こういった認証周りは、どう頑張っても総合的にGoogleより汎用的且つセキュアに保てないため、なにか具体的な問題がない限り、全てのアプリケーションで管理画面へのアクセス制御にGoogleアカウントを使っています。

GAE/GoでGoogleアカウントによるアクセス制御を行うのは、URLパターンベースであれば非常に簡単です。アプリケーション(モジュール)の設定ファイルの、URLの基本的なマッピングを指定する箇所でオプションを追加するだけで済みます。

デプロイto TOC

GAEへのデプロイは専用のツールで行います。任意のファイルだけを更新したり削除したりはできず、Servicesの個々のモジュールごとに、それぞれの全体が丸ごと差し替えられます。

このCMSの場合、管理画面用、一般公開フロント用、そしてAPI用の3つのモジュールがあります。これらは個別にデプロイも可能ですし、すべてまとめることも可能です。また、Datastoreのインデックス設定や、Taskqueueのキュー設定、リクエストのServiceへの振り分け設定など、アプリケーションに提供されるサービスの設定を更新する処理もあります。

これらのデプロイ作業はコマンドもそう複雑ではありませんが、特に設定の更新などは、いざという時忘れていたりする事もあるので、簡単なスクリプトとして用意しています。モジュール個別または全てのデプロイ、各設定個別または全ての更新、モジュールも設定も全て、といった程度です。

デプロイ後は、主にフロント用のモジュールについて、バージョンを切り替え、Versionsのトラフィック振り分け機能を用いて、Green Blue Deployment風の置き換えを行います。

記事の編集機能to TOC

じっくり書いた長文の提供を目指すサイトであるため、利用時間のほとんどは記事の編集作業です。少しずつですが、自分たちが書きやすいよう手を入れています。

記事編集画面 by kogu CC BY 4.0

記事データのフォーマットto TOC

フォーマットは、Markdownに独自記法を幾つか追加したものを採用しています。Markdown自体は、それがマークダウン系のスタンダードであること、自分が慣れていること、優れたライブラリがあることなどから選んでいます。ただし、表現力の弱い部分もあるため、独自記法で補っています。

Markdownパーサはrussross/blackfridayを使用しています。blackfridayは、Cのvmg/sundownをベースに改変したもので、GoのMarkdownパーサとしては定番です。CommonMark(旧称Common Markdown)にも対応しており、表や脚注、強制改行なども使えます。

独自記法は、主に追加的なコンテンツ用に使用しています。たとえば画像は、Google Cloud Storageにアップし、メタデータはDatastoreで管理しています。メタデータはライセンスや著者の情報なども含み、このメタデータ込みのファイル情報をシンプルに書きたかったため、独自記法を使用しています。他に、目次の挿入箇所や対象とする範囲の指定などにも使用しています。

独自記法はテンプレートエンジンやMarkdownと干渉しないよう”{: :}“をデリミタとしています。今の所規模が小さく、構文解析などを行わず単純な置換で対応していますが、記法の要求が増えてくると苦しくなるため、いずれ置き換えようと考えています。幸いGoには標準ライブラリにパーサもレキサーもあるため、良いサンプルには悩まずに済みそうです。

編集のフロントエンドto TOC

ブラウザのみでMarkdownの編集を完結したかったので、NextStepWebs/simplemde-markdown-editorを採用しています。SimpleMDEはCodeMirrorをベースに、Markdown編集の補助機能を組み込んだライブラリです。カスタマイズも容易で、独自書式のボタンを追加したり、オートセーブに対応したりも比較的簡単です。またベースがCodeMirrorなので、標準で用意されたフックのタイミングも豊富です。編集画面では、SimpleMDEの外から編集中のテキストを触るのに、CodeMirrorのフックが重宝しています。

SimpleMDEはMarkdownを、プレビューやフルスクリーン表示して編集できます。残念ながらまだ、自分たちの独自書式をレンダリングするところまでは対応できていませんが、CommonMarkなMarkdownであれば、ブラウザで十分な編集ができます。

記事に付けるタグは、登録済みのタグを補完してくれつつ、削除なども行いやすいプロダクトを探しました。こういった用途自体が需要が減っているのか、見つかるのはどれも不十分だったり、すでに長くメンテされていないものでした。結局幾つか要求を減らして、Pixabay/jQuery-tagEditorを使用しています。タグの補完は、登録済みの全タグをキャッシュしたjsonで返して対応させています。タグ数が1MBを超えるまでに、クエリから投げ返すよう変更する必要がありますが、当面これで問題ありません。

その他の機能to TOC

ページネーションto TOC

ブログ風のCMSには、何らかのページネーションが必要です。ページネーションはデータストレージに強く影響されるため、RDBMSとは大きく違うDatastoreでは、よくあるページネーションの実現に制限があります。

Datastoreのクエリでは、RDBMSのようにオフセットの指定が可能です。しかし、たとえばオフセットに500を指定すると、501件目からのみを対象としてくれるわけでなく、500件目まで含めて全て取得した上で、オフセット対象を破棄し、501件目以降のみを返すように見せます。結果、オフセットが大きくなればなるほどクエリの負荷が増えていきます。またオフセットには2000という上限があります。

Datastoreによる現実的なページネーションは、クエリカーソルを使用します。この場合、Twitterなどで見られるような「続きを取得」というページネーションになります。

このCMSでは、本来Datastoreに不向きな前者のページネーションを使用しています。記事がそもそも2000件を当面越えないこと、更新頻度が低くCDNでキャッシュすることから、オフセットの制限やコストが問題にならないと考えたためです。しかしいずれ記事数が増えてきたら、全記事のページネーションだけは、クエリカーソルを使用する方法に変更するかもしれません。

CNDの利用to TOC

CDNとしてCloudFlareを無料プランで利用しています。このサイトの場合、フロントは全てのリソースをキャッシュ可能なため、有償プランにある細かな機能は不要です。

GAEでは、GCP共通のエッジキャッシュ(実質Google Cloud CDN)が使えますが、それでもCloudFlareを使っています。理由のひとつはロックインの低減で、GAEが素晴らしいプラットフォームでも、コンテンツはなるべく切り離していたかったためです。記事や画像を全て独自ドメインでCloudFlareに通しています。

もうひとつの理由は攻撃への予防です。GAEにはDoS用のブラックリスト機能や、不正なリクエストでの費用への救済措置もあります。しかしDDoSにはまだ良い対処が無く、CloudFlareはその察知や遮断を提供してくれています。従量制のPaaSは攻撃で費用が跳ね上がりうるため、DNSベースで全自動のCloudFlareのプロテクションはありがたいです。

APIによるキャッシュの自動削除

キャッシュは寿命を短く設定すれば記事の更新をすぐに反映できますが、キャッシュの価値が減ります。長く取り過ぎれば更新がなかなか反映されません。特に修正の対応などでは問題です。結局CloudFlareの管理画面からキャッシュ消去をかけることで逃げていました。

CloudFlareにはキャッシュの制御も行えるAPIがあります。

これを使えば、記事の更新時に、その記事のみキャッシュを削除することができます。しかしつい最近まで手を出していませんでした。その理由は、GAEから外部にHTTPで通信する際に使う、urlfetchというサービスの仕様にあります。

GAE外部へのHTTP通信には、基本的に全てこのurlfetchを使用する必要があります。たとえばGitHubのAPIを使うとか、スクレイピングするとかも、全てが対象です。ところがこのurlfetchには、DELETEメソッドのbodyを勝手に捨てるという困った仕様があります。そしてCloudFlareのAPIでキャッシュを削除するには、bodyを持ったDELETEメソッドが必要です。

RFC上非推奨とはいえ、なにも無条件に捨てなくても…と思いますが、おそらく将来も変わりません。かと言って、CloudFlareがAPIを変えてくれるとも思えず、対応を延ばし延ばしにしていました。そして最近になってようやく、Socketサービスを使って対応しました。

Socketサービスは、支払設定を行ったアプリケーションでしか使えないサービスで、外部にTCPかUDPの通信をsokectで行えます。そのため「うぅ、DELETEだけのためにHTTPなぞるのか…」と手を出しあぐねていました。結果としてこれは勘違いで、たとえば次のようにhttpのTransportにsokectを使って指定してやればよかっただけでした。

r := c.Request().(*standard.Request).Request
apCtx := appengine.NewContext®
client := http.Client{
    Transport: &http.Transport{
        Dial: func(network, addr string) (net.Conn, error) {
            return socket.Dial(apCtx, network, addr)
        },
    },
}
req, err := http.NewRequest(“DELETE”, url, fsb)
resp, err := client.Do(req)

Goのサンプルが異様に少なく(公式のリポジトリすら空っぽ)躊躇しただけで、HTTPならとてもお手軽でした。ということで、GAEからbody付きのDELETEメソッドが必要なら、Socketサービスを使いましょう(HEADも)。

フロントエンドのデバイス対応to TOC

フロントではFoundationを用いてレスポンシブにしています。BootstrapとFoundationはどちらも使っていますが、Bootstrap 4がまだalpaなのと、ちょうどFoundation 6が出ていたというのが採用の理由です。現在はどのライブラリを使っても、おそらく大差ない程度の作り込みなので、Bootsrap 4がリリースされれば乗り換えるかも知れません。

費用to TOC

Googleには申し訳ないのですが、このCMSの運用でかかる純粋なGAEの費用は実質ゼロです。このサイトに記事がまだほとんど無いことも理由ですが、ほぼ同じ構成ではるかに大量のデータとリクエストがあるアプリケーションでも、数年動かして月額の費用が500円を超えたことは一度もありません。最も多かった月の費用ですら、メンテナンスで大量にDatastoreの処理をしたものです。

GAEでCMSを動かし、CDNとしてCloudFlareを使うという構成では、結局GAEで稼働するのが静的サイトジェネレーターと大差ありません。またCDNを通さない管理画面でも、大半を占める記事編集の作業中には通信も無いため、インスタンスの稼働時間は非常に短くなります。PaaSで恐ろしいDDoSなどの攻撃もCloudFlareを挟むため、理不尽な費用が発生を概ね回避できます。

もちろんGAEは他の用途にも使っており、そちらではそれなりの費用がかかるものもあります。Datastoreの苦手な用途ならBigQueryなど他のコストもかかります。しかし同じシステムを他の環境で動かすことや、インフラ管理、スケール対応のコストまで含めれば、素晴らしいコストパフォーマンスです。

まとめto TOC

ほとんどがキャッシュ可能なコンテンツをCDN経由で配るような用途は、GAEのコンセプトをろくに活かせません。ただし太っ腹すぎる無料枠もGAEの魅力であり、その恩恵は凄まじいです。おかげで、ドメイン以外に実費がかからず、自分たちの好きなようにいじれて、インフラ管理の不要な記事配信用の環境を、タダで使っているような状況です。

しかし小さいとは言え、Googleの負荷だけが増えるような状態は不健全です。たくさんの適切な費用を払える用途を、もっと増やしていきたいです。

CMSの自作そのものは、楽しいお勉強であり、GAEの練習として良い課題です。特に趣味プログラマには、無料枠をしゃぶり尽くしながらDatastoreやTaskQueueなどGAEの特徴に触れられるのでおすすめです。そして十分学んだら、ぜひ無料枠に依存しないアプリケーションを作りましょう。

広告