TerraformのProviderをシャッとデバッグする
要約
- TerraformのProvider内部処理のデバッグを行うのは結構簡単にできる
何故Providerの処理をデバッグする必要があったのか
きっかけは以下記事 Github repoの
こちらのissueに記載があるとおり
「Terraformの構成をapplyすると、terraformが固まる」現象について調査した事です。
なぜ固まるか、どこで固まっているかが検討がつかなかったので、今回はTerraformのprovider「terraform-provider-helm」にdelveをアタッチして、どの箇所で固まっているのかを調べました。
調査の経緯は大体以下のスクラップに時系列順に記載されています。
結局結論は環境依存の問題でしたが😇
Providerをデバッグするやり方
本題です。
とはいうものの、ありがたい事にすでにProviderの開発者向けやその他の利用者向けに
確立された手順が既に公式ドキュメントに記載されているので、基本的にはそちらの手段に従うだけです。
大きく以下、2つのやり方があります。
- ログを用いたデバッグとCLI設定のオーバーライド
- デバッガを用いたデバッグ
これらの方法の例を、今回実際に使用させて頂いたterraform-provider-helm
を実際にデバッグすることを想定して個別に見ていきます。
前準備
ソフトウェアのインストール
今回の場合
- terraform,delve,makeのインストール
- Goのビルド環境の構築
があらかじめ必要となります。
ソースの準備
今回は実際にHashiCorpが既に実装し、公開してくれているProviderがデバッグ対象なので、まずは手元にソース類を落としてきます。
git clone https://github.com/hashicorp/terraform-provider-helm.git
cd ./terraform-provider-helm
Providerのバイナリをビルドする
移動したディレクトリ内で
make build
を実行するとterraform-provider-helm
という名前の実行ファイルが作成されます。
このバイナリファイルを後の手順で実行する形になります。
また、必要に応じてソースの変更やMakefileの変更を行った後に、再度ビルドします。
ログを用いたデバッグとCLI設定のオーバーライド
誤解を恐れずに言うと、つまりはカスタマイズしたバイナリを用いた、printデバッグ
です。
ログの埋め込み部分に関しては、既にProvider側で用意されているものがあるので,そちらを利用し
その他の手順はこちらに従います。
ここでは、試しに
helm/resource_release.goのgetChart
の該当箇所にログを仕込んで出力を確認する手順を実践してみます。
手順
- 該当箇所にログを仕込みます
こんな感じで出力を足してみます。resource_release.gofunc getChart(d resourceGetter, m *Meta, name string, cpo *action.ChartPathOptions) (*chart.Chart, string, error) { //Load function blows up if accessed concurrently m.Lock() defer m.Unlock() + debug(" === Called getChart") path, err := cpo.LocateChart(name, m.Settings) if err != nil { return nil, "", err }
詳細
- 今回の
terraform-provider-helm
の場合だと、terraform-plugin-sdk/v2を使用しており、
内部ではproviderの起動時にlog経由でloggerの生成と設定が行われています。
内部ではnclogというパッケージが使用されています。
hclog
については、Writing Log Outputにも記載されています。 - そのため,
log.Printf
を介して,terraformCLI本体にログを出力する事ができます。また,既にhelper関数としてdebugが用意されているので、こちらを使用します。
- 今回の
- ログを仕込んだバイナリをビルドします。
- CLI設定のオーバーライドの為の設定を記述します
-
こちらにあるように、~/.terraformrcに以下の設定を記載します。Windowsはこちら参照
provider_installation { dev_overrides { "hashicorp/example" = "/***" #↑でビルドしたバイナリが配置されてあるディレクトリを指定 } direct {} }
-
こちらにあるように、~/.terraformrcに以下の設定を記載します。Windowsはこちら参照
- 最後にterraform CLIを以下コマンド経由で実行します
TF_LOGやTF_LOG_PATHについてはこちらで確認できます。TF_LOG='TRACE';TF_LOG_PATH='./terraform.log'; terraform plan
./terraform.log
のファイルの内容を確認してみると、以下のように仕込んだログが出力されている事が確認できます
~~~
2022-12-03T16:49:12.642+0900 [INFO] provider.terraform-provider-helm: 2022/12/03 16:49:12 [DEBUG] === Called getChart: timestamp=2022-12-03T16:49:12.642+0900
~~~
という形でログの出力を追加して、動作を確認する事ができます。
実際に今回も上記のログを仕込んだ箇所までは、同じようにログを仕込んでどの箇所でスタックしていたのか検討をつける事ができました。
しかし、この直後のcpo.LocateChart
箇所で止まっている事は確認できましたが、それだけではまだよく原因が分かりませんでした。
そして、ここより内部はterraform-provider-helm
の層ではなく、helm
の内部処理になります。
ログを追加する為に、helm側までいじるのは少し大がかりすぎるなと思いました。
そこで、デバッガの出番です
デバッガを用いたデバッグ
こちらもドキュメントに記載があるので
おおよそこちらの手順に従うだけです。Visual Studio Codeで接続する方法についても記載されていますが、そちらは試したところ上手く動かなったので、気が向いたら別記事にでもしようかなと思います。📜
ですので、今回はDelve CLIの手順に従います。
手順
-
Providerのバイナリをビルドする(最適化、インラインOFF)
- こちらに記載がある通り、バイナリの最適化やインライン化を無効化したバイナリにデバッガをアタッチするために、Makefileをいじってビルドします。
- 以下のように
gcflags
を追加します(別のターゲットとして作成しても問題ありません。)
GNUmakefilebuild: fmtcheck - go build -v . + go build -v -gcflags 'all=-N -l' .
-
make build
でビルドします(自分で別のターゲットを作成した場合は、そちらを使用します)
-
バイナリをデバッガ経由で起動する
- 以下のコマンドで、作成したプロバイダーのバイナリを起動します
dlv exec --accept-multiclient --listen=localhost:9999 --continue --headless ./terraform-provider-helm -- -debug
- ほぼ公式ドキュメントそのままですが、ポートだけ固定する為に
--listen=localhost:9999
が追加されています - バイナリ実行後、以下のような出力が得られます。
こちらの内容は後の手順で使用します
API server listening at: 127.0.0.1:9999 {"@level":"debug","@message":"plugin address","@timestamp":"2022-11-29T09:46:16.739246+09:00","address":"/tmp/plugin654492491","network":"unix"}Provider started. To attach Terraform CLI, set the TF_REATTACH_PROVIDERS environment variable with the following: TF_REATTACH_PROVIDERS='{"registry.terraform.io/hashicorp/helm":{"Protocol":"grpc","ProtocolVersion":5,"Pid":15777,"Test":true,"Addr":{"Network":"unix","String":"/tmp/plugin3715822300"}}}'
-
デバッガ(dlv)に別プロンプトから接続し、ブレークポイントを張る
- 以下手順では、必要に応じてdelveのコマンド(例:
break
,next
,step
など)を実行します。コマンド一覧はこちら - 別のプロンプトを用意し、以下のコマンドを実行します
dlv connect localhost:9999
- 接続できると、以下のようにdelveのコマンド入力が可能となります
(dlv)
- この状態で、任意の箇所にまずはブレークポイントを貼ってみます
-
break
コマンド(短縮系:b
)でソースコードの任意のソースの行数やパッケージ、関数名を指定してブレークポイントを張ります - ブレークポイントを張り終えたら、
continue
を実行してバイナリの実行を継続しておきます。
ブレークポイントの張り方の例
- 下記の例の
***
の箇所は単にファイルパスを省略しているだけなので、ご自身の環境に合わせて読み替えてください。 - 任意のソースの行数やパッケージの例
(dlv) b chartrepo.go:123 Breakpoint 1 set at 0x3427ba1 for helm.sh/helm/v3/pkg/repo.(*ChartRepository).DownloadIndexFile() /***/go/pkg/mod/helm.sh/helm/v3@v3.9.4/pkg/repo/chartrepo.go:123
もし、以下のようなエラーがでた場合は
(dlv) b install.go:718 Command failed: Location "install.go:718" ambiguous: /***/go/pkg/mod/helm.sh/helm/v3@v3.9.4/pkg/action/install.go, /***/go/pkg/mod/k8s.io/client-go@v0.24.2/pkg/apis/clientauthentication/install/install.go, /***/go/pkg/mod/k8s.io/kubectl@v0.24.2/pkg/scheme/install.go…
↓このように指定できます
(dlv) b go/pkg/mod/helm.sh/helm/v3@v3.9.4/pkg/action/install.go:718 Breakpoint 1 set at 0x3462878 for helm.sh/helm/v3/pkg/action.(*ChartPathOptions).LocateChart() /***/go/pkg/mod/helm.sh/helm/v3@v3.9.4/pkg/action/install.go:718
- パッケージ、関数名の例
(dlv) b repo.DownloadIndexFile Breakpoint 1 set at 0x3427a92 for helm.sh/helm/v3/pkg/repo.(*ChartRepository).DownloadIndexFile() /***/go/pkg/mod/helm.sh/helm/v3@v3.9.4/pkg/repo/chartrepo.go:118
-
- 以下手順では、必要に応じてdelveのコマンド(例:
-
別のプロンプトでTerraformを実行し、プロバイダーを動作させる
- 別のプロンプトを開きます
- terraformを実行する前に、terraform CLIが起動中のプロバイダーのバイナリと通信する為に必要な環境変数を指定します。それが最初の項目で取得した
TF_REATTACH_PROVIDERS
です。
出力の文字列をそのまま環境変数としてexportします。
export TF_REATTACH_PROVIDERS='{"registry.terraform.io/hashicorp/helm":{"Protocol":"grpc","ProtocolVersion":5,"Pid":15777,"Test":true,"Addr":{"Network":"unix","String":"/tmp/plugin3715822300"}}}'
-
terraform plan
を実行します -
3.で仕掛けたブレークポイント上の処理が実行されると、該当の箇所でバイナリの実行を一時的に中断した状態にできます
- 以下のような出力であれば、ブレークポイント上で処理が止まっているのでコマンドが入力できます。
=>
の箇所で止まっています。
(dlv) continue > helm.sh/helm/v3/pkg/repo.(*ChartRepository).DownloadIndexFile() /***/go/pkg/mod/helm.sh/helm/v3@v3.9.4/pkg/repo/chartrepo.go:118 (hits goroutine(117):1 total:1) (PC: 0x3427a92) 113: }) 114: return nil 115: } 116: 117: // DownloadIndexFile fetches the index from a repository. => 118: func (r *ChartRepository) DownloadIndexFile() (string, error) { 119: parsedURL, err := url.Parse(r.Config.URL) 120: if err != nil { 121: return "", err 122: } 123: parsedURL.RawPath = path.Join(parsedURL.RawPath, "index.yaml") (dlv)
- この状態でさらに各種コマンドを打ち込み詳細にデバッグを行っていきます。
-
next
コマンド(短縮系:n
)で、処理を1行ずつ実行できます。 -
step
コマンド(短縮系:s
)で、現在停止しているカーソル内の関数の中に潜っていく事が可能です。 -
print
コマンド(短縮系:p
)で、現在のスコープ内に存在する変数の値を確認する事ができます。 - さらにほかのブレークコマンドを
break
コマンドで追加指定したり、別のコマンドを使用する事も可能です。
- 以下のような出力であれば、ブレークポイント上で処理が止まっているのでコマンドが入力できます。
デバッグの結果
結果,以下の箇所で毎回スタックしている事が判明します。
で、この箇所が判明した事をきっかけに調査が進行して、原因が判明するわけですが...
そちらについては別記事で分割して記載します。
😉「すぐみたい、まとめて見たい方はスクラップでご覧ください」
所感
TerraformのCore部分とPlugin部分(Providerの実装)が切り離され、独立して動作するように設計されているからこそ、こういったデバッグができるのかぁと感心しました。
😁
「デバッグしてバグつぶすぞ~」
😡
「誰だ!こんなバグ埋め込んだの・・・コミット履歴見よ」
😶
「・・・」
😶🧔
「・・・ま、まさか・・・」
Discussion