- 棚井龍之介
- 約 6,300 文字
- 2,100 View
目次
Terraform連載2024 の9本目記事です。
はじめに
こんにちは。CSIG(Cyber Security Innovation Group)の棚井です。
Terraform 連載ということで…
- そういえば、実装コードは Go で書かれていたな
- コマンドの使い方はインフラエンジニアの皆様が書いてくれるはずなので、コードリーディングしようかな
との考えに至り、ソースコードリーディング自体をブログ化しました。
参考になる点が1つでもあれば幸いです。
エディタの準備
今回のコードリーディングでは VSCode を利用します。
Go のコードジャンプやテスト実行のため、以下の拡張機能を追加します。
また、コードリーディングのお供として「GitHub Copilot」も追加します。
GitHub アカウントで Copilot を有効化する方法や、VSCode の拡張機能とリンクする方法については、ネット上に多数情報がありますのでそちらをご参照ください(ex. GitHub Copilot のドキュメント)
「コードリーディングで生成系AIを使うの?」という疑問を持たれた方向けへの回答として、GitHub Copilotには「コード生成機能」以外に、「コードの説明機能」があります。
使い方としては、
- 解説して欲しいコードをハイライトする
Ctrl + iによりCopilotのポップアップを表示する/explainを入力する
の3ステップで利用可能です。
VSCodeで表示されている実コードベースで解説してくれますので、途中に詰まる部分があったとしても、Copilotのサポートにより大抵は独力で解決可能です。OSSのコードリーディングでは、まさにこの解説機能が非常に便利だと感じています。
実行環境の準備
コードリーディング中には「実際に動かしてみないと、イメージがつきにくい処理」が見つかります。いざという時にローカル環境で動かせるように、Terraformのビルド、動作検証が可能な環境を準備します。
ソースコードはこちらの hashicorp/terraform リポジトリに公開されています。
Goのバージョンを確認したところ、トップディレクトリ配下の .go-version に 1.22.1(執筆時点)と記載されていることを確認しました。
執筆時点での Go All releases も 1.22.1 なので、最新のGoバージョンに対応していることが分かります。
== 宣伝 ==
FutureではGoリリース連載を実施しております。
Go 1.22リリース連載始まります
Goリリースノートから技術ブログを書く流れ基礎
リポジトリ側でGoのバージョンが指定されているので、ローカル環境もそれに合わせて構築します。
ちなみに、私は asdf を利用して開発言語のバージョンを管理しています。asdf の利用方法は、左記の公式ドキュメント、および、こちらの解説記事(asdf で開発言語と利用ツールのバージョン管理)をご覧ください。
$ go version 実行時に、1.22.1 が表示されていれば、実行環境の準備は完了です。
go version |
リポジトリの取得、テスト実行
それではさっそく、Terraform のソースコードを取得していきます。
といっても、ここでは hashicorp/terraform リポジトリをクローンするだけです。
クローンに成功したら、まずはテストに通過するかを確認します。
テスト実行ログ
cd terraform/ |
テストを実行してみたところ、[no test files] が多数見つかりました。
少し気になりますので、テストのカバレッジを見てみます。
go test -cover |
上記ログには…
coverage: 36.7% of statements
とありますので、Terraform 実装コードのテストカバレッジ率は 36.7% です。
数字の是非はさておいて、テストが通過することは確認できました。
ビルドして動かしてみる
terraform がコマンドの1つである以上、「ビルドして動かせる」はずなので、実際に試してみます。
Go言語では王道の Makefile を見たところ、go build に相当しそうなコマンドは見つかりません。
ただし、いくつかのコマンドが $(CURDIR)/scripts/ 配下のシェルスクリプトを参照していますので、当該ディレクトリにお目当てのファイルがないかを確認します。
ls -l scripts/ |
scripts/ 配下に、build.sh というシェルスクリプトが見つかりました。
また、VSCode で Ctrl + Shift + f を実行して build.sh を検索すると、Dockerfile の中でこのシェルスクリプトが呼ばれていることも確認できます。
シェル冒頭に以下の記載があり、bash によリコールされた後、1つ上のディレクトリでビルドプロセスを動かしていることが分かります。
# Get the parent directory of where this script is. |
上記を踏まえて、さっそくビルドしてみます。
/usr/bin/bash build.sh |
計11個のビルドプロセスが並列で動いています。
このまましばらく放置していれば、11環境分すべての terraform 実行バイナリが作成されるのですが、私のPC環境では以下の問題が発生しました。
リポジトリからクローンしたソースコード全体と、ビルドで生成した実行バイナリのダブルパンチにより、ローカル PC が悲鳴を上げていました。
du -h terraform/bin/terraform |
何とかならないか? と build.sh を読み進めたところ、環境変数 TF_DEV に値を設定すれば、ビルド環境だけの実行バイナリを生成してくれるとありました。
# If its dev mode, only build for ourself |
また、TF_DEV を設定しない場合には、ビルドした実行バイナリのパッケージ化が行われるとの記載も見つかりました。
if [ "${TF_DEV}x" = "x" ]; then |
今回はローカル環境でのみビルド、動作検証ができれば十分ですので、環境変数 TF_DEV を設定し再ビルドします。
export TF_DEV=yes |
無事に、linux/amd64 分のビルドに成功しました。
実行バイナリは、以下2つのディレクトリに出力されています。
- terraform/bin
- GOPATH/bin
./terraform/bin/terraform version |
ここまでの操作により、ソースコードのビルドから、コマンドの実行手順まで確認できました。
続いて、コードに手を加えた場合には、ビルド後のコマンド中身に反映されていることを検証してみます。
サブコマンドの version が分かりやすいので、以下のログを追加します。
(対応箇所)
... |
↓
... |
この1行が追記された状態で実行バイナリをビルドすると、コマンドのログ出力が増えていることを確認できます。
/usr/bin/bash ./terraform/scripts/build.sh |
エントリーポイントから見ていく
ここまでが事前準備です。
さっそく、Terraform の実コードを見ていきます。
まずはプログラムの始まりとなる「エントリーポイント」を探します。
Go であれば…
- main.go
- func main() {…}
がプログラムのエントリーポイントです。
トップディレクトリ配下の「terraform/main.go」に以下の記述が見つかりました。
func main() { |
初手、main() の中で realMain()(直訳すると「本当のmain」)を呼び出しているようです。
呼び出し先の関数を見ると、今度は defer で logging.PanicHandler() を呼び出しているようなので、この関数の中身を見てみます。
func realMain() int { |
VSCode のコードジャンプが有効となっていれば、Ctrl を押しながら対象関数を左クリックすることにより、関数の定義元にジャンプできます。
PanicHandler()
「PanicHandler()の実装」を見ますと、TERRAFORM CRASH という仰々しい言葉が沢山の ! で囲まれていることが分かります。
通常のインフラ構築、運用保守作業にてこのようなメッセージをお目にかかることは、まずないと思います。私は今回、初めてこんなメッセージが仕込まれていることを知りました。
メッセージ内容を日本語訳しますと「Terraform が壊れたよ! 公式リポジトリの issue に記票して」とありますので、さっそく、壊してみます。
panic 発生時に TERRAFORM CRASH が表示されるようなので、エントリーポイントの直後で強制的に panic を起こす1行を入れます。
func realMain() int { |
また、terraform のビルド時に渡される GOFLAGS を「リポジトリのコード」のまま利用した場合、ビルド環境のフルパスが表示されてしまいますので、以下の -trimpath フラグを追加します。
export GOFLAGS="-mod=readonly" |
↓
export GOFLAGS="-mod=readonly -trimpath" |
この状態でソースコードのビルド、及び、コマンドの実行を試してみます。
サブコマンドを与えずに terraform を実行した場合、本来ならば help が表示されますが、無事に「壊す」ことができました。
./terraform |
terraform コマンドの実行時、何らかの理由により panic が起きてしまった場合には、TERRAFORM CRASH のメッセージ表示と issue の起票催促、デバッグトレースが表示されることを確認できました。
それでは、意図的に仕込んだ panic の1行を削除して、再ビルドまで完了したら、次の処理を見ていきます。
openTelemetryInit()
次の実装として、Open Telemetry を扱う処理が見つかります。
var err error |
openTelemetryInit() の定義元にコードジャンプしますと、トップディレクトリ配下の「terraform/telemetry.go」にて詳細内容が説明されています。
まず、実装コードのコメントには以下の記載があります。
// If this environment variable is set to "otlp" when running Terraform CLI |
Terraform の実行環境にて、環境変数として以下を設定した場合のみ、Open Telemetry 機能が有効となるようです。otlp 以外の値(値ナシも含む)が設定された場合には、この機能は有効化されずに関数の呼び出し元へ戻ります。
export OTEL_TRACES_EXPORTER=otlp |
コメントには「OpenTelemetry Protocol Exporter Configuration Options」へのリンクが添付されています。
ただし、Open Telemetry について本記事では立ち入りません。
気になる方は、以下の公式ドキュメント・日本語記事・翻訳書籍をご参照ください。
- 公式ドキュメント
- 日本語記事
- 翻訳書籍
ここでは、実装コードを参照する中で、Terraform には「環境変数の OTEL_TRACES_EXPORTER に otlp を与えることで、Open Telemetry が有効化される」ことが分かりました。
このような知識はもちろん公式ドキュメントを漁れば見つかるのだとは思いますが、自分で探索して見つけたときの「自力で発見できた感覚」を味わえるのが、OSS コードリーディングの面白さだと私は感じております。少々、蛇足に過ぎましたので、元のコードに戻ります。
tmpLogPath
続いて、tmpLogPath という「一時的なログファイルの出力先」になりそうな変数が見つかりました。
適切な変数名は「適切なメンタルモデル」を脳内に作るために重要なので、こういった側面においても、OSS のコードリーディングでは書籍からは得られない実践知が詰っていると感じます。
tmpLogPath := os.Getenv(envTmpLogPath) |
ここの実装では、環境変数の TF_TEMP_LOG_PATH で指定したファイルに、ログを追記する処理が定義されています。
それでは、ログの出力先を指定して、出力ログと実装コードの対応を確認していきます。
# ログの出力先を /tmp/tf.log に指定 |
ログ出力先の環境変数を設定してから任意の terraform コマンドを実行すると、ファイルには以下のログが追記されます。
2024-03-25T05:07:16.800+0900 [INFO] Terraform version: 1.9.0 dev |
これらのログと実装コードの対応を見ますと、TF_TEMP_LOG_PATH を設定した「直後の処理内容」が、そのままログとして格納されていることが分かります。
例えば、version.InterestingDependencies() により取得された「依存モジュールのバージョン情報」は、以下のように記録されています。
2024-03-25T05:07:16.800+0900 [DEBUG] using github.com/hashicorp/go-tfe v1.41.0 |
ログの対応を1つ1つ、実装コードと突き合わせてみますと、以下に対応するログが出力されていないことが分かります。
if ExperimentsAllowed() { |
ExperimentsAllowed() の定義にコードジャンプすると、「terraform/experiments.go」のコメントとして、この関数を有効化する方法(返り値がtrueにする方法)が記載されています。
// experimentsAllowed can be set to any non-empty string using Go linker |
コメントの内容に従うと、terraform のビルド時に…
go install -ldflags="-X 'main.experimentsAllowed=yes'" |
を混ぜ込むことにより、experiments(実験的機能)を有効化できるようです。
「scripts/build.sh」を確認すると、-ldflags に渡される値は以下のように定義されていることが分かります。TF_RELEASE に値を設定した場合のみ、gox -ldflags ""${LD_FLAGS}"" が「ビルド時に追加」されています。
# In release mode we don't want debug information in the binary and we don't |
今回は TF_RELEASE を利用しないので、以下の分岐を追加します。
if [[ -n "${TF_RELEASE}" ]]; then |
この状態で実行バイナリをビルドし、任意の terraform コマンドを実行すると、ログファイルに以下の1文が追記されることを確認できます。
... |
experiments を有効化することにより、何かしらの実験的コマンドが利用可能となったのだと思います。しかし、ここまでのコードリーディングの範囲では「どのような機能が有効化されたのか?」についての情報に遭遇していないため、ExperimentsAllowed() の探索はここまでとします。
terminal.Init()
コードリーディングとしては、ログ後半に注目してみます。
ここでは、ターミナルを初期化しているような terminal.Init() という関数と、その関数の返り値を利用して…
- 標準入力
- 標準出力
- 標準エラー出力
のそれぞれに対して、IsXXX() の方式で「ターミナルであるか、否か」を判断している以下の実装が見つかります。
streams, err := terminal.Init() |
terminal.Init() の定義元にコードジャンプすると、こちらも「長文コメント」で実装内容、実装意図が説明されています。
コマンドの実行環境は terraform が正しく入力・出力を扱える環境なのか、この terminal パッケージ内にて確認処理を行っています。普段、ツールやコマンドを取得する際には Requirements を確認してインストールしますが、コマンドの実行プロセス内においても、実行環境を確認していることが確認できました。
…
本ブログでのコードリーディングは一旦ここまでとします。func main() {...} から読み始め、進捗行数としては70行程度です。ただし、途中でコードジャンプやシェルスクリプトの確認が入ったため、単純に「main() からの進捗行数 = コードリーディング行数」というカウントにはなりません。「実際に動かしながらのコードリーディング」のスピードを実感いただけたでしょうか。
おわりに
本ブログでは、「Terraform の実装コードを、動かしながら読む」という目標を掲げ、実行バイナリのビルドやコードの改造を取り入れながら、OSS のコードリーディングを行いました。前半の環境準備に原稿の多くが割かれているため、実際のコードリーディング行数は100行未満ではないかと思います。OSS コードリーディングの面白さは「各自が、自分の好き勝手に読めること」にあると思いますので、私ならどのように読むか? を詳しく解説してきました。
ここまで長文にお付き合いいただき、ありがとうございました。