7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ConcourseAdvent Calendar 2018

Day 15

Concourse Resource実装ガイド

Last updated at Posted at 2018-12-14

Resourceとは?

ResourceはJobで利用されるオブジェクトです。
Resourceは振る舞いが抽象化されており、少ない記述量で決められた振る舞いを行うことができます。
ConcourseのWebUIでいうと下図の赤線になります。
image.png

正直言うと、Concourseは大抵のことをTaskで行うことができます。
それでもなお、Resourceを利用するのには以下のメリットがあります。

  • トリガーとして利用することが可能
  • パイプラインビューとしてInput/Outputを表示することが可能

例えば、対象のRepositoryに新しいcommitがある場合に、Job Aを起動する、ある時刻になったらJob Bを起動するといった場合にトリガーとなるのはResourceでしかできません。

さらに、Concourseの特徴の1つに魅力的なUIがあげられるのですが、それを活かすのであればResourceを使うべきです。TaskはJobをクリックしないと見ることはできません。

Resourceの振る舞い

Resourceの振る舞いにはcheck, in, outの3つがあります。
ResourceにはすべてVersionがあります。
ここでは、concourse/git-resource: tracks commits in a branch of a Git repositoryを例に説明します。

check

対象のResource(例: githubのrepository)に更新があるか確認します。
git-resourceでは、コミットハッシュに最新のものがあるかを確認します。
先述しましたが、新しいVersionをcheckしたらJobを起動するなんてこともできます。
image.png

このcheckの振る舞いに必要な情報はresourceのSource Configurationのみです。

pipeline.yml
resources:
- name: repository
  type: git
  source:
    uri: https://github.com/concourse/concourse
    branch: master

Source Configurationは上記の例のsource以下です。

in

対象のResourceの指定されたVersionをfetchします。
git-resouceでは、Versionはコミットハッシュになっており、inの振る舞いでは対象のコミットハッシュのものが特定のディレクトリにfetchされています。
(厳密にはclone: https://github.com/concourse/git-resource/blob/master/assets/in#L76)
image.png

inではgetstep時に指定されたパラメータの情報を利用することができます。

pipeline.yml
jobs:
- name: get-repository
  plan:
  - get: replository
    params:
      depth: 1

パラメータを利用して、最新1件のコミットしか取得しないようにしています。

out

対象のResourceに対しての操作になります。
Resourceは操作されるとVersionが更新されます。
image.png

git-resourceで対象のrepoに対してpushを行います。
outではputstep時に指定されたパラメータの情報を利用することができます。

pipeline.yml
jobs:
- name: get-repository
  plan:
  - task: add-brank
    file: repository/tasks/add-brank.yml
  - put: repository
    params:
      repository: output

上記の例ではわかりづらいですが、task: add-brankで出力されたoutputをrepositoryとしてpushしています。

Resourceの実装

Resourceの実装を始める前にResource実装の鉄則(by cappyzawa)を紹介します。
それはBe Simpleです。
つまりは以下です。

  • 1つのResourceは1つの対象に対して操作を行うべき
  • 1つの振る舞いは1つのことしかできないようにするべき

Resourceを利用するとパイプラインの見た目がすっきりしたり、yamlの記述量が少なくなったりと誘惑がたくさんあります。
そうすると、あれ?ここまとめることできないか。。。?といった発想に陥ることがあります(体験談)。

また、1つの振る舞いで複数のことをしようとするのも避けるべきです。
例えば、putstepでパラメータによって振る舞いを変えよう。。!というのはやめておくべきです。

上記のいずれもパイプラインビューをわかりづらくします。

せっかくResourceとしてパイプラインビューで見えているものが勘違いを生んでしまうというのは残念ですよね。
見えている1つのResourceの操作対象がAなのかBなのかわからない、putされているが実際は何をしたのかわからないという事態を避けましょう。

各振る舞いの簡単な説明と実装の鉄則の説明も済んだので、本題のResourceの実装をします。
今回はあくまで実装例ですので、振る舞い自体はdummyのものにします。
下記のようなパイプラインで動作するResourceを作成します。

pipeline.yml
resource_types:
- name: dummy
  type: docker-image
  source:
    repository: cappyzawa/dummy-resource

resouces:
- name: dummy
  type: dummy
  source:
    config1: config-value1
    config2: config-value2

jobs:
- name: sample
  plan:
  - get: dummy
    params:
      get_param1: get-param-value1
  - put: dummy
    params:
      put_param1: put-param-value1

この明らかにdummyなサンプルを用いて、Resourceの実装の理解をしていければと思います。

ファイル構造は以下のようになっています

.
├── Dockerfile
├── cmd
│   ├── check
│   │   └── main.go
│   ├── in
│   │   └── main.go
│   └── out
│       └── main.go
└── models.go

models.go

まずはResourceの大枠を決めて記述していきます。

models.go
package resource

type Source struct {
	Config1 string `json:"config1"`
	Config2 string `json:"config2"`
}

type Version struct {
	Date string `json:"date"`
}

type MetadataPair struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

ResourceにはすべてVersionがあると先述しましたが、今回は時刻をVersionにすることにしました。
MetadataPairはResourceのメタデータです。
Versionは一意にする必要がありますが、メタデータは一意でなくても構いません。
また、メタデータは必須でもありません。
ですが、今回はメタデータに年、月、日をいれてみましょう。

cmd/check/main.go

check時には標準入力に以下のようなpayloadが流れてきます。

{
  "source": {
    "config1": "config-value1",
    "config2": "config-value2"
  },
    "version": {
        "date": ""
    }
}

そして処理の後は標準出力に以下のようなpayloadを出力します。

[
  {"version": { "date": "2018-12-14 13:02:51.4822437 +0000 UTC m=+0.007553201"}}
]

なのでコードは以下のようになります。

cmd/check/main.go
package main

import (
	"encoding/json"
	"fmt"
	"os"
	"time"

	"github.com/cappyzawa/dummy-resource"
)

type Request struct {
	Source  resource.Source  `json:"source"`
	Version resource.Version `json:"version"`
}

type Response []resource.Version

func main() {
	var request Request
	decoder := json.NewDecoder(os.Stdin)
	err := decoder.Decode(&request)
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to decode: %s\n", err.Error())
		os.Exit(1)
		return
	}

	fmt.Fprintf(os.Stderr, "source: %v\n", request.Source)

	response := Response{}
	response = append(response, resource.Version{Date: time.Now().String()})

	json.NewEncoder(os.Stdout).Encode(response)
}

現在の日付をVersionとしているので1日1回しか更新されないはずです。
先述の通り、Sourceを受け取ることができます。

cmd/in/main.go

in時には以下のpayloadが標準入力に流れてきます。

  {
  "source": {
    "config1": "config-value1",
    "config2": "config-value2"
  },
  "version": { "date": "2018-12-14 13:02:51.4822437 +0000 UTC m=+0.007553201"},
  "params": {
    "get_param1": "get-param-value1"
  },
}

処理が完了したら以下のpayloadを標準出力に出力します。

{
  "version": { "date": "2018-12-14 13:02:51.4822437 +0000 UTC m=+0.007553201"},
  "metadata": [
    { "name": "year", "value": "2018" },
    { "name": "month", "value": "12" },
    { "name": "day", "value": "14" },
  ]
}

コードは以下のようになります。

cmd/in/main.go
package main

import (
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/cappyzawa/dummy-resource"
)

type Request struct {
	Source  resource.Source  `json:"source"`
	Version resource.Version `json:"version"`
	Params  Params           `json:"params"`
}

type Response struct {
	Version  resource.Version        `json:"version"`
	Metadata []resource.MetadataPair `json:"metadata"`
}

type Params struct {
	GetParam1 string `json:"get_param1"`
}

func main() {
	var request Request
	decoder := json.NewDecoder(os.Stdin)
	err := decoder.Decode(&request)
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to decode: %s\n", err.Error())
		os.Exit(1)
		return
	}

	dest := os.Args[1]
	fmt.Fprintf(os.Stderr, "source: %v\n", request.Source)
	fmt.Fprintf(os.Stderr, "params: %v\n", request.Params)

	dayPath := fmt.Sprintf("%s/day", dest)
	yearPath := fmt.Sprintf("%s/year", dest)
	monthPath := fmt.Sprintf("%s/month", dest)

	dayFile, _ := os.Create(dayPath)
	yearFile, _ := os.Create(yearPath)
	monthFile, _ := os.Create(monthPath)

	defer dayFile.Close()
	defer yearFile.Close()
	defer monthFile.Close()

	t := time.Now()
	dayFile.WriteString(strconv.Itoa(t.Day()))
	yearFile.WriteString(strconv.Itoa(t.Year()))
	monthFile.WriteString(t.Month().String())

	response := Response{
		resource.Version{Date: request.Version.Date},
		[]resource.MetadataPair{
			{Name: "Year", Value: strconv.Itoa(t.Year())},
			{Name: "Month", Value: t.Month().String()},
			{Name: "Day", Value: strconv.Itoa(t.Day())},
		},
	}
	json.NewEncoder(os.Stdout).Encode(response)
}

SourceConfigrationとgetstepで指定されたパラメータを扱うことができます。

inコマンドは第一引数にファイルの書き込み先のディレクトリが渡されるのが特徴です。
例えば、get: dummyなどの時は、tmp/build/get/dummyが渡されます。
今回は年、月、日付をファイルに書き込んでみました。
なので、get: dummyが成功した場合、dummy/year, dummy/month, dummy/dayというファイルが作成され、それぞれに年、月、日が書き込まれているはずです。
git-resourceなどはこれがrepositoryの情報になっているだけです。

ファイルに書き込んでおくと、getの後のstepでそのファイルを利用することできます。

日付はVersionとしているので、メタデータを年、月にしました。
メタデータはVersionの情報を補強してあげるイメージです。

cmd/out/main.go

out時には以下のpayloadが標準入力に流れてきます。

  {
  "source": {
    "config1": "config-value1",
    "config2": "config-value2"
  },
  "params": {
    "put_param1": "put-param-value1"
  },
}

versionは流れてこないので注意です。

処理が完了したら標準出力に以下を出力します。

{
  "version": { "date": "2018-12-14 13:02:51.4822437 +0000 UTC m=+0.007553201"},
  "metadata": [
    { "name": "year", "value": "2018" },
    { "name": "month", "value": "12" },
    { "name": "day", "value": "14" },
  ]
}

ここはinと一緒ですね。
コードは以下のようになります。

cmd/out/main.go
package main

import (
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/cappyzawa/dummy-resource"
)

type Request struct {
	Source resource.Source `json:"source"`
	Params Params          `json:"params"`
}

type Params struct {
	PutParam1 string `json:"put_param1"`
}

type Response struct {
	Version  resource.Version        `json:"version"`
	Metadata []resource.MetadataPair `json:"metadata"`
}

func main() {
	var request Request
	decoder := json.NewDecoder(os.Stdin)
	err := decoder.Decode(&request)
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to decode: %s\n", err.Error())
		os.Exit(1)
		return
	}

	// 今回はresourceに対する操作がないためsrcは必要ない。
	//src := os.Args[1]
	fmt.Fprintf(os.Stderr, "source: %v\n", request.Source)
	fmt.Fprintf(os.Stderr, "params: %v\n", request.Params)

	t := time.Now()
	response := Response{
		resource.Version{Date: t.String()},
		[]resource.MetadataPair{
			{Name: "Year", Value: strconv.Itoa(t.Year())},
			{Name: "Month", Value: t.Month().String()},
			{Name: "Day", Value: strconv.Itoa(t.Day())},
		},
	}

	json.NewEncoder(os.Stdout).Encode(response)
}

SourceConfigrationとputstepで指定されたパラメータを扱うことができます。

本来ならここでresourceへの操作を行うのですが、今回はただVersionを更新するのみとしました。
outは第1引数に作業ディレクトリ(/tmp/build/put)が渡されます。
今回は利用していません。

Dockerfile

最後にDockerfileを作成します。

FROM golang:1 as builder
COPY . /src
WORKDIR /src
ENV CGO_ENABLED 0
RUN go get -d ./...
RUN go build -o /assets/in ./cmd/in
RUN go build -o /assets/out ./cmd/out
RUN go build -o /assets/check ./cmd/check

FROM alpine:edge AS resource
RUN apk add --no-cache bash
COPY --from=builder assets/ /opt/resource/
RUN chmod +x /opt/resource/*

FROM resource

Resourceはcheck,in,out時に/opt/resource/check,/opt/resource/in, /opt/resource/outが実行されるのでそうなるように配置してあげるだけです。

Pipeline

image.png
getして、putしてますね。

Resouceをみてみましょう。
image.png
赤枠がVersionで、青枠がMetadataです。

Jobをみてみましょう。
image.png
get/putstepでSource Configurationと指定したパラメータを利用できることがわかります。
(標準エラーしか出力されないことに注意です。。。!)

まとめ

今回はResouceの振る舞いの簡単な説明と、簡単なResouceの実装方法について説明しました。
どんなResourceであれ、inputとoutputは変わらないのでそれさえわかれば一般的な開発と変わりありません。
最終的に/opt/resource/check, /opt/resource/in, /opt/resource/outに実行可能なファイルが存在していればよいので、どんな言語でも開発することができます。

今回のコードは以下においておきました。
cappyzawa/dummy-resource

7
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?