Edited at

Jenkinsの設定を最小限でJenkinsfile(Pipeline)を使う

More than 1 year has passed since last update.


概要

 Jenkins2からPipelineが標準採用されたことにより、PipelineコードをJenkinsfileに定義することで今までUIでポチポチして定義していた各種タスクをコードベースで定義できるようになった。このことよりバージョン管理との統合もしやすくなりました。

 下記ではGithub → Jenkins(JSのテスト、ソースのマージ)→ Slackへの一連の流れを簡単なサンプルと共に解説していきたいと思います。Jenkinsでの設定はプラグイン導入とジョブ作成時以外はしません!理想としてはJenkinsインストール後何もせずに使えると一番うれしいですが・・・Dockerで頑張ればいけるものでしょうか

もし、gitlabをお使いならば、gitlab runnerでシンプルにCI環境を構築できるので、検討してみるのもいいかと思います

参考として「Gitlab(Runner、Container Registry)+AnsibleでローカルにCI環境を作ってみる


環境

 Jenkinsの導入環境はAmazon Linuxになります

 各種コマンドは必要に応じて読み替えてください


手順


1. Jenkinsの導入

導入手順はこちらをご参照ください


2. Jenkinsの各初期設定

Adminのセキュリティ設定と初期プラグインのインストール等


3. プラグインの導入


  • Build Authorization Token Root(認証なしで外部からビルドをキックする)

  • Slack Notification(Slackにメッセージング)

    ※上記のプラグインは自力で頑張れば、導入不要になりますので、必要に応じて導入してください


4. Jenkinsユーザ

JenkinsのジョブはJenkinsユーザによって実行されるため、

Nodejsの導入、ssh keyの生成などをJenkinsユーザで事前に行います

# rootユーザに切替

$ > sudo su

# Jenkinsユーザに切り替え
$ > su -s /bin/bash - jenkins


5. nvmでnodejsとnpmの導入

導入方法はこちらをご参照ください

 ※バージョンは適宜変更してください


6. githubで使うssh keyの生成

生成方法はこちらをご参照ください(とりあえずパスフレーズなし)

ssh使わずにaccess tokenで接続する場合は生成不要です(下記のconfigの作成も不要)


7. ssh configの作成

GithubにはSSHで接続するため、ssh configを作成します

異なる鍵で複数のリポジトリにssh接続する場合はconfigファイルを設定すると便利です

 (同じホストだけど異なる鍵でアクセスしたい時に用いる)

# $JENKINS_HOME/.ssh (デフォルトは/var/lib/jenkins/.ssh)

# .sshフォルダがない場合は、新規作成してください
# フォルダとファイルの所有者がjenkinsになっていることを確認しましょう

$ > cd ~/.ssh/
$ > vi config


config

Host sample.github.com

User jenkins
Hostname github.com
IdentityFile ~/.ssh/hogehoge_rsa

Host sample2.github.com
User jenkins
Hostname github.com
IdentityFile ~/.ssh/hogehoge2_rsa



8. Githubのリポジトリのデプロイキーに登録

前述で生成したssh keyの公開鍵(hogehoge_rsa.pub)をデプロイキーに登録


書き込みを許可する場合は、「Allow write acces」にチェックしてください

この記事は書き込みを前提にしていますので、チェックを入れてください

2.jpg


9. リモートホストの登録

Jenkinsのタスク実行時に新規ホスト(Github)と接続する際にホスト追加のyes/no選択でコケるので、あらかじめ手動で接続許可しときます

新規ホストの接続許可をすべて許可するやり方もありますが、セキュリティ的に問題なので今回は手動で登録します

# configで設定したHost名でssh接続し、新規ホストを追加

# 問題なく接続できたなら.sshフォルダ配下にknown_hostsファイルが生成されていることを確認してください

$ > ssh -T git@sample.github.com


10. Jenkinsで新規ジョブの追加

Pipelineジョブを選択し、作成してください

1.jpg


11. ジョブの設定

「リモートからビルド」にチェックし、認証トークンは適当なキーワードを設定してください

GithubからWebhook時にこの認証トークンでジョブをキックします


4.jpg

PipelineのDefinitionで「Pipeline script from SCM」を選択し、SCMは「Git」、Repositorie URLにGithubのリポジトリを追加してください

3.jpg

URLのホスト名はssh configのHOSTで設定した値にしてください

例)

# ssh config

Host sample.github.com
User jenkins
Hostname github.com
IdentityFile ~/.ssh/hogehoge_rsa

の場合は

git@github.com:xxxxx/repo-name.git

git@sample.github.com:xxxxx/repo-name.git

以下のようなエラーが表示されている場合は、ssh keyの所有権、アクセス権、ホスト名を確認してください

5.jpg


12. GithubのWebhookを設定

hook先のURLには上記の認証トークンとジョブ名を付加します

認証トークンを「1234567890」で設定した場合は、jenkins hook urlでは「http(s)://jenkins host name/jenkins/buildByToken/build?ジョブ名&token=1234567890」になります

6.jpg

7.jpg

設定後右上の「Test service」で問題なく疎通することを確認してください

なお、AWSのセキュリティグループで送信元を絞っている場合は、GithubのIPを許可してあげてください

IP一覧はこちらを参照ください


13. pipelineコードを含むJenkinsfileをGithubにプッシュ

Jenkinsfileはリポジトリのルートに配置してください

Jenkinsfileの詳細は下記で解説します


14. ビルド終了後はSlackに通知

8.jpg


Jenkinsfileサンプルコード

pipelineでshell実行してあれこれをやるようなサンプルになっています(初心者なのでこのぐらいしかできていません)

shellの実行結果(returnStatus)で拾って実行の可否を判定しています

returnStdoutを用いれば実行ログを取得することもできますので、もっと細かい判定などに使えると思います

各shell実行時にカレントディレクトリがジョブのルートになるため、都度ディレクトリを移動してからコマンドを実行しています

下記のサンプルコードはこちらのリポジトリに一式ありますので、

必要な部分(リポジトリ名、ブランチ名、Slackのトークンなど)を書き換えて試してみてください


Jenkinsfile

#!groovy

def err_msg = ""
def repo_name = "jenkins-pipeline-sample"
def git_url = "git@sample.github.com:comefigo/${repo_name}.git"
def dev_branch = "dev"
def release_branch = "master"

node {
try {
// ソースの取得
stage("get resource") {
// カレントディレクトにgitリポジトリが存在するか否かの確認
if(fileExists("./${repo_name}") && fileExists("./${repo_name}/.git")) {
// フェッチ
def FETCH_RESULT = sh(script: "cd ./${repo_name} && git fetch --all", returnStatus: true) == 0
if(!FETCH_RESULT) {
// throw error
error "fetchに失敗しました"
}
// gitがある場合はpull
def PULL_RESULT = sh(script: "cd ./${repo_name} && git pull --all", returnStatus: true) == 0
if(!PULL_RESULT) {
error "pullに失敗しました"
}
// ブランチの切替
def CHECKOUT_RESULT = sh(script: "cd ./${repo_name} && git checkout ${dev_branch}", returnStatus: true) == 0
if(!CHECKOUT_RESULT) {
// throw error
error "checkoutに失敗しました"
}
} else {
// gitがない場合はclone
def CLONE_RESULT = sh(script: "git clone ${git_url} ${repo_name}", returnStatus: true) == 0
if(!CLONE_RESULT) {
error "cloneに失敗しました"
}
}
}

// npmライブラリのインストール
stage("install libs") {
withEnv(["PATH+NODE=${JENKINS_HOME}/.nvm/versions/node/v6.9.5/bin/"]) {
def NPM_RESULT = sh(script: "cd ./${repo_name} && npm install", returnStatus: true) == 0
if(!NPM_RESULT) {
error "npm installに失敗しました"
}
}
}

// コードのテスト
stage("testing code") {
withEnv(["PATH+NODE=${JENKINS_HOME}/.nvm/versions/node/v6.9.5/bin/"]) {
def TEST_RESULT = sh(script: "cd ./${repo_name} && npm test", returnStatus: true) == 0
if(!TEST_RESULT) {
error "testに失敗しました"
}
}
}

// コードマージ
stage("merge code") {
// ブランチの切替
def CHECKOUT_RESULT = sh(script: "cd ./${repo_name} && git checkout ${release_branch}", returnStatus: true) == 0
if(!CHECKOUT_RESULT) {
error "checkoutに失敗しました"
}

// マージ
def MERGE_RESULT = sh(script: "cd ./${repo_name} && git merge ${dev_branch}", returnStatus: true) == 0
if(!MERGE_RESULT) {
error "マージに失敗しました"
}

// リモートへプッシュ
def PUSH_RESULT = sh(script: "cd ./${repo_name} && git push origin ${release_branch}:${release_branch}", returnStatus: true) == 0
if(!PUSH_RESULT) {
error "プッシュに失敗しました"
}
}

} catch (err) {
err_msg = "${err}"
currentBuild.result = "FAILURE"
} finally {
if(currentBuild.result != "FAILURE") {
currentBuild.result = "SUCCESS"
}
notification(err_msg)
}
}

// 実行結果のSlack通知
def notification(msg) {
def slack_channel = "#jenkins" // jenkinsが通知するチャネル
def slack_domain = "" // slackのドメイン名 https://mydomain.slack.comのmydomainの部分
def slack_token = "" // slackのjenkinsプラグインで取得できるtoken
def slack_color = "good"
def slack_icon = ""
def detail_link = "(<${env.BUILD_URL}|Open>)" // SlackでOpenのアンカーとして表示されます
// ビルドエラー時にメッセージの装飾を行う
if(currentBuild.result == "FAILURE") {
slack_color = "danger"
}
def slack_msg = "job ${env.JOB_NAME}[No.${env.BUILD_NUMBER}] was built ${currentBuild.result} ${currentBuild.durationString.replace(' and counting', '')}. ${detail_link} \n\n ${msg}"
slackSend channel: "${slack_channel}", color: "${slack_color}", message: "${slack_msg}", teamDomain: "${slack_domain}", token: "${slack_token}"
}



ステージ

pipelineの各ステージを表す

stage("stage 1") {

}

stage("stage 2") {

}


変数宣言

def hogehoge = ""


関数宣言

def hogehogefunction(val1, val2) {

return true
}


変数を表示

""内に${変数}

def hogehoge

echo "${hogehoge}"

// オブジェクトの場合はエラーになるため、"${hogehoge}"のほうが安全と思われる
echo hogehoge


try ... catch ... finally

  try {

// エラーをthrow
error "my exception"
} catch (e) {
// オブジェクトをテキストとして表示
echo "${e}"
} finally {
// send message email or slack
}


スクリプトの実行結果を取得

returnStatus: trueにすることでsellの実行結果を取得することできます

成功時は0、それ以外は0以外

def RESULT = sh (

script: "do something",
returnStatus: true
) == 0

if(RESULT) {
// do something
} else {
// do something
}


スクリプトの実行ログを取得

returnStdout: trueにすることでshellの実行ログを取得することができます

def STDOUT = sh (

script: "do something",
returnStdout: true
).trim()

echo "${STDOUT}"


ビルドの成功/失敗

ビルドプロセス内の自作の判定結果に応じてビルドの成否を明示的に設定するときに

currentBuild.resultに値を設定する

resultがFAILUREでもステージの処理は継続します

if(hoge) {

// 成功時
currentBuild.result = "SUCCESS"
} else {
// 失敗時
currentBuild.result = "FAILURE"
}


環境変数を有効化

withEnvで一時的に環境変数を有効化することができます

withEnv(["PATH+NODE=${JENKINS_HOME}/.nvm/versions/node/v6.9.5/bin/"]) {

// do something with node
}


ファイルの有無の確認

fileExistsでファイルまたはフォルダの存在を確認できる

if(fileExists("hogehoge")) {

echo "hogehoge exist!!!"
}


Slackで使える文字の装飾

https://api.slack.com/docs/message-formatting


各種リファレンス(重要)

これを見るまでは何ができるのか、どういった関数を呼べるのかがわからなかったが、

この存在を知ってからはリファレンスを見ながらあれこれと試せるようになったので、皆さんも是非活用してみてください

導入しているプラグインのリファレンスも載っているのでかなり役に立ちます

1.png


Piplineサンプル

https://jenkins.io/doc/pipeline/examples/

長くなりましたが、なにか不明点やツッコミ、こういう便利な使い方があるぜなどがありましたら、ぜひコメントを頂けると幸いです。