概要
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
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」にチェックしてください
この記事は書き込みを前提にしていますので、チェックを入れてください
9. リモートホストの登録
Jenkinsのタスク実行時に新規ホスト(Github)と接続する際にホスト追加のyes/no選択でコケるので、あらかじめ手動で接続許可しときます
新規ホストの接続許可をすべて許可するやり方もありますが、セキュリティ的に問題なので今回は手動で登録します
# configで設定したHost名でssh接続し、新規ホストを追加
# 問題なく接続できたなら.sshフォルダ配下にknown_hostsファイルが生成されていることを確認してください
$ > ssh -T git@sample.github.com
10. Jenkinsで新規ジョブの追加
11. ジョブの設定
「リモートからビルド」にチェックし、認証トークンは適当なキーワードを設定してください
GithubからWebhook時にこの認証トークンでジョブをキックします
PipelineのDefinitionで「Pipeline script from SCM」を選択し、SCMは「Git」、Repositorie URLにGithubのリポジトリを追加してください
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の所有権、アクセス権、ホスト名を確認してください
12. GithubのWebhookを設定
hook先のURLには上記の認証トークンとジョブ名を付加します
認証トークンを「1234567890」で設定した場合は、jenkins hook urlでは「http(s)://jenkins host name/jenkins/buildByToken/build?ジョブ名&token=1234567890」になります
設定後右上の「Test service」で問題なく疎通することを確認してください
なお、AWSのセキュリティグループで送信元を絞っている場合は、GithubのIPを許可してあげてください
IP一覧はこちらを参照ください
13. pipelineコードを含むJenkinsfileをGithubにプッシュ
Jenkinsfileはリポジトリのルートに配置してください
Jenkinsfileの詳細は下記で解説します
14. ビルド終了後はSlackに通知
Jenkinsfileサンプルコード
pipelineでshell実行してあれこれをやるようなサンプルになっています(初心者なのでこのぐらいしかできていません)
shellの実行結果(returnStatus)で拾って実行の可否を判定しています
returnStdoutを用いれば実行ログを取得することもできますので、もっと細かい判定などに使えると思います
各shell実行時にカレントディレクトリがジョブのルートになるため、都度ディレクトリを移動してからコマンドを実行しています
下記のサンプルコードはこちらのリポジトリに一式ありますので、
必要な部分(リポジトリ名、ブランチ名、Slackのトークンなど)を書き換えて試してみてください
#!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で使える文字の装飾
各種リファレンス(重要)
これを見るまでは何ができるのか、どういった関数を呼べるのかがわからなかったが、
この存在を知ってからはリファレンスを見ながらあれこれと試せるようになったので、皆さんも是非活用してみてください
導入しているプラグインのリファレンスも載っているのでかなり役に立ちます
Piplineサンプル
長くなりましたが、なにか不明点やツッコミ、こういう便利な使い方があるぜなどがありましたら、ぜひコメントを頂けると幸いです。