とある自社サービスのパッケージングの話

前の記事から大幅に時間をあけてしまった@mazgiです。

弊社ではDSPサービスSphereの他に Sphere Paper という紙広告をつくるWebサービスを運営しています。
Webの管理画面から画像をアップロードしたりテキストを入力すると紙広告の原稿ができあがるというちょっと面白いWebサービスです。
また配布時には独自のエリアターゲティング技術を活用しています。

このSphere Paperは開発コードネームFSSというScala製のいくつかのアプリケーションで構成されさくらのクラウド上で運用されています。
今回はこのプロダクトのシステム構成とパッケージング+deployについてご紹介します。
あと本番サービスでGentoo Linuxを使っている日本の会社の一員としてGentooの話を書きます!

サーバーについて

サーバーは基本となる構成を作ってさくらのクラウドの機能で複製します。
複製して起動するとすでに下記のような構成で動く状態になっています。
(サーバー構成についてはこちらの記事にも少し書いていますのであわせてご覧ください。)

仕事で扱うサーバーは物理/クラウド両方ですが、やっぱりクラウドサーバーは環境の複製が楽ですね!
さくらのクラウド快適です♪

アプリケーションとシステム構成について

FSSのシステム構成はこのようになっています。

f:id:maverick-techblog:20190801141455p:plain

またFSSを構成するアプリケーションは下記の3つです。

WebView

いわゆる管理画面のView側です。
Single Page Applicationとして作られており、広告の入稿や管理・紙広告原稿のプレビュー等が行えます。
弊社のフロントエンド開発環境としては標準的なSlim, Sass, CoffeeScript, AngularJSで実装されています。
deployに関連する点としてビルドにMiddlemanを使用するためRubyが必要です。

WebAPI

いわゆる管理画面のWebAPI側です。
Scalaで開発されており、WebViewからのリクエストを受けデータのCRUDを行い、結果をJSONや画像で返します。
Play FrameworkSkinny-ORMを使用して実装されています。
パッケージング時にはsbtを使用します。実行には当然ながらJVMが必要です。

RenderingServer

入稿データから紙広告の原稿を生成します。
こちらもScalaで開発されており、WebAPIと連携してRGB→CMYK変換や組版処理(!)を行い結果を保存します。
WebAPIと同じくパッケージング時にsbtを使用します。
実行にはJVMはもちろんですがImageMagickも必要です。

使用ミドルウェア

ミドルウェア/ツール/実行環境として下記を使います。

  • nginx
    • WebViewおよびWebAPIのフロントで使います。
  • JDK
    • WebAPI, RenderingServerともに1.8を使います。
  • sbt
    • WebAPI, RenderingServerともに0.13.xを使います。
  • Ruby
    • WebViewのビルドに使います。
    • 以前は2.1以上を使っていました。
    • あるタイミングから2.2以上を使うようにしました。
  • ImageMagick
    • RenderingServerで使います。
    • インストール時のオプションでLittle CMSに対応するようにします。
  • MariaDB
    • 原稿データ以外の永続化に使います。
  • MongoDB
    • 原稿データの永続化に使います。
    • レプリケーションおよびシャーディングしています。

パッケージングとdeployについて

各種ミドルウェアや実行環境の構築は大抵のLinuxディストリビューションであれば、パッケージ管理システムが整備されているおかげでそれほど苦になりません。
パッケージ管理システムの流儀に法って yum installapt-get installemerge すればインストールできますし、 /etc の中に設定ファイルが配置され大抵は少し編集すれば動作します。
ディストリビューションのメンテナーの皆さまのおかげですね。

一方、自社アプリケーションのdeployはどうでしょうか?
deployを繰り返す中で「必要なJDK/JREのバージョンがインストールされてる?」「このPull Requestがマージされると必要なRubyのバージョンが2.1から2.2に上がるけど大丈夫?」「ImageMagickをインストールするときにLittle CMS対応忘れてた!」「今回から設定ファイル増えたんだけど」などなど...色々な問題が待ち受けています。

ディストリビューションで提供されるパッケージのように、作ったアプリケーションも
「インストール/アップデート時に依存関係を解決してくれればいいのに!」
そして 「デフォルト設定や雛形が自動的に配置されればいいのに!」
...そう思いませんか?
私はずっとそう思っていました。

そもそもディストリビューションで提供されるパッケージと自分たちが開発しているアプリケーションのインストール方法やアップデート方法が違うことに意義はあるのでしょうか?
そう、自分たちで開発しているアプリケーションも パッケージングしてしまえば良い ではないですか!

しかし1日にいくつものcommitが生み出される自社アプリケーションでCIが通ってmasterにブランチがマージされるたびにパッケージを作るには工夫が必要です。
FSSでは全サーバーがGentoo LinuxということもありPortageパッケージ管理システムを使ってアプリケーションパッケージを作成しています。

Portageのパッケージはebuildという形式で作成しますがその実態はインストール/アップデートの手順を書いたBashスクリプトです。Portageのコマンドであるemergeでパッケージをインストール/アップデートすると、ebuildに従ってソースコードが取得されコンパイルされ配置されます。
もし例えるならrpmdebのようなバイナリパッケージが料理そのものだとしたら、ebuildはレシピ+材料の在り処といった感じでしょうか。すぐ食べることができるのはrpm, debですが、ebuildは調理の手間はかかるものの状況に合わせてアレルギーがある食材を抜いたり旬のものを追加したりできるというイメージです。

またebuildではGit(Hub)の特定のブランチや特定のcommitを参照しインストール/アップデートすることもできます。
パッケージの依存関係として"Little CMS対応のImageMagickがインストールされていること"という記述もできます。
理想的なパッケージングの仕組みに思えてきましたね!?

ebuildに興味を持っていただけたところで(?)弊社で作っているebuildをご紹介します。
もしPortageで自社アプリケーションをdeployされる際の参考になれば幸いです!!

なお抜粋ですので全行を載せているわけではありません。また各サーバーで自家製ebuildを取得するためにはパッケージリポジトリも必要です。
全行を載せたサンプルやパッケージリポジトリについては以前書いた記事およびこちらのパッケージリポジトリにありますのでぜひご参照ください。

前提

弊社で開発しているWebView, WebAPI, RenderingServer各アプリケーションのebuildをご紹介します。
3アプリケーションともにソースコードはすべてGitHub.comで管理していますので、ソースコードの取得元を定義するEGIT_REPO_URIは弊社のGitHubリポジトリのGit URLを指定しています。

またebuildは前述のとおり拡張されたBashスクリプトこちらのドキュメントに書かれているように決められたfunctionが決められた順番で呼び出されます。 function call order

WebViewのebuild

ではWebViewのebuildです。

DEPENDでインストール時(ビルド時)の依存関係を記述します。RubyBundlerNode.jsに依存していることを示し、さらにRubyは2.2以上が必要なことを、Node.jsはnpmが必要なことも記載しています。
このように細かい依存関係が指定できるのはebuildのメリットですね。
RDEPEND(Runtime Dependencies)で実行時の依存関係を記述できますが、このWebViewアプリケーションは特に依存しているパッケージがありませんので記載していません。

「パッケージインストール時にコンパイルが必須なGentoo(Portage)でビルド時と実行時の依存関係分ける必要あるの?」と思われるかもしれませんが、じつはPortageにはバイナリパッケージを扱う仕組みがあります。今回は触れませんがバイナリパッケージを活用するとパッケージ構成の柔軟性を保ったままインストール/アップデートにかかる時間とコンピュータリソースを大幅に節約できます。

webapp-configというWeb(View)アプリケーションのインストーラを継承してそちらの機能に頼っていますので他のebuildよりシンプルになっています。

EAPI=5
inherit git-2 versionator webapp
EGIT_REPO_URI="git@github.com:???/???.git"

DEPEND=">=dev-lang/ruby-2.2
virtual/rubygems
dev-ruby/bundler
net-libs/nodejs[npm]"
RDEPEND=""

src_compile() {
    cd "${S}/sources/${APP_PROJECT}/default" || die
    bundle install --path=vendor/bundle || die
}

src_install() {
    webapp_src_preinst

    insinto "${MY_HTDOCSDIR#${EPREFIX}}/../default"
    doins -r "${S}/sources/${APP_PROJECT}/build/default"

    webapp_src_install
}

WebAPIのebuild

つぎにWebAPIのebuildです。

DEPENDでインストール時(ビルド時)の依存関係を記述しているのはWebViewと一緒ですが、Scalaアプリケーションなので依存がJDK 1.8以上とsbt 0.13以上な点が異なります。
またsbtパッケージはオフィシャルにも提供されているのですが、合わない部分があったので現在は自前のパッケージを使っています。
RDEPENDJDKではなくJREにしているのですが、実際にはOracle JDKをインストールしてしまっています。
今気づいたのですがWebAPIもImageMagickに依存していますね。ただLittle CMS対応は必須ではありません。

pkg_setup()でアプリケーション用のシステムユーザーを作っています。もちろんAnsibleのような構成管理ツールでもユーザー作成はできるのですが、個人的にはアプリケーションで必要となるユーザーはアプリケーションパッケージ側で記述する方が見通しが良いと思っています。
src_compile()ではsbt stageを実行してScalaソースコードコンパイルしています。
src_install()で必要なディレクトリを作成しsbt stageで生成されたバイナリを配置しています。また起動スクリプトや設定ファイルの配置も行います。

EAPI=5
inherit user git-2 versionator
EGIT_REPO_URI="git@github.com:???/???.git"

DEPEND=">=dev-java/sbt-bin-0.13
>=virtual/jdk-1.8"
RDEPEND=">=virtual/jre-1.8
media-gfx/imagemagick[jpeg,png]"

APP_DIR="/var/lib/${PN}"

pkg_setup() {
    enewgroup fss || die
    enewuser fss -1 -1 "${APP_DIR}" fss || die
}

src_compile() {
    rev=$(git rev-parse HEAD) || die
    cd "sources/${APP_PROJECT}" || die
    sbt clean stage || die
}

src_install() {
    keepdir "/var/log/${PN}" "/etc/${PN}" || die
    keepdir "${APP_DIR}/versions/${PN}.${PV}.${rev}" || die

    local stage="sources/${APP_PROJECT}/target/universal/stage"
    insinto "${APP_DIR}/versions/${PN}.${PV}.${rev}"
    doins -r "${stage}/bin" || die
    doins -r "${stage}/lib" || die

    newinitd "${FILESDIR}/${PN}.init2" "${PN}" || die
    newconfd "${FILESDIR}/${PN}.confd" "${PN}" || die
    cp "${FILESDIR}/application.conf.example" "${ED}/etc/${PN}/" || die "Cannot copy example application.conf"
    sed -e 's!<PLACEHOLDER_LOG_DIR>!/var/log/'${PN}'!' "${FILESDIR}/logger.xml" > "${ED}/etc/${PN}/logger.xml" || die

    if use symlink; then
        dosym "${APP_DIR}/versions/${PN}.${PV}.${rev}" "${APP_DIR}/versions/current" || ewarn 'Exist symlink!'
    fi

    fowners -R fss:fss "${APP_DIR}" "/var/log/${PN}" || die
    fperms 0755 "${APP_DIR}/versions/${PN}.${PV}.${rev}/bin/${DIST_NAME}" || die
}

RenderingServerのebuild

最後にRenderingServerのebuildです。

DEPENDはWebAPIと同じです。
RDEPENDでは組版のためにImageMagickGhostscriptへの依存関係を追加しています。こちらのImageMagickはWebAPIと違いLittle CMS対応が必要です。

pkg_setup()ではWebAPIと同じくアプリケーション用のシステムユーザーを作っています。
src_compile()でsbtによるコンパイルを行うところはWebAPIと同じですが、こちらのアプリケーションはsbt assemblyを実行します。
src_install()もWebAPIと同じく必要なディレクトリを作成しsbt assemblyで生成されたjarを配置しています。またこちらも起動スクリプトや設定ファイルの配置も行います。

EAPI=5
inherit user git-2 versionator
EGIT_REPO_URI="git@github.com:???/???.git"

DEPEND=">=dev-java/sbt-bin-0.13
>=virtual/jdk-1.8"
RDEPEND=">=virtual/jre-1.8
media-gfx/imagemagick[jpeg,truetype,postscript,png,lcms]
>=app-text/ghostscript-gpl-9.15"

APP_DIR="/var/lib/${PN}"

pkg_setup() {
    enewgroup fss || die
    enewuser fss -1 -1 "${APP_DIR}" fss || die
}

src_compile() {
    rev=$(git rev-parse HEAD) || die
    cd "sources/${APP_PROJECT}" && sbt clean assembly || die
}

src_install() {
    keepdir "/var/log/${PN}" "/etc/${PN}" || die
    keepdir "${APP_DIR}/versions/${PN}.${PV}.${rev}" || die

    local target="sources/${APP_PROJECT}/target/scala-2.11"
    insinto "${APP_DIR}/versions/${PN}.${PV}.${rev}"
    doins "${target}/${DIST_NAME}-assembly-${DIST_VERSION}.jar" || die

    newinitd "${FILESDIR}/${PN}.init2" "${PN}" || die
    newconfd "${FILESDIR}/${PN}.confd" "${PN}" || die

    if use symlink; then
        dosym "${APP_DIR}/versions/${PN}.${PV}.${rev}" "${APP_DIR}/versions/current" || ewarn 'Exist symlink!'
    fi

    fowners -R fss:fss "${APP_DIR}" "/var/log/${PN}" || die
}

おわりに

以上、弊社サービスのパッケージングとdeployのご紹介でした!
ところで弊社では一緒にebuildを書いてくださるエンジニアさんを募集しています!!
ご興味ありましたらぜひTwitter, 勉強会等でお声がけください!!!