$30 off During Our Annual Pro Sale. View Details »

Jenkins PipelineでのShared Librariesの活用

Jenkins PipelineでのShared Librariesの活用

Hisashi.Iguchi

March 23, 2022
Tweet

More Decks by Hisashi.Iguchi

Other Decks in Technology

Transcript

  1. Jenkins Pipelineでの
   Shared Librariesの活用
 @Jenkins Day Japan 2021 [2021/12/10]
 井口

    恒志 <Hisashi Iguchi>
 システム本部 品質統括部 品質管理部 SWET第二グループ
 株式会社ディー・エヌ・エー
 @ DeNA Co.,Ltd.
  2. 2 自己紹介
 井口 恒志 (Hisashi Iguchi)
 自動テストの開発・導入サポート 
 CI/CDの活用促進や関連技術の調査 


    プロジェクトの状態可視化基盤などの整備 
 CircleCI のJapanユーザコミュニティでも活動中 
 
 バスケ好きで試合観戦や週1, 2でプレイもしてます 
 DeNA システム本部 SWET第二グループ
 @hisa9chi

  3. 3 目次
 SWETとは
 Shared Libraries
 活用事例
 まとめ
 1 2 3

    4
  4. 4 本発表に関して
 • 弊社チームのTesting Blogへ投 稿した内容を発表形式にまとめ ています
 https://swet.dena.com/entry/2021/01/18/200000

  5. 5 本発表での前提
 • Jenkins controllerのversionは2.x系
 
 • Jenkinsのジョブの種類は“Pipeline”
 ◦ ジョブの定義はJenkinsfileにしてリポジトリへ登録


    ◦ Pipeline Syntaxは“Declarative Pipeline”

  6. 6 1. SWETとは
 SWETのビジョンや役割などを紹介


  7. 7 SoftWare Engineer in Test
 c.f.> Google SET 「テストから見えてくるグーグルのソフトウェア開発」 


    https://www.nikkeibp.co.jp/atclpubmkt/book/13/P85120/ 
 

  8. 8 MISSION
 1 ソフトウェアを起点とした
 ◦ DeNAサービス全般の品質向上 
 ◦ DeNAエンジニアの開発生産性向上 


    により、価値あるものを素早く提供できるようにする 

  9. 9 Vision: Make Testing Fun, Smart and Delighting End-Users
 2

    • Fun
 ◦ テスティングを楽しくクリエイティブなものにする 
 • Smart
 ◦ 定型的なテストを人の手を煩わせず実現する 
 ◦ テストによるフィードバックを上手に活用して、素早い開発サイクルを実現す る
 • Delighting End-Users
 ◦ テストにより事業の成功を後押しするため、エンドユーザをデライトさせる 様々な品質を計測し改善する

  10. 10 SWETメンバーのプロジェクトへの関わり
 3 • ソフトウェアテスト技術分野ごとのチーム 
 ◦ アーキテクチャ
 ◦ 自動テスト


    ◦ プロセス(CI/CD)
 • それぞれのチームが様々な技術領域の案件をサポート 
 ◦ QAによるテスト工程よりも前の工程で品質・生産性の向上 
 ◦ デバッグの効率化・工数削減などテスト技術の蓄積 
 ◦ テスト全般の実行環境・プロセスサポート・サービス化 

  11. 11 プロセスチームのJenkinsに関する取り組み
 4 • チームではJenkinsの運用コストを削減するための取り組みを実施 
 
 
 
 


    
 https://speakerdeck.com/dena_tech/mohairukemukai-fa-niokerujenkinskurautoshi-dai-falsejenkinsgou-zhu-toguan-li-tekunituk 

  12. 12 2 SWETの取り組みなどを
 ブログで定期的に投稿中
 
 https://swet.dena.com
 
 もしくは
 
 SWET

    Testing Blog で検索
 

  13. 13 2. Shared Libraries
 利用方法や特徴などを紹介


  14. Pipeline
 1 • PipelineのJenkinsfileをリポジトリ に登録・管理
 14 sample-01.jenkinsfile pipeline { agent

    { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } }
  15. Pipeline
 1 • PipelineのJenkinsfileをリポジトリ に登録・管理
 • ジョブの数だけJenkinsfileは増加 
 15 sample-01.jenkinsfile

    pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } sample-02.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } sample-03.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } }
  16. Pipeline
 1 • PipelineのJenkinsfileをリポジトリ に登録・管理
 • ジョブの数だけJenkinsfileは増加 
 • 時にはメソッドも定義


    16 sample-01.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } sample-02.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } sample-03.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } sample-04.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } def myFunc() { …. }
  17. Pipeline
 1 • PipelineのJenkinsfileをリポジトリ に登録・管理
 • ジョブの数だけJenkinsfileは増加 
 • 時にはメソッドも定義


    • コードの重複発生
 17 sample-01.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } sample-02.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } sample-03.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } sample-04.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } def myFunc() { …. } sample-05.jenkinsfile pipeline { agent { label 'ubuntu' } stages { stage ( 'pre-build' ) { .... } stage ( ‘build’ ) { .... } } } def myFunc() { …. } 重複部分を外部に定義
 再利用性とメンテナンス性が向上 

  18. 18 Shared Libraries


  19. 19 Shared Libraries : 概要
 • 概要
 ◦ Pipelineの外部に定義
 ◦

    ライブラリはSCM管理
 ◦ ライブラリはPipeline毎にロードして利用
 ▪ ロードは複数指定可能 
 • 特徴
 ◦ ジョブ毎で異なるバージョンの利用が可能
 ◦ メンテナンス性と再利用性の向上
 ◦ クラスのstaticメソッドのapproveが不要
 2
  20. 20 Shared Libraries : 特徴 - ジョブ毎で異なるバージョンの利用が可能 
 • ライブラリのロード時に以下の指定が可能

    
 ◦ タグ
 ◦ ブランチ
 ◦ コミットハッシュ
 2 ライブラリのロード方法 // デフォルトバージョンの読み込み // "Global Pipeline Libraries 設定" の "Default version" @Library( 'ライブラリ名' ) _ // ブランチ,タグ,コミットハッシュを指定する場合 @Library( 'ライブラリ名@{branch | tag | commit hash}' ) _ // 複数ライブラリの読み込み @Library( ['ライブラリ名1', 'ライブラリ名2'] ) _ pipeline { agent { label 'ubuntu' } .... }
  21. 21 Shared Libraries : 特徴 - メンテナンス性と再利用性の向上 
 • 変更はライブラリ内に閉じる


    • 他のJenkinsからも利用可能
 ◦ ライブラリを管理しているリポジトリのアクセス権は必要 
 • Plugin利用をラップしたライブラリの作成 
 ◦ Pluginの更新による変更影響をライブラリ内に閉じれる場合あり 
 2
  22. 22 Shared Libraries : 特徴 - クラスのstaticメソッドのapproveが不要 
 • Pipelineではgroovyによるクラスのstaticメソッドが実行可能

    
 ◦ セキュリティ的に問題
 ◦ 管理者が承認したクラスのstaticメソッドの場合実行可能 
 • Pipeline内のgroovyコードにてクラスのstaticメソッドを利用した場合 
 ◦ 利用メソッド全てにapproveが必要 
 ◦ 結構面倒な作業(管理者に承認してもらう必要があるため)
 2
  23. 23 Shared Libraries : 特徴 - クラスのstaticメソッドのapproveが不要 
 • Pipelineではgroovyによるクラスのstaticメソッドが実行可能

    
 ◦ セキュリティ的に問題
 ◦ 管理者が承認したクラスのstaticメソッドの場合実行可能 
 • Pipeline内のgroovyコードにてクラスのstaticメソッドを利用した場合 
 ◦ 利用メソッド全てにapproveが必要 
 ◦ 結構面倒な作業(管理者に承認してもらう必要があるため)
 • ライブラリであればapproveは不要 
 ◦ 承認の面倒な作業が発生しない 
 2
  24. 24 Shared Libraries : Jenkins Plugin と比較
 3 Shared Libraries

    Jenkins Plugin 他のcontrollerからの 利用 可能 Jenkins設定のみ 可能 Plugin のインストール 利用バージョン 複数 タグ/ブランチ/コミットハッシュ 1バージョン ※1 インストールされたバージョン メンテナンス ライブラリ管理者 / 作成者 Pluginメンテナー 変更影響 なし ※2 複数バージョン指定可能なため 全ジョブに影響 変更によりジョブの失敗の可能性あり ※1 Plugin の更新に controller のバージョン更新や依存 Plugin の更新が必要な場合あり 
 ※2 Plugin を利用している場合は Plugin の更新に依存して変更影響が発生する場合あり 

  25. 25 Shared Libraries : 作成と設定
 • 作成方法と実際の設定に関しては公式サイトを参考 
 ◦ https://www.jenkins.io/doc/book/pipeline/shared-libraries/


    
 • SWET Testing Blog に少し記載しているのでご参考に 
 ◦ https://swet.dena.com/entry/2021/01/18/200000
 4
  26. 26 3. 活用事例
 社内で Shared Libraries をどのように活用しているか紹介


  27. 27 活用事例
 • ジョブの成果物のGCS(Google Cloud Storage)保存 
 • エラーとなったstage名の検出
 •

    Slack通知時のアタッチメント作成 
 • Slack通知時のメンション先ユーザのID取得 
 1
  28. 28 活用事例:ジョブの成果物のGCS保存
 • ジョブ実行時の成果物はJenkins controller自体に保存が可能 
 ◦ controllerの容量には制限
 ▪ ディスクの拡張などのメンテが必要

    
 ◦ 成果物(アプリのバイナリなど)をJenkins外から取得する時に面倒 
 • ジョブの成果物をGCSへ保存
 ◦ ストレージの拡張などメンテフリー 
 ◦ オブジェクトのライフサイクル設定でデータ自動削除可能 
 ◦ パブリッククラウドのため他サービスへの取り回しが比較的楽 
 2
  29. 29 活用事例:ジョブの成果物のGCS保存
 • GCSアップロードには Plugin が存在 
 ◦ Google Cloud

    Storage Plugin: https://plugins.jenkins.io/google-storage-plugin/
 ◦ アップロード時には以下のパス構成でGCSへ保存しておきたい 
 ▪ backet_name/job_name/build_no/upload_files 
 ◦ Plugin利用時は上記のアップロードファイル以前のパスの指定 
 ▪ 基本的にはどのプロジェクトでも同様の方針 
 2 Plugin を利用したGCSアップロード // backet_name/job_name/build_no のパス設定 UPLOAD_DIR = "gs://${params.bucket_name}/${env.JOB_NAME}/${env.BUILD_NUMBER}" googleStorageUpload( bucket: UPLOAD_DIR, credentialsId: params.credentialsId, pattern: uplodFiles ) • 上記改善のためPluginをラップしたライブラリを作成 

  30. 活用事例:ジョブの成果物のGCS保存
 
 2 • ライブラリ側で以下を付加して アップロード
 ◦ ジョブ名/ビルド番号
 
 •

    Pipeline側では以下を指定
 ◦ バケット名
 ◦ 認証情報ID
 ◦ アップロードファイル 
 30 ライブラリ側 (libUploadArtifactsToGCS) // バケット名にジョブ名/ビルド番号 を付与 def uploadDir = "gs://${params.bucket}/${env.JOB_NAME}/${env.BUILD_NUMBER}" // ファイルのGCSへアップロード googleStorageUpload( bucket: uploadDir, credentialsId: params.credentialsId, pattern: params.pattern ) Pipeline側(呼び出し側) steps { // バケット名、認証情報ID、アップロード対象を指定してライブラリ呼び出し libUploadArtifactsToGCS( bucket: 'jenkins-artifacts', credentialsId: 'google-service-account', pattern: 'upload-sample.ipa' ) }
  31. 31 活用事例:エラーとなったstageの検出
 • ジョブ実行時にエラーとなったstage名を通知に利用したい 
 ◦ エラーとなったstage名を取得は以下の方法知られている 
 3 エラーとなったstage名を通知などに利用する場合

    pipeline { agent { label ‘ubuntu’ } stages { stage ( ‘pre-build’ ) { …. } post { // stage毎の post process にて失敗時タスクで環境変数に stage名を設定 failure { script { env.ERROR_STAGE_NAME = ‘pre-build’ } } } …. } post { failure { // env.ERROR_STAGE_NAME を利用して失敗 stage名を通知など実行 } } }
  32. 活用事例:エラーとなったstageの検出
 
 3 • この方法は以下の問題あり
 ◦ コード量の増加
 ◦ stageが新規追加される度 に追加が必要


    32 エラーとなったstage名を通知などに利用する場合 pipeline { agent { label ‘ubuntu’ } stages { stage ( ‘pre-build’ ) { …. } post { // stage毎の post process にて失敗時タスクで環境変数にstage名を設定 failure { script { env.ERROR_STAGE_NAME = ‘pre-build’ } } } …. } post { failure { // env.ERROR_STAGE_NAME を利用して失敗stage名を通知など実行 } } } メンテナンス性低下の要因となる

  33. 活用事例:エラーとなったstageの検出
 
 3 • ライブラリ側で以下の手順で失 敗したstage名を返却
 ◦ 全てのstage情報取得
 ◦ 先頭から順に調べて最初

    に見つかった失敗状態の stage名を取得
 
 • Pipeline 側は失敗したstage名が 欲しい時に呼び出すのみ
 33 ライブラリ側 (libGetFailedStageName) // stage 情報の取得 ChunkVisitor visitor = new ChunkVisitor( workFlowRun ); ForkScanner.visitSimpleChunks( workFlowRun.getExecution().getCurrentHeads(), visitor, new StageChunkFinder() ); // 全ての stage のステータスを検索 for ( StageNodeExt stageExt : visitor.getStages() ) { if ( stageExt.getStatus() == StatusExt.FAILED ) { errorStageName = stageExt.getName(); break; } } return errorStageName Pipeline側 (呼び出し側) .... post { failure { sh "echo Error Stage: ${libGetFailedStageName()}" } }
  34. 34 活用事例:Slack通知時のアタッチメント作成
 • Slack通知以下のような共通の処理が多く存在 
 ◦ 通知用のアタッチメント作成
 4 アタッチメント作成 JSONObject

    attachmentObj = new JSONObject(); attachmentObj.put( 'title', title.toString() ); attachmentObj.put( 'title_link', title_link.toString() ); attachmentObj.put( 'text', subject.toString() ); attachmentObj.put( 'color', colorCode ); if ( fieldsMessages.size() > 0 ) { JSONArray fileds = new JSONArray(); fieldsMessages.each { Map message -> JSONObject customField = new JSONObject(); customField.put( 'title', message.title.toString() ) customField.put( 'value', message.message.toString() ) customField.put( 'short', message.short ) fileds.add( customField ); } attachmentObj.put( 'fields', fileds ); } アタッチメント作成(続き) if ( messages.size() > 0 ) { messages.each { key, value -> // 既存の設定も上書きできるようにする attachmentObj.put( key, value ); } } JSONArray attachmentArray = new JSONArray(); attachmentArray.add( attachmentObj ); return attachmentArray.toString(); jsonオブジェクトを作成する必要ある 

  35. 活用事例:Slack通知時のアタッチメント作成
 
 4 • ライブラリ側でkey-valueの配列 で情報を受け取りjson文字列を 作成して返却
 ◦ 受け取ったkey-value配列 からjsonオブジェクトを作

    成
 
 • Pipeline側ではkey-value配列を 渡す
 35 ライブラリ側 (libCreateSlackAttachments) JSONObject attachmentObj = new JSONObject(); …. if ( fieldsMessages.size() > 0 ) { JSONArray fileds = new JSONArray(); fieldsMessages.each { Map message -> JSONObject customField = new JSONObject(); …. fileds.add( customField ); } attachmentObj.put( 'fields', fileds ); } JSONArray attachmentArray = new JSONArray(); attachmentArray.add( attachmentObj ); return attachmentArray.toString(); Pipeline側 (呼び出し側) script { def List fieldsMessages = [ [ "title":"Execute Node", "message":"test-node", "short":false ], [ "title":"Test Branch", "message":"origin/master", "short":true ] ] def messages = [ "text": "message" ] def attachments = libCreateSlackAttachments( currentBuild.currentResult, fieldsMessages, messages ) }
  36. 36 活用事例:Slack通知時のメンション先ユーザのID取得
 • Slack通知時にジョブを実行した方へメンションをしたい 
 ◦ ジョブ実行者のSlack IDを取得してメッセージにそのIDを設定 
 5

    ジョブ実行ユーザのemailからslack id の取得 // Slack API: https://slack.com/api/users.list?token=xxx へのリクエスト // Response取得 Response response = client.newCall( request ).execute(); // json へ変換 Object json = new JsonSlurper().parseText( response.body().string() ) // User の mention 用 ID 文字列 String mentionIdsStr = ""; // email から User ID を検出 for ( user in json.members ) { for ( target in params.userEmails ) { if ( user.profile.email == target ) { mentionIdsStr += "<@" + user.id + "> " } } } return mentionIdsStr; これ

  37. 活用事例:Slack通知時のメンション先ユーザのID取得
 
 5 • ライブラリ側でemailを受け取っ てslackのIDを通知形式で返却
 ◦ Slack APIを活用
 


    • Pipeline側はジョブ実行者の emailを渡すだけ
 37 ライブラリ側 (libGetFailedStageName) // Slack API: https://slack.com/api/users.list?token=xxx へのリクエスト // Response取得 Response response = client.newCall( request ).execute(); // json へ変換 Object json = new JsonSlurper().parseText( response.body().string() ) // User の mention 用 ID 文字列 String mentionIdsStr = ""; // email から User ID を検出 for ( user in json.members ) { for ( target in params.userEmails ) { if ( user.profile.email == target ) { mentionIdsStr += "<@" + user.id + "> " } } } return mentionIdsStr; Pipeline側 (呼び出し側) step { def id = libGetFailedStageName( userEmails: ['xxxx@gmail.com', 'yyyyy@dena.com'], slackAPIToken: 'token’ ) } 現在はSlack Notification Pluginでもemailから SlackのユーザIDの取得が可能になっている模 様

  38. 38 ライブラリ利用の現在
 • 導入当初は1プロジェクトで利用 -> 現在は 複数プロジェクトが利用
 ◦ Jenkins controller

    はプロジェクト毎に構築 
 
 • 他のプロジェクトメンバーもライブラリを作成して共有 
 
 • 既存ライブラリの機能拡張
 6
  39. 39 ライブラリ利用の現在:既存ライブラリの機能拡張
 • GCSファイルのアップロード/ダウンロードに関して 
 ◦ アップロード時に設定可能にした key/tagを利用してindexファイルも保存 
 ▪

    indexファイル保存パス: bucket_name/index/key/tag 
 • index ファイル内にはアップロードした成果物のダウンロードURLを記載
 ◦ ダウンロードの際は key/tagを指定することで成果物のダウンロードが可能 
 ▪ 拡張前はジョブ名と、ビルド番号が必要であり目的のファイルの場所を探すのが面倒
 6 GCSへのアップロード時 // key / tag を設定してアップロード libUploadArtifactsToGCS( bucket: BUCKET_NAME, credentialsId: GCS_CREDENTIAL_ID, pattern: build.ipa, key: "ios_app", tags: ["latest"] ) GCSからダウンロード時 // key / tag を指定して目的の成果物をダウンロード libDownloadArtifactsFromGCS( bucket: BUCKET_NAME, credentialsId: GCS_CREDENTIAL_ID, key: "ios_app", tag: "latest", dirPath: ARTIFACTS_DIR )
  40. 40 4. まとめ 
 本日の総括です
 


  41. 41 まとめ
 • Shared Libraries を利用することで以下の利点 
 ◦ ジョブのメンテナンス性向上
 ◦

    再利用性の向上
 ◦ Jenkins Pluginとは異なり複数バージョン利用可能
 ▪ 更新時の影響範囲が限定
 • 他で作成されたライブラリを利用 
 ◦ アクセス権があれば他のJenkins controllerからも 容易に利用可能
 ◦ より効率的にジョブの作成
 • 多くのプロジェクトで利用しやすいよう ライブラリを改善
 1
  42. 42 質疑応答 
 何かあれば
 


  43. @ DeNA Co.,Ltd.