先月、固定 IP アドレス機能を提供する QuotaGuard Static Add-on の Buildpack を作りました。

Buildpack を自作したのは今回が初めてです。

Add-on のドキュメント の通りにインストールすると、バイナリファイルをリポジトリに commit することになります。あまりきれいな方法に思えなかったことが、これらの Buildpack を作った動機です。

Buildpack を作ってみて、それがどのようにインストールされるか興味が湧いたので、詳しく調べてみました。

前準備

今回は Getting Started on Heroku with Node.js のサンプルアプリを使って確認しました。

Heroku App を作成したら、Buildpack に heroku-buildpack-qgsocksify の check ブランチ を指定しつつ、環境変数 HOGE をセットしておきます。

$ git clone https://github.com/heroku/node-js-getting-started.git
$ cd node-js-getting-started
$ heroku create
$ git push heroku master
$ heroku buildpacks:add 'https://github.com/masutaka/heroku-buildpack-qgsocksify#check'
$ heroku config:set HOGE=aaa

Buildpack をインストールする

適当な commit をして 2 回目の git push を行うと、heroku-buildpack-qgsocksify のインストールが始まると同時に、check ブランチに仕込んだログがダラダラと出力されました。出力多めです。読み飛ばしても OK です。

remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
remote:
remote: NPM_CONFIG_LOGLEVEL=error
remote: NODE_ENV=production
remote: NODE_MODULES_CACHE=true
remote: NODE_VERBOSE=false
remote:
remote: -----> Installing binaries
remote: engines.node (package.json): 10.x
remote: engines.npm (package.json): unspecified (use default)
remote:
remote: Resolving node version 10.x...
remote: Downloading and installing node 10.15.3...
remote: Using default npm version: 6.4.1
remote:
remote: -----> Restoring cache
remote: - node_modules
remote:
remote: -----> Installing dependencies
remote: Installing node modules (package.json)
remote: audited 233 packages in 1.411s
remote: found 0 vulnerabilities
remote:
remote:
remote: -----> Build
remote:
remote: -----> Caching build
remote: - node_modules
remote:
remote: -----> Pruning devDependencies
remote: removed 75 packages and audited 122 packages in 1.456s
remote: found 0 vulnerabilities
remote:
remote:
remote: -----> Build succeeded!
remote: -----> qgsocksify app detected
remote: -> Downloading qgsocksify... (https://s3.amazonaws.com/quotaguard/quotaguard-socksify-latest.tar.gz)
remote: pwd: /app/tmp/buildpacks/7c5e42d020854a726da281f9febed936c9d7051605dfa836df6b6091f19712beaf921429a7778d44215ea42db515c7066823f5ad87a54aa569aff75babfb73f8
remote: total 24
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:07 ./
remote: drwx------ 12 u49561 dyno 4096 Apr 26 15:07 ../
remote: drwx--x--x 2 u49561 dyno 4096 Apr 26 15:07 bin/
remote: drwx------ 8 u49561 dyno 4096 Apr 26 15:07 .git/
remote: -rw------- 1 u49561 dyno 1071 Apr 26 15:07 LICENSE
remote: -rw------- 1 u49561 dyno 874 Apr 26 15:07 README.md
remote: BUILD_DIR: /tmp/build_aed6ee4b7da0bd979d010dd5136c164d
remote: total 96
remote: drwx------ 7 u49561 dyno 4096 Apr 26 15:07 ./
remote: drwx------ 9 u49561 dyno 4096 Apr 26 15:07 ../
remote: -rw------- 1 u49561 dyno 333 Apr 26 15:07 app.json
remote: -rw------- 1 u49561 dyno 8 Apr 26 15:07 .env
remote: -rw------- 1 u49561 dyno 133 Apr 26 15:07 .gitignore
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:07 .heroku/
remote: -rw------- 1 u49561 dyno 358 Apr 26 15:07 index.js
remote: drwx------ 52 u49561 dyno 4096 Apr 26 15:07 node_modules/
remote: -rw------- 1 u49561 dyno 589 Apr 26 15:07 package.json
remote: -rw------- 1 u49561 dyno 35399 Apr 26 15:07 package-lock.json
remote: -rw------- 1 u49561 dyno 19 Apr 26 15:07 Procfile
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:07 .profile.d/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:07 public/
remote: -rw------- 1 u49561 dyno 1365 Apr 26 15:07 README.md
remote: -rw------- 1 u49561 dyno 825 Apr 26 15:07 test.js
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:07 views/
remote: CACHE_DIR: /app/tmp/cache
remote: total 16
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:07 ./
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:07 ../
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:07 build-data/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:07 node/
remote: $ ls -alF /app/tmp/cache/build-data
remote: total 16
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:07 ./
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:07 ../
remote: -rw------- 1 u49561 dyno 805 Apr 26 15:07 nodejs
remote: -rw------- 1 u49561 dyno 810 Apr 26 15:07 nodejs-prev
remote: $ ls -alF /app/tmp/cache/node
remote: total 16
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:07 ./
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:07 ../
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:07 cache/
remote: -rw------- 1 u49561 dyno 40 Apr 26 15:07 signature
remote: $ ls -alF /app/tmp/cache/node/cache
remote: total 12
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:07 ./
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:07 ../
remote: drwx------ 126 u49561 dyno 4096 Apr 26 15:06 node_modules/
remote: $ ls -alF /app/tmp/cache/node/cache/node_modules
remote: total 504
remote: drwx------ 126 u49561 dyno 4096 Apr 26 15:06 ./
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:07 ../
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 accepts/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 ajv/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 array-flatten/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 asn1/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 assert-plus/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 asynckit/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 aws4/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 aws-sign2/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 balanced-match/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 bcrypt-pbkdf/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 .bin/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 body-parser/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 brace-expansion/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 bytes/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 caseless/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 combined-stream/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 concat-map/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 content-disposition/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 content-type/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 cookie/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 cookie-signature/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 core-util-is/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 dashdash/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 debug/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 deep-equal/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 defined/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 define-properties/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 delayed-stream/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 depd/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 destroy/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 ecc-jsbn/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 ee-first/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 ejs/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 encodeurl/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 es-abstract/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 escape-html/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 es-to-primitive/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 etag/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 express/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 extend/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 extsprintf/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 fast-deep-equal/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 fast-json-stable-stringify/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 finalhandler/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 for-each/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 forever-agent/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 form-data/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 forwarded/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 fresh/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 fs.realpath/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 function-bind/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 getpass/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 glob/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 har-schema/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 har-validator/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 has/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 has-symbols/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 http-errors/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 http-signature/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 iconv-lite/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 inflight/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 inherits/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 ipaddr.js/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 is-callable/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 is-date-object/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 is-regex/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 isstream/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 is-symbol/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 is-typedarray/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 jsbn/
remote: drwx------ 9 u49561 dyno 4096 Apr 26 15:06 json-schema/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 json-schema-traverse/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 json-stringify-safe/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 jsprim/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 media-typer/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 merge-descriptors/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 methods/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 mime/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 mime-db/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 mime-types/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 minimatch/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 minimist/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 ms/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 negotiator/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 oauth-sign/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 object-inspect/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 object-keys/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 once/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 on-finished/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 parseurl/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 path-is-absolute/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 path-parse/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 path-to-regexp/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 performance-now/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 proxy-addr/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 psl/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 punycode/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 qs/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 range-parser/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 raw-body/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 request/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 resolve/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 resumer/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 safe-buffer/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 safer-buffer/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 send/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 serve-static/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 setprototypeof/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 sshpk/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 statuses/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 string.prototype.trim/
remote: drwx------ 6 u49561 dyno 4096 Apr 26 15:06 tape/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 through/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 tough-cookie/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 tunnel-agent/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 tweetnacl/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 type-is/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 unpipe/
remote: drwx------ 5 u49561 dyno 4096 Apr 26 15:06 uri-js/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 utils-merge/
remote: drwx------ 4 u49561 dyno 4096 Apr 26 15:06 uuid/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 vary/
remote: drwx------ 3 u49561 dyno 4096 Apr 26 15:06 verror/
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:06 wrappy/
remote: $ cat /app/tmp/cache/build-data/nodejs
remote:
remote: buildpack=nodejs
remote: node-package-manager=npm
remote: has-node-lock-file=false
remote: stack=heroku-18
remote: build-uuid=d58881e9-ed06-4fd0-9c2a-7676ebcfdb15
remote: app-uuid=1ac36792-914c-4bdd-bf37-e5e385192025
remote: checked-in-node-modules=false
remote: node-version-request=10.x
remote: npm-version-request=
remote: yarn-version-request=
remote: node-version-request=10.x
remote: npm-version-request=
remote: yarn-version-request=
remote: install-node-binary-time=1.005
remote: install-node-binary-memory=3
remote: install-npm-binary-time=0.324
remote: install-npm-binary-memory=2
remote: node-version=v10.15.3
remote: npm-version=6.4.1
remote: cache-status=valid
remote: build-script=
remote: postinstall-script=
remote: heroku-prebuild-script=
remote: heroku-postbuild-script=
remote: npm-install-time=2.004
remote: npm-install-memory=92
remote: node-custom-cache-dirs=false
remote: npm-prune-time=2.104
remote: npm-prune-memory=93
remote: skipped-prune=false
remote: node-modules-size=2764
remote: node-build-success=true
remote: build-time=7.447
remote: $ cat /app/tmp/cache/build-data/nodejs-prev
remote:
remote: buildpack=nodejs
remote: node-package-manager=npm
remote: has-node-lock-file=false
remote: stack=heroku-18
remote: build-uuid=fccde9d2-6fda-47ef-9001-24f807dbb55b
remote: app-uuid=1ac36792-914c-4bdd-bf37-e5e385192025
remote: checked-in-node-modules=false
remote: node-version-request=10.x
remote: npm-version-request=
remote: yarn-version-request=
remote: node-version-request=10.x
remote: npm-version-request=
remote: yarn-version-request=
remote: install-node-binary-time=1.009
remote: install-node-binary-memory=3
remote: install-npm-binary-time=0.322
remote: install-npm-binary-memory=2
remote: node-version=v10.15.3
remote: npm-version=6.4.1
remote: cache-status=not-found
remote: build-script=
remote: postinstall-script=
remote: heroku-prebuild-script=
remote: heroku-postbuild-script=
remote: npm-install-time=4.882
remote: npm-install-memory=125
remote: node-custom-cache-dirs=false
remote: npm-prune-time=2.085
remote: npm-prune-memory=93
remote: skipped-prune=false
remote: node-modules-size=2764
remote: node-build-success=true
remote: build-time=9.595
remote: $ cat /app/tmp/cache/node/signature
remote: v2; heroku-18; v10.15.3; 6.4.1; ; false
remote: ENV_DIR: /tmp/d20190426-60-1hbiilw
remote: total 12
remote: drwx------ 2 u49561 dyno 4096 Apr 26 15:07 ./
remote: drwx------ 9 u49561 dyno 4096 Apr 26 15:07 ../
remote: -rw------- 1 u49561 dyno 3 Apr 26 15:07 HOGE
remote: $ cat /tmp/d20190426-60-1hbiilw/HOGE
remote: aaa -> Installing qgsocksify...
remote: DONE
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing...
remote: Done: 18.5M
remote: -----> Launching...
remote: Released v5
remote: https://obscure-wildwood-20309.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/obscure-wildwood-20309.git
97e6c72..19b7138 master -> master
view raw git-push.log hosted with ❤ by GitHub

Buildpack の作り方

解説の前に Buildpack の作り方を話しておきましょう。

今回使用した bin/compile を見ると分かりますが、この実行ファイルがあればとりあえず動きます。

第1引数 BUILD_DIR にファイルを作れば、それが Buildpack の生成物となります。割と簡単です。

第2引数 CACHE_DIR には Node.js なら、前回のリリースで作られたキャッシュ、今回は Node.js 関連の metadata や node_modules などが含まれていました。

第3引数 ENV_DIR には 1 つの環境変数が 1 つのファイルとして格納されています。

さらなる情報はドキュメント をどうぞ。

分かったこと

何回か試してみて、分かったことをまとめます。

作業ディレクトリは /app/tmp/buildpacks/{128 byte のハッシュ文字列} で、同じ Heroku App なら毎回同じようだ。
例)/app/tmp/buildpacks/7c5e42d020854a726da281f9febed936c9d7051605dfa836df6b6091f19712beaf921429a7778d44215ea42db515c7066823f5ad87a54aa569aff75babfb73f8

BUILD_DIR は /tmp/build_{32 byte のハッシュ文字列} で、毎回異なるようだ。
例)/tmp/build_aed6ee4b7da0bd979d010dd5136c164d

CACHE_DIR は /app/tmp/cache で毎回同じようだ。

ENV_DIR は /tmp/d{YYYYMMDD}-{2 桁の数字}-{6 byte のハッシュ文字列} で、毎回異なるようだ。
例)/tmp/d20190426-60-1hbiilw

まさに [2018-12-21-1] で説明した、Slug compiler の処理そのものという認識です。

303 行目 Compressing… の生成物が Slug で、305 行目 Launching… で、その Slug を元にして Dyno が起動しています。

まとめ

Heroku Buildpack がどのようにインストールされるのかを調べました。

インストール時のディレクトリ構造を知ることで、Heroku Buildpack を身近に感じられましたし、以前調べた Slug compiler と繋がりました。

おまけ

個人的には QuotaGuard Static 推しですが、世間的には Proximo が知られているようなので、こちらの Buildpack も作ってみました。

https://github.com/masutaka/heroku-buildpack-proximo

QuotaGuard Static のニッチな使い方は以下の記事をどうぞ。Proximo との比較も書きました。