--- title: はじめてのConcourse CI tags: ["Concourse CI", "Docker"] categories: ["Dev", "CI", "ConcourseCI"] date: 2016-04-10T13:29:35Z updated: 2017-06-04T05:46:12Z --- [前記事](https://blog.ik.am/entries/379)の続きです。 **目次** ### はじめてのConcourse CI #### はじめてのTask まずはJobを作る前に、Task単体(One-Off Task)を試しましょう。JobはTaskとResourceから構成されますが、One-Off Taskの実行方法を覚えておくとJobの開発・デバッグに役立ちます。 `hello.yml`を作成して、以下の内容を記述してください。 ``` yaml --- platform: linux image_resource: type: docker-image source: repository: alpine run: path: echo args: ["Hello", "World"] ``` repositoryには極小サイズのDockerイメージである`alpine`を指定しました。 以下のコマンドを実行してください。 ``` console $ fly -t lite execute -c hello.yml ``` 以下のような出力が得られるでしょう。 ``` console targeting http://192.168.100.4:8080 executing build 1 initializing Pulling alpine@sha256:9cacb71397b640eca97488cf08582ae4e4068513101088e9f96c9814bfda95e0... sha256:9cacb71397b640eca97488cf08582ae4e4068513101088e9f96c9814bfda95e0: Pulling from library/alpine 420890c9e918: Pulling fs layer 420890c9e918: Verifying Checksum 420890c9e918: Download complete 420890c9e918: Pull complete 420890c9e918: Pull complete Digest: sha256:9cacb71397b640eca97488cf08582ae4e4068513101088e9f96c9814bfda95e0 Status: Downloaded newer image for alpine@sha256:9cacb71397b640eca97488cf08582ae4e4068513101088e9f96c9814bfda95e0 Successfully pulled alpine@sha256:9cacb71397b640eca97488cf08582ae4e4068513101088e9f96c9814bfda95e0. running echo Hello World Hello World succeeded ``` TaskはDockerコンテナ上で行われます。 初回はDockerイメージのプルが行われましたが、2回目以降はプル済みのイメージを使用します。 ``` console $ fly -t lite execute -c hello.yml targeting http://192.168.100.4:8080 executing build 2 initializing running echo Hello World Hello World succeeded ``` YAMLでインラインスクリプトを書く場合は、以下のように`sh -c`で文字列の複数行記法を使うのが好みです。 ``` yaml --- platform: linux image_resource: type: docker-image source: repository: alpine run: path: sh args: - -c - | echo "Hello World" ``` 実行 ``` console $ fly -t lite execute -c hello.yml targeting http://192.168.100.4:8080 executing build 3 initializing running sh -c echo "Hello World" Hello World succeeded ``` タスクで何らかのコマンドやライブラリが必要な場合は、JenkinsのようにCIサーバーにそれをインストールするのではなく、必要なものが用意されたDockerイメージを使用すれば良いです。 例えば、前述の`alpine`パッケージには`bash`も`git`もインストールされていません。 先ほどの`hello.yml`の`path`を`sh`から`bash`に変えると次のエラーが発生するでしょう。 ``` yaml targeting http://192.168.100.4:8080 executing build 4 initializing running bash -c echo "Hello World" proc_starter: ExecAsUser: system: program 'bash' was not found in $PATH: exec: "bash": executable file not found in $PATH failed ``` `bash`や`git`を使うために、それらがインストール済みのイメージを指定すればよいです。ここでは`getourneau/alpine-bash-git`を使用します。`hello.yml`を次のように変更します。 ``` yaml --- platform: linux image_resource: type: docker-image source: repository: getourneau/alpine-bash-git run: path: bash args: - -c - | echo "Hello World" ``` 実行 ``` console $ fly -t lite execute -c hello.yml targeting http://192.168.100.4:8080 executing build 5 initializing Pulling getourneau/alpine-bash-git@sha256:5a8941b28a8dafb0a1bd43e4ba1a4ea6f9f0e186e326ee4378deabe83d263442... sha256:5a8941b28a8dafb0a1bd43e4ba1a4ea6f9f0e186e326ee4378deabe83d263442: Pulling from getourneau/alpine-bash-git 4d06f2521e4f: Pulling fs layer e2433d8ede7d: Pulling fs layer 061a2bf86483: Pulling fs layer 4d06f2521e4f: Verifying Checksum 4d06f2521e4f: Download complete e2433d8ede7d: Verifying Checksum e2433d8ede7d: Download complete 4d06f2521e4f: Pull complete 4d06f2521e4f: Pull complete e2433d8ede7d: Pull complete e2433d8ede7d: Pull complete 061a2bf86483: Verifying Checksum 061a2bf86483: Download complete 061a2bf86483: Pull complete 061a2bf86483: Pull complete Digest: sha256:5a8941b28a8dafb0a1bd43e4ba1a4ea6f9f0e186e326ee4378deabe83d263442 Status: Downloaded newer image for getourneau/alpine-bash-git@sha256:5a8941b28a8dafb0a1bd43e4ba1a4ea6f9f0e186e326ee4378deabe83d263442 Successfully pulled getourneau/alpine-bash-git@sha256:5a8941b28a8dafb0a1bd43e4ba1a4ea6f9f0e186e326ee4378deabe83d263442. running bash -c echo "Hello World" Hello World succeeded ``` 新たなイメージがダウンロードされ、bashを実行することができました。 スクリプトはインラインだけでなく外部ファイルを指定することも可能です、 ``` yaml run: path: ./hello/hello.sh ``` ここで、疑問が出ます。ファイルはどうやってコンテナに渡せば良いでしょうか? Taskには`inputs`、`outputs`属性で入出力フォルダを指定できます。 ``` yaml --- platform: linux image_resource: type: docker-image source: repository: getourneau/alpine-bash-git inputs: - name: hello run: path: ./hello/hello.sh ``` インラインスクリプトは同じフォルダの`hello.sh`に移してください。 ``` sh #!/bin/bash echo "Hello World" ``` また`chmod +x hello.sh`で実行権限をつけてください。 `fly execute`を実装する際に`-i`オプションで`inputs`のマッピングを指定します。 ``` console $ fly -t lite execute -c hello.yml -i hello=. targeting http://192.168.100.4:8080 executing build 10 initializing running ./hello/hello.sh Hello World succeeded ``` `-i`で指定したディレクトリがアップロードされてコンテナからアクセスできました。 `hello.sh`の内容を以下に書き換えて再度実行してみましょう。 ``` sh #!/bin/bash find . ``` 実行 ``` console $ fly -t lite execute -c hello.yml -i hello=. targeting http://192.168.100.4:8080 executing build 11 initializing running ./hello/hello.sh . ./hello ./hello/hello.yml ./hello/hello.sh succeeded ``` カレントディレクトリの内容がコンテナ内の`hello`ディレクトリに配置されたことがわかります。 ただしTaskを実行するコンテナはステートレスで、ここでアップロードした内容は永続化されるわけではありません。 Taskで使用するファイル等を永続化したい場合はどうすれば良いでしょうか。ここで出てくるのがResourceです。 #### はじめてのJob ここで使用した`hello.sh`などはResourceに置くことで、Taskから常にアクセスできます。 このようにResourceとTaskを組み合わせたものがJobです。 実際にはResourceの定義はJobの定義ファイルに記述します。 ここではTaskで必要なファイルをGitHubに置き、Git Resourceを定義してTaskが使用するJobを作成しましょう。 まずは現状のカレントフォルダをGitHubにpushしましょう。 ここでは[making/hello-concourse](https://github.com/making/hello-concourse)を使用します。 ``` console $ git init $ git add -A $ git commit -m "first commit" $ git remote add origin https://github.com/making/hello-concourse.git $ git push -u origin master ``` 次にJobの定義を行います。 同じフォルダに`pipeline.yml`を作成してください。 ``` yaml --- # Resourceの定義 resources: # Git Resourceの定義 - name: hello type: git source: uri: https://github.com/making/hello-concourse.git # Jobの定義 jobs: - name: hello-job public: true # UI上でJobの結果をログイン不要で公開するかどうか plan: - get: hello trigger: true # Resourceに変更があれば自動でジョブを実行するかどうか - task: run-hello file: hello/hello.yml ``` `jobs`内の`plan`でResourceとTaskを組み合わせます。`get`はResourceをプルして、`put`はResourceをプッシュします。`task`にはさきほど作成したYAMLのパスを指定します。これもResourceから取得できるので、Resource名を考慮したパスを指定します。Taskはインラインで記述することも可能です。 Taskの定義ファイル内で宣言した`inputs`の名前を持つResourceが`get`で定義されている必要があります。今回の場合は`hello`です。 Jobは1つですが、これが最小のパイプラインになります `fly set-pipeline`でこのパイプラインをConcourse CIに設定します。 `-p`はパイプライン名です。 ``` console $ fly -t lite set-pipeline -p hello -c pipeline.yml targeting http://192.168.100.4:8080 resources: resource hello has been added: name: hello type: git source: uri: https://github.com/making/hello-concourse.git jobs: job job-hello has been added: name: job-hello public: true plan: - get: hello trigger: true - task: run-hello file: hello/hello.yml apply configuration? [yN]: y pipeline created! you can view your pipeline here: http://192.168.100.4:8080/pipelines/hello the pipeline is currently paused. to unpause, either: - run the unpause-pipeline command - click play next to the pipeline in the web ui ``` これでパイプラインが作成されました。[http://192.168.100.4:8080](http://192.168.100.4:8080)にアクセスするとパイプラインが表示されます。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/3943e8eb-6da2-0426-8cf5-56b985534fe7.png) `fly set-pipline`は `fly sp`と省略可能です。 この時点では"paused"と呼ばれる状態で、パイプラインは止まっておりResourceの変更をウォッチしません。UIのヘッダーが青色なのは"paused"な状態を示しています。 パイプラインを動作させるには"un-pause"します。 ``` console $ fly -t lite unpause-pipeline -p hello targeting http://192.168.100.4:8080 unpaused 'hello' ``` `fly unpause-pipeline`は `fly up`と省略可能です。 [http://192.168.100.4:8080](http://192.168.100.4:8080)をリロードするとヘッダーの青色は消えます。そしてしばらくするとジョブが開始します。`hello` Resourceの変更(初回分)を検知したためです。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/ea31605b-d472-4d62-30a3-c4f9a127dd16.png) Jobが成功すればブロックが緑色になります。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/fe81fe4c-db3a-6de1-e523-e5007915409b.png) Jobをクリックすると結果を確認することができます。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/23dd7624-9012-ff37-42c5-46c2051aed57.png) Taskによる`find`の結果に`.git`の中身も含まれていることがわかります。 Jobの実行は、Jobのページ右上の(+)ボタンをクリックしても行えますし、`fly trigger-job`でも実行可能です。 #### 結果のプッシュ 次に、`put`も使ってみましょう。`hello.sh`の出力結果をGitにpushしてみます。(ただのデモ用です。実際にはこんなことはしないでしょう) `hello.sh`を以下のように修正してください。 ``` sh #!/bin/bash find . > out/result.log ``` `out`ディレクトリは`hello.yml`に`outputs`として登録します。 ``` yaml --- platform: linux image_resource: type: docker-image source: repository: getourneau/alpine-bash-git inputs: - name: hello outputs: - name: out run: path: ./hello/hello.sh ``` `fly execute`で動作確認しましょう。(`fly e`と省略できます) ``` console $ fly -t lite e -c hello.yml -i hello=. -o out=/tmp/out targeting http://192.168.100.4:8080 executing build 22 initializing running ./hello/hello.sh succeeded ``` `/tmp/out/result.log`がダウンロードされていることを確認してください。 次にこの結果をGitにコミットするTaskを作成します。 以下のような`commit-log.yml`を作成してください。 ``` yaml --- platform: linux image_resource: type: docker-image source: repository: getourneau/alpine-bash-git inputs: - name: hello # ソースコード - name: result # clone用のGit - name: out # 全タスクの出力を入力にする outputs: - name: updated-result # commit用のGitリポジトリ run: path: ./hello/commit-log.sh ``` Concourseでは入力を出力に使えないため、clone用のGitとcommit用のGitを`inputs`と`outputs`でそれぞれ定義しています。 `commit-logs.sh`は以下のようになります。 ``` sh #!/bin/bash # clone用のGitをcloneしてcommit用のGitリポジトリを作成する git clone result updated-result cd updated-result/ # 前Taskの出力結果をGitのcommit用のGit作業ディレクトリに移動する mv -f ../out/* ./ git config --global user.email "makingx at gmail dot com" git config --global user.name "Toshiaki Maki" git add -A git commit -m "Update result log" ``` ちょっと複雑になってきましたが、これも`fly execute`で動作確認しておきましょう。 ``` console $ mkdir /tmp/git $ pushd /tmp/git $ git init # 動作確認用のダミーGitレポジトリ作成 $ popd $ fly -t lite e -c commit-log.yml -i out=/tmp/out -i result=/tmp/git -i hello=. -o updated-result=/tmp/updated-result targeting http://192.168.100.4:8080 executing build 26 initializing running ./hello/commit-log.sh Cloning into 'updated-result'... warning: You appear to have cloned an empty repository. done. [master (root-commit) 86b381e] Update result log 1 file changed, 54 insertions(+) create mode 100644 result.log succeeded ``` `outputs`である`updated-result`を確認すると、コミットされていることがわかります。 ``` console $ cd /tmp/updated-result/ $ git log commit 86b381ece045bbd8d8d94554ae885049974d66f6 Author: Toshiaki Maki Date: Sun Apr 10 17:12:58 2016 +0000 Update result log ``` さて、ようやくJobの準備です。ここまでの内容をまとめると`jobs`の`plan`は以下のようになります。 ``` yaml jobs: - name: job-hello public: true plan: - get: hello # パイプラインのGitをpull trigger: true - get: result # 出力結果のGitをpull (次に定義する) - task: run-hello # findの結果をファイルに書き出すTask file: hello/hello.yml - task: commit-log # 出力結果ファイルをGitにコミットするTask file: hello/commit-log.yml - put: result # 出力結果のGitのpush params: repository: updated-result ``` 今回は出力結果を格納するGit Resource(`result`)としてGistを使います。 まずは`result.log`というファイルを持つ、Gistを作成してください。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/20d84ee6-c5e5-1f3a-79bf-3f6552fcf8a2.png) Gistができたら、SSH用のURLをコピーしてください。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/93b148a3-637a-9812-b2e3-dcfdc07a7e75.png) SSHのURLを`result` Resourceの定義に貼り付けます。 ``` yaml - name: result type: git source: uri: git@gist.github.com:dbc56fbd08f415fa52c784f4cd2e4bd3.git private_key: {{github-private-key}} branch: master ``` GitHubにプッシュするためにはSSHのプライベートキーが必要です。この機密情報はソースコード管理対象外にするため、`pipeline.yml`上は`{{github-private-key}}`という形式でプレースホルダを利用できます。プレースホルダはパイプラインをConcourse CIに設定するタイミングで別ファイルから埋め込み可能です。 以上の内容をまとめると`pipeline.yml`は次のようになります。 ``` yaml --- resources: - name: hello type: git source: uri: https://github.com/making/hello-concourse.git - name: result type: git source: uri: git@gist.github.com:dbc56fbd08f415fa52c784f4cd2e4bd3.git private_key: {{github-private-key}} branch: master jobs: - name: job-hello public: true plan: - get: hello trigger: true - get: result - task: run-hello file: hello/hello.yml - task: commit-log file: hello/commit-log.yml - put: result params: repository: updated-result ``` 機密情報は`~/.concourse/credentials.yml`に記述しましょう。 ``` yaml --- github-private-key: | -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAuvUl9YU... ... HBstYQubAQy4oAEHu8osRhH... -----END RSA PRIVATE KEY----- ``` ようやくパイプラインの設定です。`-l`で機密情報ファイルのパスを指定してください。 ``` console $ fly -t lite sp -p hello -c pipeline.yml -l ~/.concourse/credentials.yml ``` 追加したパイプラインの設定ファイルをpushしてください。 パイプライン上はTaskのスクリプトファイルをGit Resourceから取っているため、パイプラインで使用するTaskを更新するにはGitHubにpushする必要がある点に注意が必要です。 ``` console $ git add -A $ git commit -m "update pipeline" -a $ git push origin master ``` UI上は以下のように表示されます。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/1615f672-7018-3bb0-6b5b-75e20b053672.png) Gitの更新が検知されると、`job-hello` Jobが開始します。成功すると次のような出力が得られます。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/66b1a578-8da5-9848-1c6f-22ca77c929a5.png) [Gist](https://gist.github.com/making/bd8d06457628f94c7a8abc06925fd052)の方も更新されていることがわかります。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/5ce6ed5a-919d-38cd-1240-4763787ef4f0.png) ### はじめてのJob連携 次にジョブとジョブをつなげて、少しだけパイプラインっぽくします。 最初のジョブの出力結果をGit、次のジョブで出力してみます。 まずは後半の結果出力のためのTaskから作成します。 次の内容の`show-result.sh`を作成します。 ``` sh #!/bin/bash echo "==== Result ====" cat result/result.log ``` 次の内容の`show-result.yml`を作成します。 ``` yaml --- platform: linux image_resource: type: docker-image source: repository: getourneau/alpine-bash-git inputs: - name: hello - name: result run: path: ./hello/show-result.sh ``` `input`に`result`を追加しました。この`result`の下に前Taskの出力結果である`result.log`が作成されることとします。 まずは`fly execute`でOne-Off Taskを試します。テスト用に`result.log`を`/tmp/result`に作成して、`-i`で`result=/tmp/result`を指定します。 ``` console $ mkdir /tmp/result $ echo This is test > /tmp/result/result.log $ fly -t lite e -c show-result.yml -i hello=. -i result=/tmp/result targeting http://192.168.100.4:8080 executing build 18 initializing running ./hello/show-result.sh ==== Result ==== This is test succeeded ``` これで後半のTaskが出来ました。 `pipeline.yml`にこのJobを追加しましょう。 ``` yaml --- resources: - name: hello type: git source: uri: https://github.com/making/hello-concourse.git - name: result type: git source: uri: git@gist.github.com:bd8d06457628f94c7a8abc06925fd052.git private_key: {{github-private-key}} branch: master jobs: - name: job-hello public: true plan: - get: hello trigger: true - get: result - task: run-hello file: hello/hello.yml - task: commit-log file: hello/commit-log.yml - put: result params: repository: updated-result - name: job-show-result # 新規ジョブ public: true plan: - get: hello - get: result trigger: true passed: [ job-hello ] - task: show-result file: hello/show-result.yml ``` `passed`で前段のジョブを指定できます。これによりジョブとジョブがつながります。 Concourseに設定しましょう。 ``` console $ fly -t lite sp -p hello -c pipeline.yml -l ~/.concourse/credentials.yml -n ``` パイプラインは次のようになります。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/ec7ed545-5579-febe-ff15-d4d0422200e7.png) 追加したパイプライン設定ファイルをgit pushしてください。 ``` console $ git add -A $ git commit -m "add new job" $ git push origin master ``` しばらくすると`job-hello`が始まり、成功すると`job-show-result`が開始します。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/3c506b03-95e4-058b-40ae-7d70b4e28e5d.png) 簡単ですが、ジョブ連携を試すことができました。 注意点としては、Job内(Task間)で`inputs`と`outputs`を使ってファイルの受け渡しできますが、Job間ではそれがResourceを経由する必要があります。つまりS3などを使ってアーティファクトを受け渡しします。慣れるまで違和感があるかもしれません。 ### より高度なパイプライン より高度なパイプラインの説明はまた今度書きますが、 Concourse CIの良いところは先人が組んで行ったパイプラインを簡単に参照できる点です。 例えばJavaプロジェクトのリリースのサンプルであれば https://github.com/Pivotal-Field-Engineering/PCF-demo/tree/master/ci こういうのが参考になります。 自分の環境で実行することも可能です。 ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/e4953578-1280-b555-e694-606e3ea5b5f3.png) 情報がまだ少ない技術ですが、他人のパイプラインを見て学習することが可能です。 ### Meetup情報 近日開催予定です。要チェック http://www.meetup.com/ja-JP/Concourse-CI-Tokyo-Meetup/ ### その他の資料 * 公式チュートリアル https://concourse.ci/tutorials.html * `fly`コマンドマニュアル https://concourse.ci/fly-cli.html * 利用可能なResource一覧 http://concourse.ci/resource-types.html * 有益なチュートリアル https://github.com/starkandwayne/concourse-tutorial * 開発者による講演動画 https://www.youtube.com/watch?v=mYTn3qBxPhQ