s1r-Jの技術ブログ

とあるSEの技術ブログ

JenkinsのPipeline Best Practicesを邦訳したので後で読み直す

最近Jenkinsを使うようになり、Jenkinsfileの書き方などをちょっと勉強するようになりました。

Pipeline Best Practicesという有用そうな記事を後で読み直したいので日本語訳して残します。もう少し勉強して記事の内容がわかるようになったら、もう一度読み直すつもりです。


Pipeline Best Practices

このガイドはいくつかのパイプラインのベストプラクティスを示し、最もありがちな間違いを指摘します。

ゴールは、パイプラインの作成者およびメンテナーがより良いパイプラインの実行となるパターンを目指し、知らず識らずに嵌ってしまう落とし穴を避けるようにすることです。このガイドは考えられるすべてのパイプラインのベストプラクティスを網羅したリストではなく、一般的な手法を見出すために有用ないくつかの具体的な事例を紹介します。一般的に「これをおこなう」ということを知るために利用し、驚くほど詳細な「ハウツー」としては利用しないでください。

一般

パイプラインの接着剤としてGroovyコードを使用する

パイプラインの主たる機能としてではなく、一連のアクションをつなげるためにGroovyコードを利用してください。言い換えると、ビルドプロセスを進めるためにパイプラインの機能性(Groovyやパイプラインステップ)に依存するのではなく、ビルドの複数のパートを完了させるためにシングルステップ(shのような)を使ってください。複雑性の増したパイプライン(Groovyコードの量、使用されるステップ数など)は、コントローラにより多くのコンピューターリソース(CPU、メモリ、ストレージ)を必要とします。ビルドのコアではなく、ビルドを完了させるためのツールとしてパイプラインを捉えてください。

例:単一のMavenビルドステップを使い、build/test/deployプロセスのビルドを進める

パイプラインで複雑なGroovyコードを避ける

パイプラインのため、Groovyコードは常にコントローラで実行され、コントローラのリソース(メモリおよびCPU)を使用します。それゆえ、パイプラインで実行されるGroovyコードの量(パイプラインにインポートされるクラスで呼び出されるメソッドを含む)を減らすことは非常に重要です。以下は、利用を避けるべき最も一般的なGroovyメソッドの例になります:

  1. JsonSlurper:この機能(およびXmlSlurperやreadFileのような類似した機能)はディスク上のファイルを読み取り、ファイルのデータをJSONオブジェクトに変換し、JsonSlurper().parseText(readFile("$LOCAL_FILE"))のようなコマンドを利用してパイプラインにオブジェクトを注入します。このコマンドはコントローラのメモリにローカルファイルを2回ロードします。ファイルが非常に大きい場合やコマンドが頻繁に実行される場合、大量のメモリが必要となります。
    1. ソリューション:JsonSlurperを使用する代わりに、シェルステップを使用して標準出力を返してください。このシェルは以下のようになります:def JsonReturn = sh label: '', returnStdout: true, script: 'echo "$LOCAL_FILE"| jq "$PARSING_QUERY"'。これはファイルの読み取りのためにエージェントのリソースを使い、$PARSING_QUERYはファイルをより小さいサイズに変換することに役立ちます。
  2. HttpRequest:このコマンドは外部ソースからデータを取得し、変数に格納するためによく利用されます。このプラクティスは理想的ではありません。コントローラから直接リクエストが発行される(コントローラに証明書が存在しない場合、HTTPリクエストに対して正しくない結果が返ってくる)だけでなく、リクエストに対するレスポンスが2回格納されます。
    1. ソリューション:エージェントからHTTPリクエストを実行させるためにシェルステップ、必要に応じてcurlwgetのようなツールを使用します。結果がパイプラインの後半で必要な場合、エージェント側で可能な限り結果をフィルダリングし、最小限の必須情報をJenkinsコントローラに返すべきです

類似したパイプラインステップの繰り返しを減らす

パイプラインの複数のステップを単一のステップに結合することは、パイプラインの実行エンジン自体のオーバーヘッドを減らすためにしばしば利用されます。例えば、連続して3つのシェルステップを実行する場合、ステップごとに開始と停止をしなければならず、作成と片付けのためにはエージェントとコントローラのコネクションとリソースが必要となります。しかし、すべてのコマンドを単一のシェルステップに入れる場合、たった1つのステップを開始および停止することが必要です。

例:echoshステップの作成を避け、それらを1つのステップまたはスクリプトに結合する。

Jenkins.getInstanceの呼び出しを避ける

パイプラインや共有ライブラリでJenkins.instanceやそのアクセッサメソッドを利用することは、パイプラインや共有ライブラリ内でのコードの誤用を表します。サンドボックス化されていない共有ライブラリからのJenkins APIの呼び出しは、その共有ライブラリが共有ライブラリであると同時にある種のJenkinsプラグインであることを意味します。パイプラインでのJenkins APIの呼び出しは重大なセキュリティ問題およびパフォーマンス問題を避けるため、非常に注意深くなる必要があります。ビルド時にJenkins APIを利用しなければならない場合、推奨アプローチはパイプラインのステップAPIを利用し、アクセスしたいJenkins APIを安全なラッパーでくるむように実装したJavaの小さいプラグインを作成することです。サンドボックス化されたJenkinsfileからJenkins APIを直接使用するということは、パイプラインを編集できる人によってサンドボックス保護をバイパスできるようなメソッドをホワイトリストに登録する必要があることを意味します。これは重大なセキュリティリスクです。ホワイトリストに登録されたメソッドは管理者権限を持つシステムユーザによって実行され、開発者が意図しているよりも大きな権限で実行されることになります。

ソリューション:ベストソリューションは呼び出しをおこなわないことですが、実行する必要がある場合には必要なデータを収集できるJenkinsプラグインを実装することを推奨します。

共有ライブラリを利用する

組み込みパイプラインステップをオーバーライドしない

可能な限り、カスタマイズ/上書きされたパイプラインステップを避けるべきです。組み込みパイプラインステップのオーバーライドは、shtimeoutのような標準パイプラインAPIを上書きするような共有ライブラリを利用する処理を指します。パイプラインAPIはいつでも変更できるため、この処理は危険です。カスタムコードが壊れたり、期待と異なる結果をもたらしたりします。パイプラインAPIの変更によってカスタムコードが破壊されたとき、カスタムコードが変更されてなくともAPIの変更後に同じように動作するわけではないため、トラブルシューティングが難しくなります。カスタムコードが変更されなかった場合でさえ、API更新後に同じように動作するわけではありません。最後に、パイプライン全体でこれらのステップが広く使われているため、何かが正しく実装されていない/非効率に実装されている場合にJenkinsに壊滅的な結果をもたらします。

巨大なグローバル変数宣言ファイルを避ける

巨大な変数宣言ファイルを持つことは、メリットがわずかもしくは全くない代わりに変数の必要有無に関わらずすべてのパイプラインにファイルがロードされるため膨大なメモリを必要とします。現在のジョブに関連する変数だけを含んだ小さな変数ファイルを作成することを推奨します。

非常に巨大な共有ライブラリを避ける

パイプラインで巨大な共有ライブラリを利用することは、パイプラインを開始する前に非常に巨大なファイルをチェックアウトし、実行中のジョブごとに同じ共有ライブラリをロードすることを必要とします。これはメモリオーバーヘッドを増やし、実行時間を遅くすることになります。

追加のFAQへの回答

パイプラインでの並列処理

複数のパイプライン実行または複数の異なるパイプライン同士でワークスペースを共有しないようにしてください。このプラクティスは、各パイプラインまたはワークスペースでの改名を含む予期しないファイル変更を引き起こす可能性があります。

理想的には、共有ボリュームやディスクは別の場所にマウントされ、その場所から現在のワークスペースにファイルがコピーされます。そして、ビルドが完了すると、更新があった場合にファイルをコピーして戻すことができます。

必要なリソースをスクラッチで作成する個別のコンテナでビルドすべきです(クラウドタイプのエージェントはこれに最適です)。これらのコンテナをビルドすることは、いつでもビルド処理を開始し、簡単に繰り返せることを保証します。コンテナのビルドが機能しない場合、パイプラインで同時実行を無効にするか、Lockable Resourcesプラグインを使用し、実行時にワークスペースをロックし、ロック中は他のビルドがワークスペースを利用できないようにしてください。注意:同時実行を無効にしたり、実行中にワークスペースをロックしたりすることは任意のリソースがロックされている場合、リソースの待機中にパイプラインがロックされます。

また、これらの両方の手法はジョブごとに一意のリソースを利用するよりもビルド結果が出るまでに時間がかかることになります。

NotSerializableExceptionを避ける

パイプラインコードはCPS変換されるため、パイプラインはJenkins再起動後に再開することができます。パイプラインがスクリプトを実行している間、Jenkinsをシャットダウンしたり、エージェントとの接続を切断したりすることができます。再開したとき、Jenkinsは何を実行していたのかを覚えており、パイプラインのスクリプトは中断されなかったかのように処理を再開できます。「continuation-passing style(CPS)」実行として知られるテクニックはパイプラインの再開で重要な役割を果たします。しかし、いくつかのGroovy表現はCPS変換の結果として正しく動作しません。

内部的には、CPSはパイプラインの現在の状態とともに実行予定のパイプラインの残りがシリアライズ化可能であることに依存します。これはシリアライズ化できないオブジェクトをパイプラインで利用することは、パイプラインが状態を永続化しようとするとき、NotSerializableExceptionをスローさせることに意味します。

詳細な情報と問題となりうるいくつかの例は、Pipeline CPS method mismatchesを御覧ください。

以下では期待するようにパイプラインが機能することを保証するテクニックを紹介します。

永続変数がシリアライズ可能であることを保証する

ローカル変数は、シリアライズ化中にパイプラインの状態の一部としてキャプチャされます。これは、パイプライン実行に変数内のシリアライズ化できないオブジェクトを保存する際にNotSerializableExceptionがスローされることを意味します。

変数にシリアライズ不可オブジェクトを割り当てない

シリアライズ化できないオブジェクトを使う手法の1つは、値を計算して変数に格納する代わりに常に「ジャストインタイム」で値を推測させることです。

@NonCPSを利用する

必要であれば、CPS変換されると正しく実行できなくなる特定のメソッドのために@NonCPSアノテーションを利用してCPS変換を無効化できます。これは、Groovy関数が変換されていないため、完全に再実行させる必要があることに注意してください。

非同期なパイプラインステップ(shsleepのような)は常にCSP変換され、@NonCPSアノテーションがついたメソッドの内部で利用することができません。一般的に@NonCPSアノテーション付きメソッドの内部でパイプラインステップを利用することを避けるべきです。

パイプラインの耐久性

パイプラインの耐久性を変更することは、スローされるはずの箇所でNotSerializableExceptionがスローされないことは注目に値します。PERFORMANCE_OPTIMIZEDによってパイプラインの耐久性を低下させることはパイプラインの現在の状態を永続化させる頻度を大幅に低下させることを意味するからです。つまり、パイプラインはシリアライズ化できない値をシリアライズ化しようとしなくなり、結果として例外がスローされなくなります。

このメモは、この操作の根本的な原因についてユーザに。パイプラインの耐久性の設定はパフォーマンス最適化のためだけに操作し、シリアライズ化問題を避けるためにおこなうことは推奨されません。