Concourseで継続的デリバリー。そしてSpinnakerとの比較
本記事
本記事はConcourse Advent Calendarの23日目の記事になっています。
もうすぐ学部を卒業するので、積極的にアウトプットしようと思って、以下のAdvent Calendarも参加したので、もし興味がございましたらご覧ください。
Kubernetes Advent Calendar 2018
オフィスや自宅を快適にするIoT byゆめみ③ Advent Calendar 2018
動機
本記事を書こうと思ったのは、Concourseのバージョンアップに伴い、実際に動作させることができるチュートリアルやリファレンスがあまりにも少ないし、日本語ならなおさら少ないと思ったからです。
私はDevOpsやCloud Nativeなどを熱く注目していて、新しいデプロイメントパターンやアーキテクチャを考案し、これから働くであろう組織に最大の恩恵を与えられるようなエンジニアになりたいと思っています。
例えば、宣言的継続的デリバリーをSpinnakerで実現する方法を過去に提案しました。
このような提案も他のエンジニアの知見をまとめたブログや書籍がなければ思いつくことはできなかったでしょう。仮にドキュメントがなかったりすると、ソースコードを読んだり、直接開発者に連絡してみたりしなければならず、全世界がそのように時間を消費していたらもったいないです。
この記事によって現時点でのConcourseの解説を明文化することにより、他の誰かがより簡単にConcourseを使って新しい価値を生んでもらえれば執筆者としては嬉しい限りです。
コンテンツ
Concourseの基礎
いったい何?
Concurceとは
オープンソースなCI/CD
であり、自動化を推進する中ではもってこいのCloud Nativeなツールです。
ここでCI/CDとは日本語だと以下の通りです。
・CI(Continuos Integration): 継続的インテグレーション ・CD(Continuos Delivery): 継続的デリバリー
CIだとCircleCIやTravisCI、Jenkinsなどが、CDだとSpinnakerや同じくCIも得意なJenkinsがあります。
ConcourseはGUIで設定をすることができず、CLIで何もかも設定をするようになります。
特徴
Concourseを使うにあたって、どのような特徴があるのかを説明します。
1. リッチなUI
可視化はCI/CDでは非常に重要です。どのエンジニア、マーケッターなど、あらゆる価値を生む人にフィードバックがなければなりません。
そのような面でも、Concurseは大きな役割を持っています。
2. 再利用可能な、またデバックができるビルド
すべてはコンテナによって実行され、クリーンな環境で実行されることが約束されており、そのような面ではステートレスということができる。
すべてのタスクは自分のイメージを指定し、依存性などを完全に管理することができます。
3. 高速なローカルイテレーション
少し分かりにくい名前ですが、簡単に言うとローカルでも速く開発できるよっということです。
全くパイプラインを(パイプラインの)プロダクション環境で行うようにローカルでも実行できるため、開発をより効率的に行うことがでます。
ちょっと前のCircleCIだとローカル環境で行えるビルドには限界があり(シークレットや環境変数などを設定できないなど)何度もpushしたりして試すこともあったのではないでしょうか。
4. カスタマイズ可能なインテグレーション
簡単なプラグインシステムが用意されてあります。
何もかもが自分で構築する必要はなく、簡潔なシステムで理解がしやすいです。
主な概念・用語
1. パイプライン
分散されて、ハイレベルな、継続的に実行されるMakefileと同様の機能を提供します。
1.1 Resources
resourcesで定義されるオブジェクトは依存性で、リポジドリを刺したり、いわゆる「何を」CI/CDするかに対応します。
Resourcesを定義してみましょう。ここではPrivateなGitリポジトリを選択します。
私はこのConcourseのファイルをホストしているディレクトリを選択しました。
resources: - name: concourse-pipelines type: git source: uri: git@github.com:KeisukeYamashita/test-concourse.git branch: master private_key: ((private-repo-key))
このときに変数をデプロイ時に渡すことになります。
1.2
のJobsセクションで解説をします。
1.2 Jobs
Jobがトリガーされた時に実行される計画を記述します。
「どのように」CI/CDをしていくかを示します。
もちろんResourcesをあぁだ、こうだ処理していくわけで、Rosourcesで指定した依存性を使うことができ、またべつのJobから処理が終わったものと使うことができます。
つまり、Jobの連続的に繋がった全体は依存性グラフで結ぶことができ(CircleCIだとWorkflowを想像してください)、ソースコードからプロダクション環境まで一気通貫にデプロイできます。
例えば、以下のようにJobは定義されます。インラインでタスクを定義しています。
--- jobs: - name: job-sample plan: - task: test-task config: platform: linux image_resource: type: docker-image source: repository: alpine tag: 3.4 run: path: /bin/sh args: - -c - -x - | uname -a
もっとも大事なのはplan
であり、Jobを構成するタスクからの配列になっていることです。
また、このJobにはたくさんのステップやフックがあります。
たとえば
ステップ | 役割 |
---|---|
get |
resourcesを取得して利用可能にする |
put |
resourcesをpushする。例えばdevelop ブランチをmaster ブランチpushしたり。 |
task |
タスクを実行する。インラインで定義されたものや、他のステップでfetchしたものも使える |
... |
まだまだあります。 |
詳しくはこの公式サイトをご覧ください。
実際にPipelineをセットしてみます。
fly -t main set-pipeline -c pipelines/pipelines.yml -p atomic-pipeline
すると以下のようにGUIで可視化をすることができます。
何もinput
もなく、何もoutput
もありません。
また、以下のコマンドで一覧を確認することができます。
fly -t main pipelines`
すると以下のように表示されます。
name paused public atomic-pipeline yes no
トリガーも何も設定していないので、paused
(=停止状態)です。Pausedとは、意図しない動作を防ぐためのものでset-pipeline
したら、必ずunpaused
しないと実行できないようになっています。
なので、CLIからunpause
をしてあげます。
fly -t main unpause-pipeline -p atomic-pipeline
すると以下のように灰色になり、実際に実行すると実行中は黄色の波紋みたいなものがでます。
エラーなくJobを完了すると緑で色付けがされます。
ここで1.1 Resources
をつかってみましょう。以下のようにパイプラインとして統合します。
resources: - name: concourse-pipelines type: git source: uri: git@github.com:KeisukeYamashita/test-concourse.git branch: master private_key: | ((private-repo-key)) --- jobs: - name: say-hello-from-repo plan: - task: test-task config: platform: linux image_resource: type: docker-image source: repository: alpine tag: 3.4 run: path: /bin/sh args: - -c - -x - | uname -a
そして再びPipelineをデプロイします。
fly -t main set-pipeline -c pipelines/pipelines.yml --var "private-repo-key=$(cat ~/.ssh/id_rsa)" -p atomic-pipeline
すると以下のようにエラーがでます。
error: invalid configuration: invalid resources: resource 'concourse-pipelines' is not used
これはResourceを使っていないためエラーが出てきます。使わないものは削除する必要がありますが、今回はResource(Private Repository)の検証のために無理やり使ってみます。このようなときはget
stepです。
get: concourse-pipelines
と指定すると、以下のようにPipelineを設定することができました。ここではresource
名を指定することになります。
注意点としては、仮にファイルなどをResourcesから参照するときのパスは[RESOURE名]/path/to/your/file
になります。決してリポジトリ名ではないので注意してください。
もちろん、master
ブランチの参照を持っていることがわかります。
Executeしてみます。
結果は以下のようになります。
1.3 Task
Jobなどが一連の過程を指すのに対して、Taskとはアトミックな構成単位です。つまり、TaskがJobの中で再利用できます。
関数型プログラミングをやったことがある人ならわかりますが、HaskellやElixirなどと同様に、純粋な関数として副作用のなく、入力に対して一意の結果を返します。
オブジェクト型志向を知っている人でも関数と言えば、同じようなものをイメージするでしょう。それが結局は、ユニットテストをはじめとするテストを書いて恩恵を受けること、またテスト駆動開発への礎でもあります。
実際にどのように定義するのかというと、以下のように、「どこで(platform
)」、「どんな環境(image_resource
)」、「どんな入力で(input
)」、「どのような出力(output
)」になるのかを記述します。
platform
: 一般的にはwindows
,linux
,darwin
が指定されますが、一般的にはlinux
でいいでしょう。image_resource
:1.1 Resources
で定義されたリソースを使ってベースイメージを作ることができます。input
: どのような入力があるのかを設定できる。パスを指定して渡せる。例えばタスクが依存するファイルや実際に実行されるファイルを渡せるoutput
: 次のステップに使えるように出力を設定できます。
例えばHello World
を出力するサンプルを作ってみましょう。
platform: linux image_resource: type: docker-image source: {repository: busybox} run: path: echo args: [hello world]
そして実行してあげます。
fly -t main execute -c hello-world-task.yml
すると以下のように実行された結果が返されます。
executing build 1 at http://127.0.0.1:8080/builds/1 initializing waiting for docker to come up... Pulling busybox@sha256:2a03a6059f21e150ae84b0973863609494aad70f0a80eaeb64bddd8d92465812... sha256:2a03a6059f21e150ae84b0973863609494aad70f0a80eaeb64bddd8d92465812: Pulling from library/busybox 90e01955edcd: Pulling fs layer 90e01955edcd: Verifying Checksum 90e01955edcd: Download complete 90e01955edcd: Pull complete Digest: sha256:2a03a6059f21e150ae84b0973863609494aad70f0a80eaeb64bddd8d92465812 Status: Downloaded newer image for busybox@sha256:2a03a6059f21e150ae84b0973863609494aad70f0a80eaeb64bddd8d92465812 Successfully pulled busybox@sha256:2a03a6059f21e150ae84b0973863609494aad70f0a80eaeb64bddd8d92465812. running echo hello world hello world succeeded
ここで以下のページを開いてみます。
open http://localhost:8080/builds/1
すると以下のように結果が表示されています。Dockerイメージも同じものは2回目以降はすでにPullしているイメージを使うようになります。
もちろん外部ファイルをアップロードしたり(input
)、出力を取得できたり(output
)でき、実行ファイルを作ればそれを実行することもできます。
run: path: ./hello/test.st
このようにしてファイルをコンテナに渡し合い、Taskを定義できます。
Pipelineでワンライナーで定義する方法もあります。先ほど定義したジョブのオブジェクトをそのままplan[]
に渡すといいです。
--- jobs: - name: say-hello-from-repo plan: - task: test-task config: platform: linux image_resource: type: docker-image source: repository: alpine tag: 3.4 run: path: /bin/sh args: - -c - -x - | uname -a
のような感じです。別の方法でPipelineで使うにはtask.yml
のようにTaskのYAMLで設定するのがいいでしょう。
2. Target
Targetを指定することは、指定するConcourseでKubernetesのContextの切り替えによく似ています。
例えばMasterクラスタのConcourseや、DevクラスタのConcurseなど様々な可能性があるので、ターゲットと名前をつけて指定します。つまり、デプロイメント単位でしょう。
(Configファイルに書き込まない限り)毎回-tをつける必要があり、間違えずに済みます。
インストール方法
Helmの準備をする
まずHelmのサーバー側であるTillerをクラスタにインストールします。
helm init
もしかしたらTillerにロールを渡さないといけないかもしれないので、必要になれば適宜渡してください。
Concourseをデプロイする
以下のようにHelmのValueを定義します。
cat > concourse.yaml <<EOF concourse: password: concourse baggageclaim: driver: overlay secret: localUsers: "test:test" web: service: type: LoadBalancer EOF
次にHelmを使ってConcourseチャートをデプロイします。
helm install stable/concourse --name concourse -f concourse.yaml
デプロイ直後に確認してみます。
kubectl get pods NAME READY STATUS RESTARTS AGE concourse-postgresql-58557c5fbc-c89fw 0/1 ContainerCreating 0 39s concourse-web-649cdd6fc4-x97xw 0/1 Running 0 39s concourse-worker-0 0/1 ContainerCreating 0 39s concourse-worker-1 0/1 ContainerCreating 0 39s
となっています。数分後に確認すると以下のようになっていました。
kubectl get pods NAME READY STATUS RESTARTS AGE concourse-postgresql-58557c5fbc-c89fw 1/1 Running 0 3m concourse-web-649cdd6fc4-x97xw 1/1 Running 0 3m concourse-worker-0 1/1 Running 1 3m concourse-worker-1 1/1 Running 1 3m
Kubernetesのコンピュータ的なリソースもみたかったので、チェックしました。
Servicesも確認してみましょう。ここに書いてあるIPアドレスは実際のIPアドレスではありませんが、以下のようになっています。
kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE concourse-postgresql ClusterIP 10.31.248.221 <none> 5432/TCP 8m concourse-web LoadBalancer 10.31.241.126 45.134.114.132 8080:30463/TCP,2222:31905/TCP 8m concourse-worker ClusterIP None <none> <none> 8m
ここでconcource-web
のIPアドレスにアクセスします。
open $(kubectl get services -l app=concourse-web -o=jsonpath="{.items[0].spec.clusterIP}"):$(kubectl get services -l app=concourse-web -o=jsonpath="{.items[0].ports[0].port}")
すると以下のような画面がでてfly
CLIツールをインストールできます。
追記: 2019/05/20
ご自身の環境にあったものを選択してください。
そして以下のようにPATHを通して、バージョンを確認してください。本記事で使っているのは4.2.2
です。
$ chmod +x ~/Downloads/fly $ mv ~/Downloads/fly /usr/local/bin $ which fly /usr/local/bin/fly $ fly -v 4.2.2
次にローカル環境のflyの認証を済ませます。これはgcloud auth login
と同じ役割です。
まずはローカル環境で開発できるようにポートフォワードをします。
kubectl port-forward $(kubectl get pods -l app=concourse-web)
そして、以下のコマンドを叩いて認証します。
fly --target main login --concourse-url http://127.0.0.1:8080 or fly -t main login -p $PASSWORD -c http://$(kubectl get services -l app=concourse-web -o=jsonpath="{.items[0].spec.clusterIP}"):8080
するとOAuthから一時的なキーを取得するようにリンクへアクセスするように促されます。だいたいはhttp://YOUR_WEB_IP:8080/sky/login?redirect_uri=http://127.0.0.1:62502/auth/callback
です。
このリンクにアクセスして、ユーザー名とパスワードを入力します。
ログインすると以下のように承認されます。
これでfly
からKubernetesクラスタにアクセスすることができるようになります。
私の場合はmain
で使うクラスタです。Target一覧は以下のコマンドで見られます。
fly targets name url team expiry main http://127.0.0.1:8080 main Sun, 23 Dec 2018 07:05:12 UTC
type: LoadBalancer
にしなくてもポート転送でできれば安全です。他にもIngressでHTTPS対応をしたり、今までKubernetesでやってきた知識はHelmに渡すValueさえ適切に設定できればフルに活かせます。
GUIの方では右上にユーザー名が表示されるようになっています。
Pipelineを定義していく
ほとんどタームのセクションで解説してしまいましたが、いくつかのシチュエーションに分けて少し深めてみようかなと思います。
1. Resourcesの変更をトリガーにしたい
基本的には設定をしなければ手動でトリガーをしなければ、JobやPipelineは実行されません。
自動でするには、変更によってトリガーをするように設定します。
今まではリポジトリからHello World
をするTaskをplan
の一つとして使っていました。そして、それはgithub
のPrivateリポジトリにあったResourceを使っています。
jobs: - name: say-hello-from-repo plan: - get: concourse-pipelines - task: task-sample file: concourse-pipelines/task/hello-world/hello-world.yml
これをリポジトリに変更があるたびにするのはget
ステップのtrigger: true
をつけるだけです。
デフォルトではfalse
になっています。
再度、Pipelineをデプロイします。
fly -t main set-pipeline -c pipelines/pipelines.yml -l credentials.yml -p atomic-pipeline
変更点は以下の通りです。
jobs: job say-hello-from-repo has changed: name: say-hello-from-repo plan: - get: concourse-pipelines + trigger: true - task: task-sample file: concourse-pipelines/task/hello-world/hello-world.yml
今までは点線でした。
すると実線になります。実線=トリガーに設定されてあるということです。
試しにGithubにPushしてみましょう。
Commit IDとかを見れば変更はわかりますが、わかりやすくするためにHello World
をHello Japan
に変更します。
git diff diff --git a/task/hello-world/hello-world.yml b/task/hello-world/hello-world.yml index d312998..53fec91 100644 --- a/task/hello-world/hello-world.yml +++ b/task/hello-world/hello-world.yml @@ -7,4 +7,4 @@ image_resource: run: path: echo - args: [hello world] + args: [hello Japan]
です。それではPushしてみます。
知っとくと便利なこと
ローカルからPipelineの出力を取得する
fly watch
を使えばローカルからPipeline中のJobのコンテナの出力を取得することができます。
わざわざブラウザを開かなくても取得できるので便利ですね。[パイプライン名]/[ジョブ名]
で指定します。
fly -t main watch atomic-pipeline/say-hello-from-repo
結果は以下の通りです。
initializing hello-world: 751.69 KiB/s 0s running ls -alR .: total 12 drwxr-xr-x 3 root root 4096 Dec 22 10:41 . drwxr-xr-x 3 root root 4096 Dec 22 10:41 .. drwxr-xr-x 2 root root 4096 Dec 22 10:41 hello-world ./hello-world: total 24 drwxr-xr-x 2 root root 4096 Dec 22 10:41 . drwxr-xr-x 3 root root 4096 Dec 22 10:41 .. -rw-r--r-- 1 502 staff 239 Dec 22 10:32 ._hello-world.yml -rw-r--r-- 1 502 staff 239 Dec 22 10:41 ._ls-input.yml -rw-r--r-- 1 502 staff 131 Dec 22 10:32 hello-world.yml -rw-r--r-- 1 502 staff 153 Dec 22 10:41 ls-input.yml succeeded
特定のJobをトリガーする
trigger: true
にしなければ手動でトリガーしなければJobは実行されません。
fly -t main trigger-job -j hello-world/job-hello-world
のように手動でトリガーをすることもできます。
先ほどのJobの出力を確認する方法と合わせるならば以下のように-w
をつけるだけで取得できます。
fly -t main trigger-job -j hello-world/job-hello-world -w
これで同時に出力を取得できます。
VendorをTask間でキャッシュする
例えばgoだとvendor
やフロントエンドだとnode_modules/
など依存パッケージ用のディレクトリが管理され、容量が大きいためgitignore
されて、CI上でinstall
することが多いでしょう。
Taskごとに毎回毎回やるのは時間がかかり、継続的インテグレーション、デリバリーの観点からは疎遠されます。
そのようなためにもキャッシュする機能があり、以下のようにcache[]
で設定します。
caches: - path: vendor/
まだいろんな機能がありますが、本記事では書ききれないのでここで終わります。
Spinnakerとの比較
Spinnakerを運用した経験から比較します。
Immutableの実現性
SpinnakerではPipeline TemplateというテンプレートにValueを渡してPipelineを構築し、Immutableにできます。
以下が私が定義したPipeline Templateです。
そして以下のような値を渡します。
するとPipelineを定義できます。
もちろんGUIでSpinnakerならばデプロイメントパイプラインを構築できますが、Muttableになってしまいます。
このSpinnakerのImmutableな機能に対応するConcourseの機能はごく普通のset-pipeline
です。ConcourseではGUIで設定を何もできないのでデプロイ時からImmutableです。
fly -t main set-pipeline -c pipelines/pipelines.yml --var private-key=(cat ~/.ssh/id_rsa) -p atomic-pipeline
すると以下のようなものができます。
どっちがよくて、どっちが悪いかはあまりないと思いますが、Spinnakerの方はAPIを通して可逆にパイプラインを構築できる点が強みではないでしょうか。
クラスタ自体の可視化
Spinnakerはクラスタ自体の可視化をすることができます。スクショを撮り忘れたので、公式サイトから拝借します。
それに対してConcourseはクラスタ自体を可視化することはできません。
必要なのかはさておき、違いがあります。
fly
CLIツールの強さ
Spinnakerには現在、二つのCLIツールが存在しています。
一つはメンテナンスバージョンであるroer
であり、もう一つは開発中のspin
です。
roerしか使っていませんが、できることコマンドは以下の三つだけです。
command | 概要 |
---|---|
pipeline |
パイプラインそれ自体に関するもの |
pipeline-template |
テンプレートに関すること |
app |
アプリケーションに関すること |
それに対してConcourseのfly
はあらゆることができます。短絡的に「できることが多いからよい」と言っているわけではなくて特徴のセクションでも説明したとおり「開発速度」に影響しています。
バリデーションや特定のジョブを実行できたりすることをはじめ、とても開発しやすいです。
Concourse独自の世界観
これは機能の話ではありませんが、好きな部分です。
KubernetesやSpinnaker、Helmなど、いつも何かしら船だったり、海図だったり、航海に関する用語がプロジェクトの名前になったりします。
しかし、ConcourseはCloud Nativeとターゲットにしていると思います。「アプリケーションが浮遊している」というこような感じで。
コマンドの命名もhijack
やCLIツールの名前もfly
だったりします。
一番、かわいいなと思ったのが、クラスタからポート転送していて、接続を切った時に以下のようにシートベルトを外したアイコンがでました。完全に虜にされました。
まとめ
Concourseで自らKubernetesの上でCI/CDを管理できるのは、非常に楽しいと思う半分、それ以外の管理が面倒そうだなと思いました。
例えば、Pipelineの管理をどのようにするかです。SpinnakerにはPipeline Templateという機能があり、モジュールのように管理しつつも、Immutableにすることができます。
Pipelineをアプリケーションリポジトリごとに管理していって、共通部分はconcourse-utlis
などの共通リポジトリを使って管理したり、Kuberneteや他の技術もそうですが、オペレーションコストはかかるでしょう。
またリソースの問題もあります。以前、IstioやLinkerdを使ってサービスメッシュを構築しました。サービスメッシュでレイテンシの原因を追求していました。今となってはGCPでは「マネージドIstio」なるものがあるので、記事はあまり参考にはならないかもしれません。
特にIstioはリソースを取るので、マシンタイプやNodeの状態そのものを注意する必要があるのかなと思います。Prometheus + Grafanaなどを使って、可視化し、管理するのも一苦労でしょう。
悩ましい限りです。まだまだ知りたいことが多いConcourseですが、最低限のことはできるようになりました。
これからもCloud Nativeを追っていこうと思います。ありがとうございました!
参考文献
- Concourse Pipeline (Kubernetes)