• 作成:

ゲーム販売webアプリケーションSYAKERAKEを支える技術, HaskellとYesodで作られています

Haskell (その3) Advent Calendar 2017 - Qiitaの3日目の記事です.

この前「Haskellで書かれたwebサービスって何がある?」と聞かれて, HackageとかStackageのようなHaskellに関連したサービスぐらいしかパッと出せませんでした.

なので, webアプリケーションであるSYAKERAKEがHaskell製であることと, これを構成するライブラリなどを書いていこうと思います. SYAKERAKEがどういうサービスかはサイトを見ていってください. 半分このサービスの宣伝です. お許しください.

この記事を読むことで, 小規模ながらもプロダクションレベルのwebアプリケーションがHaskellで作れるということがわかっていただけると幸いです. 特に実際の周辺環境を書いた記事はあまり無いと思います.

ただ, SYAKERAKEはクローズドソースなので, 現在ソースコードを全公開することは出来ません. 我々してはOSSを推進していて, 実際切り出して公開したOSSは存在するのですが, 雑に作っていった結果公開できない範囲をリポジトリに含んでしまったので全公開は出来ません. 公開していないソースコードを元に技術を語ることをお許しください.

SYAKERAKEの開発ストーリー, なぜHaskellが選ばれたか

SYAKERAKEは私の2年先輩の人達が, 専修大学ネットワーク情報学部の2014年プロジェクトとして開発していたwebアプリケーションでした. これは未完成のまま, 所有権は起業によって作られたSyake株式会社に移りました.

ちなみにSyake株式会社の公式webサイトもHaskellとHakyllで作られており(私が私のサイト(ここ)からコードをコピーしたりもしました), このソースコードは公開されています. Dan344/www.syake.co.jp: Syake株式会社 公式HP

そして, 専修大学に在学中の社長が大学内でM先生にwebプログラミングが出来る人を紹介してもらおうとした結果, 私が紹介されました.

SYAKERAKEは2014年プロジェクトではRuby on Railsで作られていました. これは専修大学ネットワーク情報学部ネットワークシステムコースの応用演習がRailsで行われているので, それに合わせた形となっていました.

私はこれを改造して作り直すよりは, 仕様などを参考にしつつ新規に作成した方が早いと決断しました. 当時Railsがあまり好きではなかったという感情も影響しましたが, 決定的な理由は他にあります, 語りません.

それで1から作り直そうとしたわけですが. 暫くはこのアプリケーション開発に関われるのは私1人ということを伝えられました. ならば私の最も好きな言語を使って良いと判断して, Haskellを使うことにしました.

それで, 私がSYAKERAKEに関わり始めたのは2015年8月頃からです. その当時, 私は今から見てもwebプログラミングのずぶの素人だったので, 2015年は独学しながら暗中模索していました. 前提にしていた認証システムのPersona - Mozilla | MDNの終了は悲しかったですね.

2016年は私が学生生活を続けながら開発が出来るように, SYAKERAKEを専修大学の2016年プロジェクトとして開発していました.

2017年現在は私の卒業演習としてSYAKERAKEを開発しています.

2017年の前期は私が就活のストレスで履修登録時倒れてその後復活したので学校に週1回しか行かなかったのと, 社長のゲーム開発が一段落してSYAKERAKEの開発に関われるようになったことで, 開発が進みリリースに至りました. プレイ後に購入額を決めるゲーム販売サイト「SYAKERAKE」をαリリース

2017年の後期, つまり今は, 割と私が授業が多くて忙しく, 開発は停滞しています.

トポロジー図または構成技術ポンチ絵

卒業演習の中間発表のために作ったポンチ絵を少しだけ修正してここに貼り付けておきます.

よく会社が「こんな技術を使っています」と使っている技術のアイコンをペタペタ貼っているような奴です. アレなんて言うんでしょうね? トポロジー図? アーキテクチャーダイアグラム? ネットワーク図作成に使えるアイコン集 - Qiita

SYAKERAKEのアーキテクチャーダイアグラム

ものすごい細かい図はhaskell-import-graphという自作ツールで作って加工しました.

フルスタックwebフレームワークのYesodを使っています

webフレームワークにはYesodを選択しています.

Yesodだったのは, 当時(2015年)のHaskellのフルスタックwebフレームワークはYesodぐらいしか有名ではなかったからです. 私がRailsのようなフルスタックなwebフレームワークで開発することしか知らず, APIを切り離して開発する技法を知らなかったという事情もあります.

Yesodの評判は人によって善し悪しですが, 今考えてもこの選択は間違っていなかったと思います.

HaskellのwebフレームワークはWeb/Frameworks - HaskellWikiにまとまっています.

Scotty はシンプルすぎて機能が足りてないように思います. もっと大規模に開発メンバーが居ればシンプルなのはむしろメリットになるかもしれませんが, 少人数での開発ではフルスタックフレームワークが魅力的に感じます.

Spock はかなり魅力的ですが, 当時は有名では無かったですし, 型レベルルーティングの機能が弱いです. ただ, GHCJSのサポートは魅力的ですね. 実用的かどうかはともかく.

Servant も当時は有名では無かったですし, 今でもこの最小限のサーバーサイドだけのフレームワークで高速な開発が出来るかは疑問です. しかしかなり魅力的ではあるので, オーバーエンジニアリングであることをわかりつつも, Servant+PureScriptという体制で一度開発してみたいなとは思っています.

脱線しました, ともかくYesodは悪くない選択肢でした.

採用した当初はStackが存在しなかったため, 依存関係が壊れることにいつも悩まされていましたが, それは他のフレームワークも同じですし, 今はStackがあるので問題になっていません.

ただ, hamletを変更するたびに重たいコンパイルが走ることだけは困りものです. その代わり, 変数の名前ミスやルーティングのミスからプログラマを守ってくれるわけですが. これはトレードオフの関係にあります.

私はおっちょこちょいなのでYesodのコンパイル時チェックにはたいへん助けられました. しかし, 文章の多いaboutページを書いたりすると少しの文章修正で多くのビルド時間を要求されてイライラするようですね. モジュールの一部の修正だけでもリンクには同じ時間がかかるため, 最小時間が長いのです.

これを書いてて気がついて調べたのですが, 最近はインストールされている場合goldやlldがデフォルトで使われるようになったようです.

GHC now tries to use the gold and lld linkers by default. These linkers are significantly faster than the BFD linker implementation that most Linux distributions use by default. If gold or lld are not available GHC will use the system's default linker. GHC can be forced to use the default linker by passing --disable-ld-override to configure.

Blog: GHC 8.2.1 is available – GHC

今度時間を計測してみましょう.

ちなみにYesodで作られたサイトはPowered by Yesod · yesodweb/yesod Wikiにまとまっています. 今SYAKERAKEも追加しました.

クラウドサービスの選択

SYAKERAKEはAWS上で動いています. Syake株式会社名義で既にRoute53経由で所有しているドメインが存在したこと, 2014年プロジェクトではサンプルアプリケーションをEC2上で動かしていたことなどが影響していますが, 一番の決め手はPostgreSQLのサポートです.

Yesodが提供しているDBアクセスライブラリである Persistent はMySQLよりPostgreSQLのサポートが手厚いです. 更に私としてもPostgreSQLの方をMySQL(MariaDB)より気に入っていたので, そちらを使いたいという気持ちがありました.

Google Cloud PlatformはCloud SQLでPostgreSQLをサポートしていなかったので, 候補から外れました. 今は使えるようですが, ベータ版です. Google Cloud SQL for PostgreSQL ドキュメント  |  Cloud SQL for PostgreSQL  |  Google Cloud Platform

Microsoft Azureは検討をしていませんでした. やはりどうしてもWindowsのイメージがあるので無意識に外していたようです. PostgreSQLは使えるようですが, プレビュー版のようです. Azure Database for PostgreSQL – 完全管理型サービス | Microsoft Azure

IBM Cloudですが, パブリッククラウドの存在を認知すらしていませんでした. 今「サービス情報システム」の講義でやたらと講師が推しているので一応調べてみました. IBM Compose for PostgreSQL - 概要 - 日本一応あるんですかね…? IBM Cloudの名前で提供しているのはDb2でPostgreSQLどころかMySQLすら無いようですが. 問い合わせないと料金がわからないってこれ本当にプライベートクラウドなんですか? 何か私は勘違いしています? IBM製品は問い合わせないと詳細すらわからないものが多すぎて, 全く魅力がわかりません.

一時期はオンプレミス(自宅サーバ)も考えました. 私はLGBTPZNの人権を守ることを考えているため, AWSからBANされることを危惧したためです. しかし, サーバが24時間動くことが保証しづらいため, それは諦めました. もしBANされたらmstdn.jpのようにさくらインターネットを頼るつもりです.

AWSとの連携

というわけでAWS上でSYAKERAKEは動いているので, AWSのサービスと連携しなければいけません. とは言ってもEC2とRDSとS3ぐらいしか使っておらず, S3ぐらいしか特別に対応しないといけないものは無いのですが. 当初はS3のバケットをgoofysでマウントしてファイルシステムとして使っていたのですが, パフォーマンスが低下しているのでは? という疑問からS3はS3として扱うことにしました. 今では私が小手先でプログラミングするよりgoofysに任せておいた方がパフォーマンスが良かったのでは? と疑っています. 工数が圧倒的に足りていないです.

それはともかく, AWSのAPIを利用するためにawsパッケージを使っています. amazonkaというパッケージもあるのですが, こちらはamazonka-s3がLensを前提としていたので敬遠しました.

ドキュメント変換にPandocを使用

Haskell利用者以外にも有名なPandocですが, SYAKERAKEでも制限されたMarkdownをHTMLに変換するために利用しています. 他の言語だとFFIとか気にしたりコマンドを動かしたりしないといけないところ, SYAKERAKEはネイティブHaskellなので, ネイティブに内部関数を呼び出すことが可能です. Haskellの利用するかなりの利点だと思います.

Stripeの利用

決済システムにはStripeを利用しています. 自前でクレジットカードデータを保存したりする怖いことはやりません.

この間言ったら驚かれたのですが, Haskellにもstripe-coreというStripe用のライブラリが存在します.

メールはGmailでまだ問題ない

ユーザ登録時のメールやサポートにはG SuiteのGmailをnullmailer経由で使っています.

サポートはGmailで来た問い合わせがGoogle Groupで閲覧出来るようになっています.

送信上限が存在するため, 大量にユーザ登録が来たら破綻しますが, 破綻したら嬉しい!

webpackの利用

あまりHaskellとは関係ないのですが, 一部の多少複雑な画面を制御するために, TypeScriptとReactを利用しています. TypeScriptはwebpackを利用してtemplates内でtsxをjsに変換しています. こうするとYesodでも問題なくTypeScriptが利用できます. shakespeareにもText.TypeScriptモジュールが存在するのですが, これはセットアップが面倒な上, エラーメッセージがわかりにくいので採用しませんでした. それでは変数やルーティングをどう渡しているのかと言うと, hamlet側にdata-project=@{ProjectR projectId ProjectHomeR}のように要素の属性として埋め込んでいます. こういうことをしているとServantにしてJSON APIベースでシステムを組みたくなってきますが, こういう制御が必要なのは一部の画面だけなのでYesodの方が現状開発速度は高いです.

また, CSSを多く書けるほど時間が取れないため, Bootstrapを利用しています. 4.0.0-alpha.5の頃からBootstrap v4を利用していました. Bootstrapの基本色設定などを変更するために, luciusではなくscssで統一したスタイルシートを使用して, Bootstrapをビルドしています.

これらをビルドするためのwebpack.config.jsは以下のようになっています.

const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = [{
    entry: {
        "project-edit": "./templates/project-edit.tsx",
        "stripe-checkout-form": "./templates/stripe-checkout-form.tsx"
    },
    output: {
        filename: "[name].js",
        path: __dirname + "/templates"
    },
    resolve: { extensions: [".ts", ".tsx", ".js", ".json"] },
    module: {
        rules: [{
            test: /\.tsx?$/,
            use: "awesome-typescript-loader"
        }]},
    externals: {
        "js-cookie": "Cookies",
        "react": "React",
        "react-dom": "ReactDOM"
    },
}, {
    entry: {
        "bootstrap": "./static/bootstrap.scss"
    },
    output: {
        filename: "[name].css",
        path: __dirname + "/static"
    },
    module: {
        rules: [{
            test: /\.scss$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: "css-loader!sass-loader"
            })
        }]
    },
    plugins: [new ExtractTextPlugin("[name].css")]
}];

また, ビルド前にwebpackでこれらをビルドするために, Setup.hsに以下のように書いてビルド前にhookをかけています.

import           Distribution.PackageDescription
import           Distribution.Simple
import           Distribution.Simple.Setup
import           System.Process

main :: IO ()
main = defaultMainWithHooks simpleUserHooks
    { preBuild = preBuildSyakerake
    }

preBuildSyakerake :: Args -> BuildFlags -> IO HookedBuildInfo
preBuildSyakerake _ _ = callProcess "yarn" ["run", "build"] >> return emptyHookedBuildInfo

テスト

テストにはyesod-testの機構を使っています. JavaScriptを動かせるヘッドレスブラウザを動かすのはうまくいかなかったのでやめました. TypeScriptも型に守られているのでAPIが動くことだけテストしています.

コード管理

GitHubでコード管理して問題があればIssue建てて新しいコードはPull Requestで作って互いに問題がないことをコードレビューで確かめてMergeしています.

継続的インテグレーション

Travis CIを使っています.

.travis.ymlを一部公開すると以下のようになっています. これでstack環境でテストが可能です.

sudo: false

cache:
  timeout: 1000
  yarn: true
  directories:
    - $HOME/.local
    - $HOME/.stack

language: node_js
node_js: "8"

services: postgresql

addons:
  apt:
    packages:
      - libgmp-dev
      - libpam-cracklib
      - nullmailer

before_install:
  - mkdir -p ~/.local/bin
  - export PATH=$PATH:$HOME/.local/bin
  - hash stack || travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
  - stack setup

install:
  - stack --jobs 2 --no-terminal test --only-dependencies
  - stack install hlint
  - yarn
  - createuser -U postgres syakerake
  - createdb -U postgres -O syakerake syakerake_test

script:
  - hlint src
  - stack --jobs 2 --no-terminal test

デプロイ

Docker, Ansible, Chefと言ったものは使っていません. 依存ライブラリをインストールして, 単一のコードベースでEC2上で動くので, 過剰と判断しました. 一応インストール手順はメモしていますが, それぐらいです.

git clone(次回からはgit pull)してstack buildして多少のセットアップをしてsudo systemctl restart syakerake.serviceするという原始的な手段を取っています.

Yesodのデプロイ手段として公式に推奨されているKeterがあります. Deploying your Webapp :: Yesod Web Framework Book- Version 1.4 これを私は使っていません.

HTTPSの解決手段としてはLet’s Encryptとnginxを使っています.

一時期はKeterに移行することを考えていて, 未だにKeter移行用のブランチが残っています. しかし, 実際にステージング環境に投入してみたところ, Warpが直接2GBサイズのファイルを受け付けるとクラッシュすることが判明したため, 移行は凍結されました.

Keterの利点は, サーバでビルドしなくて済むということです. しかし, EC2のインスタンスタイプをt2.mediumにしていれば, 十分ビルドできるので無問題と判断しました. Herokuとか使っている人は頑張ってください.

EC2上のディストリビューションがUbuntuになっているのに深い意味は無いです. Debian系でEC2が公式サポートしているのがUbuntuだっただけです. Amazon Linuxは辛すぎる. systemdを使わせてくれ, systemdは嫌いだけど好きなんです.

副産物としてのOSS

副産物として以下のOSSが産まれました. もっと切り分けて公開していきたいです.

まとめ

長い記事となり, Haskellと関係ない話題も多く含まれました. しかし, これでHaskellで商用webアプリケーションが構築可能であることを, 実際の周辺環境を含めて解説しました.

もちろんSYAKERAKEには技術的に至らぬ部分も多く存在しており, 例えばCloudFrontを未だ導入できていない所などが挙げられます. しかし, これはHaskellは特に関係なく, 単に私に時間が無いだけです. ゲームを売る前に私自身もゲームがしたいのです.

SYAKERAKEをよろしくお願いします.