---
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)の続きです。

**目次**
<!-- toc -->

### はじめての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 <makingx at gmail dot com>
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
