Heroku CLIの動きを観察する

Herokuは[CLI](https://devcenter.heroku.com/articles/heroku-cli)でもちゃんと利用できます。でも、CLIがうまく動かなかったり、同様の動作を、直接[Platform APIを叩いて](https://www.utakata.work/entry/2019/12/06/000000)、プログラムから実行したくなることもありますよね。そんな時は、CLIの動作を眺めてみるといろいろとヒントが得られるかもしれません。

この記事は[Heroku Advent Calendar 2019](https://qiita.com/advent-calendar/2019/heroku)の8日目の記事です。7日目はさえきさんによる「[見積から開発・運用まで！Herokuの基本とTips](https://qiita.com/yonyonsaeki/items/eb57278061c9930ca206)」でした。9日目は、「[Herokuのdynoは何コアか](https://qiita.com/zunda/items/acf33c53b30da5349b0b)」です。すみません！連投になっちゃった。

この記事の内容は2019年12月時点のものです。Heroku CLIの内部構成や非公開のAPIなどは予告なく変更になる可能性があります。

## Heroku CLIとプラグイン
現在のHeroku CLIはNode.JSで書かれたCLIフレームワークである[oclif](https://github.com/oclif/oclif)で構築されていて、プラグインを書きやすく、配布しやすくなっています。逆に言うと、CLIのそれぞれの動作がどのファイルで定義されているのか、コードを読むだけではわかりづらく、実際に動作する様子を観察して見たほうがわかりやすい場合があります。(ご指摘いただいて追記) プラグインを書いてみたくなったら、Dev Center記事[Developing CLI Plugins](https://devcenter.heroku.com/articles/developing-cli-plugins)を読んでみてください！

## うまく動かない例
今回はone-off dynoに接続できない問題を例にして、Heroku CLIのどこで何が起きているのか観察してみましょう。

```sh
$ heroku run bash
Running bash on ⬢ app-name... !
 ▸    ETIMEDOUT: connect ETIMEDOUT 50.19.103.36:5000
```

## 環境変数を設定して動作を観察する
上記の例では、エラーメッセージから、[Herokuのエンドポイントのポート5000に接続できない](https://devcenter.heroku.com/articles/one-off-dynos#timeout-awaiting-process)ことが原因とわかりますが、Heroku CLIはどのようにこの接続先を認識してるのかな？

そんな時には、環境変数`DEBUG`を設定して、Heroku CLIを起動してみましょう。下記のように、実行ファイルの場所、プラグインのパス、クレデンシャルのパス、プラットフォームAPIとのやりとり、さらには、one-off dynoとセッションを接続するために必要なURLが表示されます。(ご指摘いただいて追記) Dev Center記事[Developing CLI Plugins](https://devcenter.heroku.com/articles/developing-cli-plugins#debugging-tips)より、`HEROKU_DEBUG=1`でプラットフォームAPIとのやりとりの概略、`HEROKU_DEBUG_HEADERS=1`でプラットフォームAPIとのリクエスト・レスポンスヘッダを表示させることもできます。

```sh
$ DEBUG=* heroku run bash
/usr/local/Cellar/heroku/7.24.1/lib/client/bin/heroku run bash
HEROKU_BINPATH=/usr/local/Cellar/heroku/7.24.1/lib/client/bin/heroku /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/bin/node /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/bin/run run bash
  @oclif/config reading core plugin /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0 +0ms
  @oclif/config loadJSON /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/package.json +0ms
  @oclif/config loadJSON /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/oclif.manifest.json +3ms
  @oclif/config:heroku using manifest from /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/oclif.manifest.json +0ms
  @oclif/config reading user plugins pjson /Users/zunda/.local/share/heroku/package.json +0ms
  @oclif/config loadJSON /Users/zunda/.local/share/heroku/package.json +3ms
  @oclif/config loading plugins [
  { name: 'heroku-repo', tag: 'latest', type: 'user' },
  { name: 'heroku-pg-extras', tag: 'latest', type: 'user' },
  { name: 'heroku-builds', tag: 'latest', type: 'user' },
  { name: 'heroku-slugs', tag: 'latest', type: 'user' },
  { 中略 }
] +1ms
  @oclif/config loadJSON /Users/zunda/.local/share/heroku/package.json/package.json +5ms
  :
  @oclif/config reading user plugin /Users/zunda/.local/share/heroku/node_modules/heroku-repo +0ms
  @oclif/config loadJSON /Users/zunda/.local/share/heroku/node_modules/heroku-repo/package.json +1ms
  :
  @oclif/config loading plugins [
  '@oclif/plugin-legacy',
  '@heroku-cli/plugin-addons-v5',
  中略
] +25ms
  @oclif/config reading core plugin /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/node_modules/@oclif/plugin-legacy +0ms
  @oclif/config loadJSON /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/node_modules/@oclif/plugin-legacy/package.json +4ms
  :
  @oclif/config init hook done +1s
  heroku init version: @oclif/command@1.5.18 argv: [ 'run', 'bash' ] +0ms
  @oclif/config runCommand run [ 'bash' ] +5ms
  @oclif/config:@heroku-cli/plugin-run-v5 require /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/node_modules/@heroku-cli/plugin-run-v5/commands/run.js +7ms
  @oclif/config start prerun hook +27ms
  heroku:heroku:hooks:prerun start /usr/local/Cellar/heroku/7.24.1/lib/client/7.35.0/lib/hooks/prerun/analytics +0ms
  netrc-parser load /Users/zunda/.netrc +0ms
  http --> POST /apps/app-name/dynos +0ms
--> POST /apps/app-name/dynos
  http
  http     accept=application/vnd.heroku+json; version=3
  http     content-type=application/json
  http     user-agent=heroku/7.35.0 darwin-x64 node-v12.13.0
  http     range=id ..; max=1000
  http     host=api.heroku.com
  http     authorization=REDACTED +0ms
--> {"command":"bash","attach":true,"env":{"TERM":"xterm-256color","COLUMNS":80,"LINES":25}}
Running bash on ⬢ app-name... ⣽
<-- 201 Created
  http <-- POST /apps/app-name/dynos
  http {"attach_url":"rendezvous://rendezvous.runtime.heroku.com:5000/省略","command":"bash","created_at":"2019-12-07T19:33:22Z","id":"省略","name":"run.8399","app":{"id":"省略","name":"app-name"},"release":{"id":"省略","version":123},"size":"Hobby","state":"starting","type":"run","updated_at":"2019-12-07T19:33:22Z"} +780ms
  http
  http     cache-control=private, no-cache
  http     content-length=474
  http     content-type=application/json
  http     date=Sat, 07 Dec 2019 19:33:22 GMT
  http     oauth-scope=global
  http     oauth-scope-accepted=global write write-protected
  http     ratelimit-multiplier=1
  http     ratelimit-remaining=4499
  http     request-id=省略
  http     vary=Accept-Encoding
  http     via=1.1 spaces-router (d458a6f05c96), 1.1 spaces-router (d458a6f05c96)
  http     x-content-type-options=nosniff
  http     x-runtime=0.194371404 +779ms
<-- {"attach_url":"rendezvous://rendezvous.runtime.heroku.com:5000/省略","command":"bash","created_at":"2019-12-07T19:33:22Z","id":"6f38da4c-bb1d-4053-9c11-0ef19cedb4ee","name":"run.8399","app":{"id":"省略","name":"app-name"},"release":{"id":"省略","version":123},"sizeRunning bash on ⬢ app-name... !
  heroku:run Error: connect ETIMEDOUT 50.19.103.36:5000
  heroku:run     at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1128:14) +0ms
 ▸    ETIMEDOUT: connect ETIMEDOUT 50.19.103.36:5000
Error: connect ETIMEDOUT 50.19.103.36:5000
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1128:14)
```

それでは、Heroku CLIで/を、Happy Hacking!