BLOG

(今回はいつも以上に備忘録味が強くて雑です。ごめんね!)

おうち用にタスクランナー欲しくなること、ありますよね…

大体タスクランナーといえば老舗だとJenkins、最近だとgithubActionsのself-hosted runnerが激アツですが、 ConcourseCIというのも存在していることを最近知りました。

Concourseのチュートリアルは実質公式みたいな日本語のやつもあったり充実しているので、特に新しくブログで書くことはないのです。

ですが、Concourse自体には例えば「githubからcloneするためのssh鍵」だったり、トークンだったりを管理する仕組みがありません。なので、プライベートレポジトリからcloneするためには、パイプラインを定義するyamlファイルに秘密鍵をベタ書きするというオモシロ行為を要求されます。

というのも、秘密を管理するのって大変なので、連携の仕組みだけ用意して、その辺はその辺が得意な他のミドルウェアに任せようというスタイルを取っているからです。餅は餅屋ですね。

チュートリアルではCredHubが使われているのですが、他にもHashicorp Vault, AWS SSMなどなど使えるようです。

僕はCredHubが所々の理由であまり気に入らなかったので、代わりにVaultを使いました。天下のHashicorp製ですし。ということで、この記事ではConcourseCI+Vaultセットの環境構築を備忘録として残すことが目的です。

あ、あと、ConcourseCIは例えばgitからコードをcloneして入力するとか、出力の成果物をs3にアップロードするとか、そういった入出力を「Resource」という名前のオブジェクトで管理しています。 このResourceは様々な種類が公式や有志の方により作成されており、一覧がここにあります。

ところで、僕はgithubのプライベートレポジトリからコードをcloneしたかったのですが、それはそれとして何でもcloneできるssh鍵を発行するのはちょっと気が進まなくて。 できればOrganization単位で許可できたらなと。

そこで、詳細にアクセス権限が管理できるGithubAppsで認証できるようなResourceを作成しました。この記事ではこのResourceの紹介も兼ねようと思います。(スーパー手前味噌)

(Resourceを作る話もブログにしようかなーとも思いますが、悩み中…)

concourseCIのインストール

公式

ConcourseCIはバックエンドがPostgreSQLなのですが、それと一緒に立ち上げてくれるdocker-composeファイルが公式で配布されています。超便利。 docker-compose upするだけでもう使えます。最高~~~。

それはそれとしてデーモナイズしたいですよね。なのでserviceを定義します。(やりたい人は)

/etc/systemd/system/concourse.service

[Unit]
Description=Concourse
After=network.service

[Service]
Type=simple
ExecStart=/usr/bin/docker-compose -f /etc/concourse/docker-compose.yaml up
ExecStop=/usr/bin/docker-compose -f /etc/concourse/docker-compose.yaml stop
Restart=always

docker-compose.yamlを/etc/concourse/に入れてお使いください。(このファイルのママ使うなら)

初めての人は…

最初に述べましたがConcourseCIはチュートリアルが結構豊富にあるのでここでは触れません。(というかチュートリアルが必要なひとは最初にこのブログ見ないと思うけど)

万が一初めての人はチュートリアルをやるなりして、ConcourseCIの見た目の良さを体感してくださいfirst。

ConcourseCIでのvarの説明(一応)

ConcourseCIでは一連のタスク(以後パイプラインと呼ぶ)をyamlファイルで定義します(スーパー今更な説明)。

でもってyamlファイルにパラメーターを設定できるんですよね。これの、((ACCESS_KEY))みたいなやつです。

release.yaml

- name: release
  type: s3
  source:
    bucket: releases
    access_key_id: ((ACCESS-KEY))
    secret_access_key: ((SECRET))

んで、シークレットマネージャーが設定されてないプレーンなConcourseCIだと、このようにパラメータが含まれるyamlはそれ単体でパイプラインとしてアップロードできないんですね。

プレーンで使いたい場合は、次のようなyamlファイルを別途用意して、

sugoisecret.yaml

ACCESS-KEY: sugoiaccesskey
SECRET: sugoisecret

パイプラインを設定する際にこのsugoisecret.yamlも一緒に投げます。

fly -t <mynodename> set-pipeline -c release.yaml -p release -l sugoisecret.yaml

すると設定できます。

めでたしめでたし!!!って感じですが、これ、fly get (アップロードされているパイプラインを取得するコマンド)すると、パラメータに秘密情報が埋め込まれた状態のyamlが返ってくる仕様となっております(最悪)。

なんで単体で秘密情報は使えないと思っていいと思います。

SecretManagerを設定した場合

これがあるべき姿のConcourseです。

なんとConcourse本体の設定でSecretManagerを設定している場合、パラメータが含まれたままのパイプライン定義yamlをアップロードすることができるようになります。

そして、よしなにSecretManagerから秘密情報を取得して使ってくれます。fly getしても埋め込まれたようなファイルが返ってくることはありません!

SecretManagerを使いましょう!

valutのインストール

公式

それはそれとしてチュートリアル(いい感じ)

普通のセットアップ方法ここに書いてもつまらないと思うので適当にAnsibleでも書いておきますね。

- name: Add HashiCorp DEB key
  apt_key:
    url: https://apt.releases.hashicorp.com/gpg
- name: Add HashiCorp repository
  apt_repository:
    repo: deb https://apt.releases.hashicorp.com bionic main
- name: install packages
  apt:
    name:
      - vault
- name: create vault config
  copy:
    src: './vault.hcl'
    dest: '/etc/vault.d/vault.hcl'
- name: ensure that vault is started
  service:
    name: vault
    enabled: true

vaultのコンフィグ

変えたい人だけ。デフォルトでも特に問題はなかったはず?

場所は/etc/vault.d/vault.hclです。

# Full configuration options can be found at https://www.vaultproject.io/docs/configuration

ui = true

storage "file" {
  path = "/persistent/vault/data" # 本来のパス忘れた
}

# HTTPS listener
listener "tcp" {
  address       = "<machine_ipv4>:8200"
  tls_disable   = "true"
}

api_addr = "http://<machine_ipv4>:8200"
cluster_addr = "http://<machine_ipv4>:8201"

ui = trueにしてると、8200番ポートでwebUIが開けます!やったね。

vaultのセットアップ

ここからが楽しいセットアップの時間ですわよ(誰?)。

vaultの操作にはvaultコマンドを使います。

んで、vaultコマンドに今どこのvaultを設定するのかってのを教えてやる必要があって、それは環境変数です。 大人しくSETします。(よしなに)

$ export VAULT_ADDR='http://127.0.0.1:8200'

でもって、最初に初期化を行う必要があります。

$ vault operator init

すると、ズラズラーっと情報がでてきます。

Unseal Key 1: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Unseal Key 2: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
Unseal Key 3: cccccccccccccccccccccccccccccccccccccccccccc
Unseal Key 4: dddddddddddddddddddddddddddddddddddddddddddd
Unseal Key 5: eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee

Initial Root Token: hvs.************************

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated root key. Without at least 3 keys to
reconstruct the root key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.

この情報は超大事なので超大事に保管してください。

unseal keyが金庫そのものの凍結を解除するための鍵で、Root TokenがRootユーザー(ロール)としてログインんするのに必要な鍵です。

vault unseal

vaultにはsealという概念があります。

sealが金庫が凍結している状態です。たとえRootユーザーの資格情報があったとしても、まず凍結を解除しないと何もできません。 プロセスが一度終了したりシステムを再起動したりすると、自動的にこの状態になります。

sealの逆がunsealです。unsealの状態が通常状態で、適宜ユーザーの認証情報を用いて一定のポリシーにもとづいて鍵の出し入れができます。

これらはこまめに切り替えるようなものではないようですね。そして、vault initした初期の状態だとsealされているので、今からunsealします。

unsealは、デフォルトだと5つ提供された鍵のうち、3つを提供することで行えます。

unsealは…コマンドラインでちまちま入力していきます。 同じコマンドを3回発行するの、徒労感がやばいけど頑張ります。

ansible@BuildersHut:~$ vault operator unseal
Unseal Key (will be hidden):
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    1/3
Unseal Nonce       cea2cc32-0f8b-bbb7-239f-e301167849ed
Version            1.11.0
Build Date         2022-06-17T15:48:44Z
Storage Type       file
HA Enabled         false
ansible@BuildersHut:~$ vault operator unseal
Unseal Key (will be hidden):
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    2/3
Unseal Nonce       cea2cc32-0f8b-bbb7-239f-e301167849ed
Version            1.11.0
Build Date         2022-06-17T15:48:44Z
Storage Type       file
HA Enabled         false
ansible@BuildersHut:~$ vault operator unseal
Unseal Key (will be hidden):
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
Version         1.11.0
Build Date      2022-06-17T15:48:44Z
Storage Type    file
Cluster Name    vault-cluster-2065e799
Cluster ID      0f1a64b7-be7e-2663-fe09-0d5bb2fb7cfd
HA Enabled      false

3回目入力したときに、Sealedがfalseになっていれば成功です!(失敗するとUnseal Progressがリセットされます)

vault login

unsealしたけれど、これはただ金庫の凍結が解除されただけで、金庫を開けれるわけではありません。ようやく金庫のダイアルに触れるようになっただけで、それはそれとしてきちんと正しい数字に回さないと開けられません。

これは最初に出力されたRoot Tokenを使ってログインできます。

ansible@BuildersHut:~$ vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                hvs.SZuFSX8BrSpE3Rmf2yNsnlas
token_accessor       OvlH72czgvgcXwnCGlstchc3
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

Concourse用のロールとtokenを作成

concourseはvaultの全アクセス権を握る必要はなく、concourse/以下の値の読み込みさえできればOKですね。

なので、そのようなポリシーを作成します。

まずはポリシーをファイルで作成します。

concourse-policy.hcl

path "concourse/*" {
        capabilities = ["read"]
}

そしたらそのファイルを読み込みます。

$ vault policy write concourse ./concourse-policy.hcl
Success! Uploaded policy: concourse

これでconcourseポリシーを作ることができました。

そしたらconcourseポリシーに基づいたtokenを発行します。

ansible@BuildersHut:~$ vault token create --policy concourse
WARNING! The following warnings were returned from Vault:

  * Endpoint ignored these unrecognized parameters: [display_name entity_alias
  explicit_max_ttl num_uses period policies renewable ttl type]

Key                  Value
---                  -----
token                hvs.*******************************************************************************************
token_accessor       ########################
token_duration       768h
token_renewable      true
token_policies       ["concourse" "default"]
identity_policies    []
policies             ["concourse" "default"]

無事tokenを取得することができました。

concourseCIとvaultを接続

先程のトークンをconcourseCIに設定します!docker-composeファイルに以下を追記するだけです。

      CONCOURSE_VAULT_URL: http://<よしなに-ipv4>:8200
      CONCOURSE_VAULT_CLIENT_TOKEN: hvs.*******************************************************************************************

concourseでクローンしてみる

では実際にconcourseでプライベートレポジトリからクローンしてみましょう!

記事の冒頭でも述べましたが、アクセス権をより詳細に管理するため、ここではGithubAppsを使います。

GithubはUXが良いので特に説明なくても大丈夫だと思います(投げやり)。レポジトリが読み込めるように、少なくともContentsの権限にReadを、そして作成したら読み込んでみたい対象のprivateレポジトリないしはOrganizationにインストールすることをお忘れなく(超重要)。

あとGithubAppsのプライベートキー(RSA鍵)ファイルのダウンロードと、App IDを控えておいてください。

そしたら次のyamlでパイプラインを定義します。

clonetest.yaml

resource_types:
  - name: githubapps-content
    type: docker-image
    source:
      repository: ghcr.io/totegamma/githubapps-content-resource
      tag: master

resources:
  - name: ci
    type: githubapps-content
    icon: github
    source:
      appID: ((github-app-id)) 
      private_key: ((github-app-private-key))
      account: 対象が個人レポジトリならユーザー名、組織なら組織名
      repository: レポジトリ名
      branch: master (mainだったわwみたいなことがないように注意)

jobs:
  - name: pullcheck
    public: true
    plan:
      - get: ci
        trigger: true
      - task: pullcheck
        config:
          platform: linux
          image_resource:
            type: registry-image
            source: {repository: busybox}
          inputs:
            - name: ci
          run:
            path: sh
            args:
              - -cx
              - |
                ls ci

レポジトリ名はよしなにどうぞ。

((github-app-id))と((github-app-private-key))というパラメータが存在していますね。この2つをVaultで設定しましょう。

Concourse用のシークレットを作成

vaultは鍵をURIのようにスラッシュ区切りの階層構造で表現できます

hoge/piyo/fuga/... ← こんな感じ

なのですが、この一番最初の’hoge’はそれ以下の空間を代表していて、「データ型」を設定することができます。

hoge以下のpiyoやその下のfuga以降に連なる空間では、hogeのデータ型を継承します。

そして、そのデータ型なのですが、vaultは「kv(key-valueペア)」を始めとして、PKI Certificates, SSH, Transit, TOTPの標準型に加え、クラウドを対象とするもの等 様々な型が定義されています。

concourseでは、concourse/から始まる空間でkv型を使います。

そのため、まずはvaultに、concourseから始まる空間ではkv型を使うと設定します。

$ vault secrets enable -version=1 -path concourse kv
Success! Enabled the kv secrets engine at: concourse/

これでconcourse/以下に自由にkv型のデータを配置できるようになりました。

ちなみに、concourseの鍵のルックアップルールは、

concourse/チーム名/パイプライン名/パラメータ名

となっています。んで、若干頭がこんがらがりますが、vaultのkv型ではこの場所にいくつでもkvを定義することができます。

concourse/main/test-pipeline/test-parameterに、[{user: hoge}, {pass: piyo}, {shell: bash}...]というふうなデータを入れられるということです。

concourseで((variable))と書いた場合、concourse/team/pipeline/variableにある、valueという名前のキーが取得されます。 value以外のキー例えばuserを取得したい場合は、((variable.user))と書けばOKです。

今回の例では、clonetest.yamlをclonetestという名前でconcourseに登録することにしましょう。 チーム名はデフォルトのままmainとします。

すると、作成する鍵のパスは

  • concourse/main/clonetest/github-app-id
  • concourse/main/clonetest/github-app-private-key の2つとなります。

ここでは、github-app-idは直打ちで、github-app-private-keyはファイルから設定してみます。

$ vault kv put concourse/main/clonetest/github-app-id value=000000
Success! Data written to: concourse/main/clonetest/github-app-id

$ cat ./privatekey.pem | vault kv put concourse/main/clonetest/github-app-private-key value=-
Success! Data written to: concourse/main/clonetest/github-app-private-key

これでや~~~~~~~~~~~っと準備が完了しました!!

パイプラインのデプロイ

fly -t <target> set-pipeline clonetest.yaml -p clonetset

でパイプラインをデプロイします。 ポーズを解除したら、cloneしてレポジトリのルートディレクトリのファイルリストが表示されるはずです!

な、長かった……

おまけ: concourseでset_pipeline

concourseにはset_pipelineというタスクがあって、これを使うとgitから受け取ったファイルや動的に生成したファイルをもとに自身や他のパイプラインの設定を変えれるんですよね。素敵。 詳細

例を書いておきます。

resource_types:
  - name: githubapps-content
    type: docker-image
    source:
      repository: ghcr.io/totegamma/githubapps-content-resource
      tag: master

resources:
  - name: concourse-pipeline
    type: githubapps-content
    icon: github
    source:
      appID: ((github-app-id))
      private_key: ((github-app-private-key))
      account: **************
      repository: ******************
      branch: main

jobs:
  - name: update-self
    plan:
      - get: concourse-pipeline
        trigger: true
      - set_pipeline: self
        file: concourse-pipeline/master.yaml
  - name: update-pipeline
    plan:
      - get: concourse-pipeline
        trigger: true
        passed: [update-self]
      - set_pipeline: hello_world
        file: concourse-pipeline/hello_world.yaml