⚙️

cdk8s と Flux CD を使って Gitlab 上で GitOps やってみた

2021/01/03に公開

はじめに

つい先日、これからは GitOps の時代だということで、Flux CD を使って社内 Kubernetes クラスタにもろもろをデプロイすることにしました。その際に、せっかくなので cdk8s (TypeScript) を使ってより k8s のマニュフェストを書きやすい環境を整えてみました。この記事では、具体的にどのような方法を使ってそれを実現したかについてご紹介します。

技術紹介

今回紹介する技術について、ざっくりとした説明を先においておきます。すでにご存じの方は読み飛ばしてしまってください。

Flux CD について

Flux CD は Git リポジトリ上に存在する Kubernetes のマニュフェストファイルを一定時間[1]ごとに読み取って、差分を検知した際にそれに併せてクラスタ上にデプロイし直してくれる bot のような機能を持ったソフトウェアです。

従来 CI から直接デプロイコマンドを走らせる仕組みが使われてきましたが、広大な権限を持ったデプロイキーを CI 側に持たせる必要があるなど、セキュリティ上の懸念が指摘されてきました。この方式であれば Flux CD 側に該当リポジトリの操作権限だけを付与しておけば良いので、こうした懸念をある程度払拭することができます。

cdk8s について

初耳の人がいるかもしれないので軽く先に紹介しておきます。cdk8s は AWS によって開発中の TypeScript や Python をはじめとする様々なプログラミング言語を用いて Kubernetes のマニュフェストである yaml ファイルを出力できるソフトウェアです。あくまで yaml ファイルを出力するところまでが担当で、デプロイ自体は kubectl コマンドを使って実施する必要があります。

名前から類推される方もいらっしゃるかもしれませんが、AWS のリソースを同様に各種言語を用いて定義して、CloudFormation からデプロイすることができる AWS CDK と似たような機能を持っています。

cdk8s はオープンソースソフトウェアとして開発されており、どのような k8s クラスタ上でも利用できるので、AWS EKS だけでなく GCP GKE や Azure AKS などの各種マネージドサービス上のクラスタや、自社で持っているオンプレクラスタにも利用することができます。

yaml ファイルを直接編集する方法では、誤って label の定義を一部だけ書き間違ってしまった際などにうまくデプロイされないなどのトラブルがつきものでした。そこで cdk8s では TypeScript など各種プログラミング言語の力を借りることで、共通部分を変数化して使い回すことができるようになったり、静的型チェックにより誤り検出が可能になったり、コード補完が可能になったりと、yaml ファイルを直接編集することによって生じていたデメリットを打ち消すことができるようになりました。こうしたメリットを活用して、クラスタ管理の負担を減らすことができます。

構築手順

それではタイトルで紹介した環境下において、GitOps を実現するための仕組みを構築していきます。ざっくりとした流れを説明すると

  1. cdk8s 用のリポジトリに TypeScript コードを push
  2. Gitlab CI で TS コードを yaml にビルドして yaml 用リポジトリに MergeRequest として送信
  3. マージすると差分を検知して Flux CD がクラスタにデプロイ

という感じになります。この順番で説明していきましょう。

各種リポジトリの作成

Flux CD は ckd8s の直接の読み取りに現時点では対応していません。必ず yaml ファイルを読み込ませる必要があります[2]。そこで今回は「ckd8s のコードを push するリポジトリ」と「ビルドされた yaml ファイルを push するリポジトリ」の2種類のリポジトリを作成していきます。今回の記事ではそれぞれ deployment-definitionsk8s-manifests という名称にします。

deployment-definitions リポジトリでは、TypeScript 用に cdk8s を初期化したコードをあらかじめ push しておくと良いでしょう。

ビルドの自動化

deployment-definitions リポジトリに push されてきた TypeScript コードをビルドして、k8s-manifests に対して Merge Request を作成するような Gitlab CI の定義を書いていきます。まずは全体のコードを示します。

stages:
  - build
  - push

build:
  stage: build
  except:
    - master
  image: node:alpine3.12
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - .npm/
  before_script:
    - npm ci --cache .npm --prefer-offline
  script:
    - npm run build

create-mr:
  stage: push
  only:
    - master
  image: node:alpine3.12
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - .npm/
  before_script:
    - apk add git lab
    - npm ci --cache .npm --prefer-offline
    - git config --global user.email ${GITLAB_USER_NAME}
    - git config --global user.name ${GITLAB_USER_EMAIL}
  script:
    - npm run build
    - cd dist
    - git clone https://gitlab-ci-token:${LAB_CORE_TOKEN}@gitlab.com/aruneko/k8s-manifests
    - cd k8s-manifests
    - git switch -c ${CI_COMMIT_SHORT_SHA}
    - git rm ./*.yaml
    - cp ../*.yaml .
    - git add .
    - git commit -m "${CI_COMMIT_TITLE}"
    - git push -u origin ${CI_COMMIT_SHORT_SHA}
    - unset CI_JOB_TOKEN
    - lab mr create origin master -d -s -m "${CI_COMMIT_TITLE}"

build ステージの解説

build ステージではいったん TypeScript のコードをビルドしてみて、きちんと通るかどうかをチェックしています。各作業ブランチでチェックすれば基本的に master にマージされた際に問題が起こることはないと踏んで except: [master] を仕込んでいます。

create-mr ステージの解説

master ブランチにマージされると、このステージが走ります。ここではざっくり以下のようなことをしています。

  1. git コマンドと lab コマンドの導入
  2. npm コマンドで依存パッケージをインストール
  3. Git の初期設定
  4. yaml ファイルのビルド
  5. yaml 用リポジトリのクローン
  6. Merge Request の作成

各種コマンドの導入や依存パッケージのインストールは解説不要でしょう。多少キャッシュ戦略を意識した作りにしていますが、apk や npm でサクッと入ります。ちなみに lab は大変ググりづらいですが Gitlab の CLI です。Merge Request の作成に利用します。

次に Git の初期設定です。これを忘れると commit 時に怒られます。Gitlab CI では GITLAB_USER_NAME ならびに GITLAB_USER_EMAIL という大変便利な環境変数を提供しているので、これをそのまま流用することにします。

git config --global user.email ${GITLAB_USER_NAME}
git config --global user.name ${GITLAB_USER_EMAIL}

続いては Merge Request の作成です。ここがややトリッキーです。lab コマンドを用いてそれを作成するのですが、ややはまりポイントがあります。まずクローンしているコマンドに注目してください。

git clone https://gitlab-ci-token:${LAB_CORE_TOKEN}@gitlab.com/aruneko/k8s-manifests

このコマンド中で、パスワードとして LAB_CORE_TOKEN という環境変数が用いられています。これは lab コマンドがデフォルトで読みに行く環境変数で、ここに入っているアクセストークンを使って Gitlab を操作してくれます。

この トークンは k8s-manifests リポジトリ左側メニューの「Settings」より「Access Token」から作成することができます。クローンやプッシュといったリポジトリの操作と、Merge Request 作成のための API の呼び出し両方の権限が必要ですので、api, read_repository, write_repository の3つの権限を付与してください。

できたトークンを CI 用の環境変数として該当の名称で保存しておくことも忘れずに。これでこのリポジトリに限定した操作ができるようになりますので、よりセキュアになります。

Merge Request のための差分作成には以下のようなコマンドを利用しています。リソースの削除に伴って yaml ファイルが削除されることを考慮して、いったん全ての yaml ファイルを削除するようにしています。それ以外は普通にブランチを作って add して commit して push しているだけのシンプルな動作です。

git switch -c ${CI_COMMIT_SHORT_SHA}
git rm ./*.yaml
cp ../*.yaml .
git add .
git commit -m "${CI_COMMIT_TITLE}"
git push -u origin ${CI_COMMIT_SHORT_SHA}

さて、最後に Merge Request の作成部分です。ドキュメントに明示的に書かれている箇所を発見できなかったのですが、どうやら lab コマンドは CI_JOB_TOKEN に値が入っていることを検知すると LAB_CORE_TOKEN よりも CI_JOB_TOKEN を優先的に使ってしまい、結果として権限不足で Merge Request の作成に失敗してしまいます。

そのためいったん CI_JOB_TOKENunset してあげる必要があります。こうすることで別のリポジトリに対して Merge Request を作ってあげることが可能となります。

unset CI_JOB_TOKEN
lab mr create origin master -d -s -m "${CI_COMMIT_TITLE}"

こうしてできた Merge Request を、任意のタイミングでマージしてあげることでデプロイが可能となります。

Flux CD のセットアップ

ではマージリクエストができたところで Flux CD をセットアップしていきます。たいていの環境では認証が必要な領域に置かれた Git リポジトリを読み取ることになると思いますが、そのセットアップ方法もやや面倒なポイントがあるのでそれを踏まえて解説していきます。

なおインストールには helm を利用しますので、事前にインストールしておいてください。

リポジトリの追加

Flux CD 用の helm リポジトリを追加しておきましょう。以下のコマンドをサクッと叩いてしまいます。

$ helm repo add fluxcd https://charts.fluxcd.io

秘密鍵の登録

認証が必要な領域から yaml ファイルが入ったリポジトリをクローンするために、SSH のキーペアを作成しておきます。また、flux 名前空間をクラスタ上に作成し、そこに secret として秘密鍵を登録してあげます。また、秘密鍵は手元に残しておくと厄介なので、登録が終わったら早々と消してしまうと良いでしょう。何か不都合があったら同じ手順で作り直して、上書きしてあげれば問題ありません。

$ ssh-keygen -q -N "" -f ./identity
$ kubectl create namespace flux
$ kubectl -n flux create secret generic flux-ssh --from-file=./identity
$ rm -rf ./identity

この手順で作ると identity.pub という名前で公開鍵が生成されています。これを Deploy Key として k8s-manifests 側のリポジトリに登録してあげます。やはりこうすることでキーの影響範囲をこのリポジトリに限定することができ、幾分かセキュアにすることができます。

Gitlab では左側メニューの「Settings」より「Repository」からデプロイキーを登録することができます。このとき、通常の用途において書き込み権限は不要ですので、付与しなくても問題ありません。

known_hosts の取得

次に、SSH における known_hosts を事前に生成しておく必要があります。どうやら Flux CD はこれを自動生成してくれないらしく、インストール時に環境変数として注入する必要があります。

そこで、ssh-keyscan コマンドで、該当する Gitlab サーバーの情報を調べます。一部結果を省略していますが、例えば gitlab.com の場合は以下のような結果が得られると思います。

$ ssh-keyscan gitlab.com
# gitlab.com:22 SSH-2.0-OpenSSH_7.9p1 Debian-10+deb10u2
gitlab.com ssh-ed25519 AAAA...
# gitlab.com:22 SSH-2.0-OpenSSH_7.9p1 Debian-10+deb10u2
gitlab.com ssh-rsa AAAA...
# gitlab.com:22 SSH-2.0-OpenSSH_7.9p1 Debian-10+deb10u2
gitlab.com ecdsa-sha2-nistp256 AAAA...

ここから # で始まるコメント行を除いたものをいったん環境変数に格納してください。

KNOWN_HOSTS='gitlab.com ssh-ed25519 AAAA...
gitlab.com ssh-rsa AAAA...
gitlab.com ecdsa-sha2-nistp256 AAAA...'

Flux CD のデプロイ

この情報を使って、helm で Flux CD をデプロイします。git.url の部分は k8s-manifests の Git リポジトリのアドレスで上書きすることを忘れずに。これで該当リポジトリを一定間隔ごとに監視して、デプロイを自動的に行ってくれるようになります。

$ helm upgrade -i flux fluxcd/flux \
  --set git.url=git@gitlab.com:aruneko/k8s-manifests.git \
  --set git.secretName=flux-ssh \
  --set-string ssh.known_hosts="${KNOWN_HOSTS}" \
  --namespace flux

仕上げに先ほど作っておいた Merge Request をマージしてみて、変更が反映されてくれば完成です。

おわりに

さて、だいぶ長くなってしまいましたが、Gitlab 上で ckd8s と Flux CD を使った GitOps を実装する方法について紹介してきました。仕組みの構築には多少の労力が必要ですが、一度作ってしまえば後は動き続けてくれますので、かなり楽になると思います。

今回は単に手動で yaml ファイルや Helm チャートを書くというわけではなく、cdk8s を利用してその手間を幾分か軽減しようとした結果、リポジトリの分割やマージリクエストの作成など、CI 側がやや複雑な作りになってしまいました。ただ、cdk8s による yaml ファイルの自動生成はそれを上回るメリットを提供してくれると思いますので、皆さんもぜひ使ってみてください。

また、今回は Gitlab CI を前提とした記述をしましたが、Github Actions にも応用できるやり方だと思いますので、そちらも試していると良いかもしれません。

それではまたどこかの記事で。

参考文献

脚注
  1. デフォルトでは5分間隔 ↩︎

  2. Helm Operator を使えば Helm Chart は読んでくれるようになります ↩︎

Discussion