本記事はアカウンティング・サース・ジャパン Advent Calendar 2016の5日目の記事です。

アカウンティング・サース・ジャパンは税理士向けクラウド会計システムA-SaaS(エーサース)を提供しているベンチャー企業です。ここしばらく常駐でお手伝いさせていただいており、現在はインフラ周りの改善を担当しています。

はじめに

継続的インテグレーションや継続的デプロイが当たり前の時代ですが、皆様のプロジェクトにおかれましてはTravisCIやCircleCIなどCIツールは導入済みでしょうか。

A-SaaSのシステムはRabbitMQなどで構成されたマイクロサービスアーキテクチャになっていて、アプリケーションは主にJavaやScala、たまにErlang(!)で構築されています。このようなシステムの中でデプロイフローがやや複雑になってきていたので、CIツールとしてJenkins2.0で導入されたPipelineを導入し、フローの改善を行うことになりました。

本記事では簡単なPipelineの説明と、逆引き形式で使い方について書きました。細かい導入の仕方などはPipelineの公式ドキュメントにゆずり、試行錯誤した部分を主にまとめています。

Pipelineとは

Jenkins 2.0でPipeline Pluginが標準装備となりました。Groovyのスクリプトを書くことで可視化されたビルドパイプラインを実現できます。

スクリーンショット 2016-12-01 16.13.30.png

こんな感じのコードを書くと

スクリーンショット 2016-11-29 15.16.19.png

このように可視化しつつ実行が可能です。

どんなときに嬉しいの?

やはりデプロイに関わるビルドからリリースまでの一連の流れを可視化できるのが一番のメリットだと思います。

手作業が入るときにも一時的にストップさせて途中からの実行が可能です。現在の開発からリリースまでフローが複雑になっていて、そのまま一部を自動化したいときには合致すると思います。

また、エンジニア以外のメンバーがQA環境やステージング環境にデプロイしたいときにも、GUIツールでできると心理的な安心感が強くなります。

Groovyで書けるので、頑張ればある程度なんでもできるのも大きいです。

もちろんデメリットとしてはJenkinsサーバの管理の問題や、自作でツールを作る程は細かいところに気が利かないなどの問題点は感じます。

CircleCIなどのクラウドツールでできるのが一番楽ではあるのですが、そういったCIツール導入が難しい状況では検討する価値はあるのではないでしょうか。

最初に読むドキュメント

まずはTutorialとして公式ドキュメントに目を通すのが一番だと思います。

Getting Started with Pipeline

Groovyについては、Rubyにインスパイアされている言語らしいのでRubyを書ける人は親しみやすいと思います。スクリプトを書き下すだけなので下記を流し読みで言語仕様を把握しました。

Java開発の強力な相棒として今すぐ使えるGroovy
RubyistがGroovyを始めてみた

step/stage/nodeについて

用語として覚えるのはこの3つくらいでしょうか。

stepが一つ一つの手順で、stageはそれの集まり。nodeはJenkinsのノードの概念で実行マシンをイメージするのが良いと思います。

node('master') {
  stage('手順1') {
    sh 'step1'
    sh 'step2'
    sh 'step3'
  }
}

上記の例はmasterノードでstep1~3のコマンドを順次実行し、手順1という名前でブラウザ上で見ることができます。各ステップでエラーが返るとその時点でジョブがエラー終了します。

コツとかハマったところとか

ということで、使っていく中で調べた内容を下記にまとめました。

出典はそのたびにググってきた内容なのでわからなくなってしまっているのですが、Stack Overflowが参考になる場合が多かったです。

shの実行でコマンドのエラーを無視したい

groovy上でtry-catchをしても良いのですが、最終的なreturn codeが0で返ってくれば良いので | で繋いで echo でもやってやれば大丈夫です

sh 'some_command_to_fail | echo "ignore failure"'

yes/noのユーザ入力を受け付ける

input "次に進んでよいですか?"

スクリーンショット 2016-12-01 16.17.55.png

パラメータをユーザ入力を受け付ける

パラメータが1つだとその値がそのまま返ってきて、複数だとHashに入ってくるという驚きの仕様。

1つの場合
def version = input(
  id: 'userInput', message: 'バージョン名を指定してください', parameters: [
  [$class: 'TextParameterDefinition', description: 'ver.', name: 'version']
])
複数の場合
def userInput = input(
  id: 'userInput', message: 'Let\'s go?', parameters: [
  [$class: 'TextParameterDefinition', defaultValue: 'a text\nwith several lines', description: 'A multiple lines text', name: 'aText'],
  [$class: 'StringParameterDefinition', defaultValue: 'a text', description: 'A String', name: 'aString'],
  [$class: 'BooleanParameterDefinition', defaultValue: true, description: 'A Boolean', name: 'aBoolean'],
  [$class: 'PasswordParameterDefinition', defaultValue: 'a password', description: 'A password', name: 'aPassword']
]) 
echo "${userInput.aString}"

スクリーンショット 2016-12-01 16.19.45.png

ちなみに$classに指定できるのは https://jenkins.io/doc/pipeline/steps/pipeline-input-step/ にまとまっています。

ユーザ入力待ち画面で他のジョブ実行をブロックしない

node内にinputを書くとMain Executorを消費してグローバル設定のConcurrencyの値までしか同時実行できなくなってしまうので必ずnodeの外でユーザ入力を受け付けるようにします。

node {
  stage('hoge') {
    ...
  }
}

stage('user input') {
  input 'OK?'
}

node {
  stage('fuga') {
    ...
  }
}

DSL以外のGroovy/Javaのメソッドを使う

Pipelineはsandboxの中で実行されるため、GroovyやJavaのメソッドを使うには許可が必要です。

Use Groovy Sandbox のチェックを外すか、一度実行させエラーが出た後に「Jenkinsの管理」→「In-process Script Approval」からApproveします。

ビルド実行時にパラメータを受け取る

通常のJenkinsの機能であるビルドのパラメータ化をジョブの設定で追加します。設定した名前の変数が定義された状態でスクリプトが実行されます。

JSONをパースする

payload という名前のビルドパラメータで渡ってきたときの例。

@NonCPS
def parseJson(text) {
    return new groovy.json.JsonSlurperClassic().parseText(text)
}

def data = parseJson(json)
node{
  stage('hoge') {
    echo data.hoge
  }
}

GitHubのWebhookの情報を受け取る

1.payloadという名前のパラメータを受け取るパラメータ付きビルドを設定する

def json = parseJSON(payload) //parseJSONは別項に書いたもの

def repository_name = json.repository.name
def url = json.repository.ssh_url

2.GitHub側でWebhookの設定を行う

  • URLはジョブのURL + /buildWithParameters
  • Content Typeは application/x-www-form-urlencoded

gitからpullする

git url: 'https://github.com/miyasakura/cidemo.git', branch: 'master'

Private Repositoryをgit pullする

Jenkinsの認証情報から鍵の情報を登録した上で、下記のようにcredentialのIDを指定できます。

git branch: url: 'git@github.com:company/repo.git', branch: 'master', credentialsId: '12345-6789-0abc-def1-234567'

SCM上のJenkinsfileから読み込む

Pipeline script from SCM を指定する。

Groovyスクリプトを分割する

直接Jenkins上で入力する形式ではなくGitHubなどから呼び出す場合はファイルを分割することが可能です。

まず、外部ファイルで関数定義をします。最後の return this がポイントです。

slack.groovy

def post(message) {
  def encoded_message = java.net.URLEncoder.encode(message)
  sh "curl -XPOST https://...."
}
return this

使うときは load で読み込みますが、job名@scriptという名前のディレクトリに置かれている(カレントディレクトリはjob名のディレクトリ)のでそこのファイルを無理矢理読みに行くようにします。

Jenkinsfile
node {
  slack = load "${pwd()}@script/slack.groovy"
  slack.post("hoge")
}

shell scriptを実行して標準入力を受け取る

returnStdout: true オプションを付けます

def res = sh(script: 'ls', returnStdout: true)

Mavenビルドを行う

Javaのビルドで使ったので一応メモ。Maven Integration Pluginが必要。

    withEnv(["PATH+MAVEN=${tool 'MAVEN3'}/bin"]) {
        sh "mvn clean package"
        step([$class: 'ArtifactArchiver', artifacts: '**/target/*.war', onlyIfSuccessful: true, allowEmptyArchive: true])
    }

タイムアウトを設定する

stage('input') {
  timeout(time: 5, unit: 'DAYS') {
    input 'ok?'
  }
}

エラー終了する

  error message: 'An error has occurred!'

Jenkinsの画面上に指定した情報を表示する

これはできませんでした。echo コマンドでステップ実行のログに表示させるくらいしかできなさそうです。

他のジョブを実行する

  build job: 'AnotherJob', parameters: [
    [$class: 'StringParameterValue', name: 'version', value: 'hoge']
  ]

sshでコマンドを実行する

まさかid/passでログインなんてしないよねということで鍵認証前提で。

SSH Agent PluginとSSH Credentials Pluginを入れて、認証情報からsshの情報を登録しておきます。

node {
  stage('ssh') {
    sshagent(credentials: '12345-6789-0abc-def1-234567') {
      def cmd = 'pwd'
      def server = 'hostname'
      def user = 'user'
      sh "ssh -t -t ${user}@${server} ${cmd}"
    }
  }
}

成功するまで実行を繰り返す

戻り値がtrueになるまでループする waitUntil を利用します。下記の例ではスリープを入れてますが、入れなくてもある程度の時間のwaitが入るようになっています。

waitUntil {
  try {
    sh("check_launched.sh")
    true  //いらないかも
  } catch(error) {
    sh "sleep 30"
    false
  }
}

Array.eachが最初の1つしか実行されない

なぜか .each はうまく動作しない。

for (int i = 0; i < arr.size(); i++) {
  echo arr[i]
}

定義した関数の中でも値を使う

def hoge = 'test'

def func() {
  println hoge // null
}

だとhogeが使えないので、defを取るとグローバルに使える

hoge = 'test'

def func() {
  println hoge // test
}

終わりに

いやまぁAWSのCodePipelineとか使いたかったんですが東京リージョンに来てないしということでJenkinsにしたのですが意外と悪くないですよ!

最初の感想は「思ったより色々できて良い」で、使ってると「やっぱりできないことあるし使いづらい」で、最近は「なかなか良い選択肢じゃないかな」という感じに落ち着いてます。

Groovy力とシェルスクリプト力がやや試されますが、ぜひ一度触ってお試しください。