KINTO Tech Blog
Development

GitHub Actionsだけで実現するKubernetesアプリケーションのContinuous Delivery

Cover Image for GitHub Actionsだけで実現するKubernetesアプリケーションのContinuous Delivery

GitHub Actionsだけで実現するKubernetesアプリケーションのContinuous Delivery

こんにちは。Toyota Woven City Payment Solution開発グループの楢崎と申します。

我々は、Woven by ToyotaToyota Woven Cityで利用される決済基盤アプリケーションの開発に携わっており、バックエンドからWebフロントエンド、モバイルアプリケーションまで決済に関わる機能を横断的に開発しています。

決済バックエンドはKubernetes上で動作し、いわゆるクラウドネイティブなツール群を使って開発しています。

今回はKubernetesアプリケーションを構築・安定運用していく上でキーとなる、GitOps(Gitでインフラ構成ファイルを管理し変更を適用指定する運用方法)を踏襲しつつ、CD(Continuous Delivery)プロセスに関して、一般に用いられている、いわゆる「クラウドネイティブなツール」ではなく、GitHub Actionsだけで構築することを目指します。
ここでいうCDの機能はあくまで

  • Kubernetesの構成管理ファイルの変更の適用
  • コンテナイメージのアップデート

です。他にもBlue / GreenやCanaryデプロイなど応用的なCDプロセスはありますが、「小さくスタート」することを想定しています。既にDevOpsチームが組成されていて、その恩恵にあやかれる人ではなく、最小の開発人数で、かつ新たなツールなしに普段利用しているGitHub Actionsのみを利用してKubernetes上で動作するアプリケーションを生産性高く継続的デリバリーさせたい人が対象になります。
レポジトリもアプリケーションのコードとKubernetesの構成管理ファイルを同じレポジトリで管理していることを想定しています。(権限の設定次第ではレポジトリをまたいで実行可能だとは思いますがここでは触れません)

(Gitlabを普段お使いの方であればAuto DevOpsという非常に優秀なツールがあるので、決してGitHub及びGitHub Actions最高!というつもりはありませんので悪しからず)

クラウドネイティブなKubernetes向けCI/CDツール

Kubernetes向けアプリケーションのCI/CDと聞いて読者の皆さんはどのようなツールを思いつきますか?

などが挙げられます。
いずれのツールも非常に高機能で、Kubernetesの機能をフルに活かすために非常に有用です。
またGitOpsを実践する上で、Kubernetesの構成ファイルやアプリケーションイメージを柔軟に安全に更新できます。

一方でツール特有の知識や運用も必要で、DevOpsに専門の人員がいるような大きな組織ではないと継続的に運用するのは難しいのではないでしょうか?
CDツールの運用そのものにKubernetesが必要で、Kubernetesの構成ファイルを管理するツールのためにKubernetesの構成ファイルが必要ということで、少人数の組織では導入や運用の敷居も非常に高いと思っています。

この記事では以下の図のようなパイプラインを、GitHub Actionsだけで構成することを考えます。
Kubernetesは特定のクラウドではなく汎用的なクラスタを想定しています。何かしらのコンテナレジストリがあることを想定しています。構成管理ファイルはKustomizeを例にしますが、Helm, Terraformなど何でも応用可能だと思います。

デモ

Kubernetesの構成ファイルを管理するフォルダとアプリケーションのフォルダが入っているレポジトリを考えます。
フォルダ構成は以下になります。ここでは具体的なコードやDockerfileの中身、各アプリケーションのソース等は省略します。

├── .github
│   ├── actions
│   │   └── image-tag-update
│   │       └── action.yaml
│   └── workflows
│       ├── build-go.yaml
│       ├── build-java.yaml
│       ├── build-node.yaml
│       └── kubectl.yaml
├── go-app
│   ├── src/
│   └── Dockerfile
├── java-app
│   ├── src/
│   └── Dockerfile
├── k8s
│   ├── service-go.yaml
│   ├── service-java.yaml
│   ├── service-node.yaml
│   └── kustomization.yaml
└── node-app
    ├── src/
    └── Dockerfile

それぞれのアプリケーションは以下のような形でplaceholderとし、

k8s/service-*.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
...
  template:
...
    spec:
      containers:
      - name: some-server
        image: go-placeholder # placeholderとしてkustomizationと同じ文字列を入れておく

その複数のplaceholderをkustomization.yaml上で一元管理します。

ks8/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: techblog
resources:
  - service-go.yaml
  - service-java.yaml
  - service-node.yaml
images:
  - name: go-placeholder
    newName: go-app
    newTag: v1.1.1
  - name: java-placeholder
    newName: java-app
    newTag: v2.7.9alpha
  - name: node-placeholder
    newName: node-app
    newTag: latest

まずKubernetesの構成ファイルを適用するために、以下のようなyamlのGitHub Actionsを構成します。

.github/workflows/kubectl.yaml
name: kubectl

on:
  pull_request:
    branches:
    - "**"
    paths:
    - "k8s/**" #Kubernetesのmanifest fileが入っている場所
  push:
    branches:
    - main
    paths:
    - "k8s/**"

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: azure/setup-kubectl@v4
    - env:
        KUBECONFIG_CONTENTS: ${{ secrets.KUBECONFIG_CONTENTS }} #事前にkubeconfigをGitHubのシークレットに格納しておく
      run: |
        echo "${KUBECONFIG_CONTENTS}" > $HOME/.kube/config
        chmod 600 $HOME/.kube/config
    - run: kubectl apply --dry-run=server -k ./k8s  >> $GITHUB_STEP_SUMMARY

    - if: github.ref == 'refs/heads/main # mainブランチだったら実際に適用
      run: kubectl apply -k k8s/

これは管理者権限を持ったkubeconfigが手元にある場合の一般的なKubernetesの構成ファイルを適用するパイプラインです。
各クラウドなどのクラスタの設定方法に応じてconfigの取得方法は変更してください。
次に各アプリケーションをpushする際に自動でプルリクエストを作るか、コンテナのイメージタグを書き換えるcompositeを作成します。

.github/actions/image-tag-update/action.yaml
name: image-tag-update
description: 'コンテナイメージの更新時にkustomizationのイメージタグを書き換えるタスク'
inputs:
  target_app:
    description: '対象のアプリケーション'
    required: true
  tag_value:
    description: '新しいコンテナイメージタグ'
    required: true
  token:
    description: 'PRや内容の更新の権限を持ったトークン'
    required: true
runs:
  using: 'composite'
  steps:
    - uses: actions/checkout@v4
      id: check-branch-exists
      continue-on-error: true
      with:
        ref: "image-tag-update" # タグ更新用のデフォルトブランチ名
    - uses: actions/checkout@v4 # checkoutはブランチが存在しないとデフォルトブランチにフォールバック、みたいなことはできないのでこういう書き方
      if: steps.check-branch-exists.outcome == 'failure'
      with:
        ref: main
    - uses: mikefarah/yq@master # yqで対象のplaceholderのタグの値を置換
      with:
      cmd: yq eval '(.images[] | select(.name == "'"${{ inputs.target_app }}-placeholder"'")).newTag = "'"${{ inputs.tag_value }}"'"' -i k8s/kustomization.yaml
    - uses: peter-evans/create-pull-request@v6
      if: steps.check-branch-exists.outcome == 'failure' # プルリクエストがないと新しいプルリクエストを作成
      with:
        title: 'コンテナイメージの更新'
        body: |
           `${{ inputs.target_app }}` を更新します
        branch: "image-tag-update"
    - uses: stefanzweifel/git-auto-commit-action@v5
      if: steps.check-branch-exists.outcome == 'success' # チェックアウトが成功したら、既存のブランチにコミットを追加
      with:
        commit_message: "Image update for ${{ inputs.target_app }}"

各アプリケーションのイメージを作成する過程で、上記のcompositeを呼びます。複数のアプリケーションを管理している場合は、それぞれのアプリケーションの後に付け加えるとよいでしょう。

.github/workflows/build-go.yaml
...
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          file: ./Dockerfile
          push: true
          tags: ${{ env.tag }} # なにかしらのタグ
      - uses: ./.github/actions/image_update
        if: github.ref == 'refs/heads/main'
        with:
          target_app: go
          tag_value: ${{ env.tag }}
          token: ${{ secrets.GITHUB_TOKEN }} # コンテンツ、プルリクエスト編集権限を持ったgithub token

これで、アプリケーションを実行するタイミングでコンテナイメージが自動でアップデートされ、プルリクエストベースで新しいコンテナイメージがデプロイできるようになります!
(タグの導出方法は各自のワークフローにおまかせします。下記の例はマイナーバージョンをインクリメントした例です)

  - name: go-placeholder
    newName: go-app
-    newTag: v1.1.1
+    newTag: v1.1.2 

運用上の注意点

デプロイのタイミング

イメージ更新用のプルリクエストが、マージされた瞬間にデプロイされます。インフラの構成ファイルの修正と合わせてリリースしたい場合は、このブランチに修正を追加するか、マージするタイミングを合わせて適用するといいでしょう。

新規コンテナアプリケーションの追加

例えば上記の例でPythonのアプリケーションを足したいという時に、イメージ更新用のプルリクエストがそのまま残っていると、Pythonのイメージタグをどれだけ更新してもプルリクエスト自体に最新版の変更が反映されてない限り、空振りし続けるので注意が必要です。

切り戻し

Commitをrevertすれば戻せるので非常にシンプルです。

Reconcileのタイミング

GitOps Toolの多くがドリフトを抑制するためのリコンサイルをほぼリアルタイムで実施できるのに対して、このやり方だとCDパイプラインが動作したタイミングでないと実施できません。
Kubernetesのクラスタに更新権限をどれくらいのチームメイトが保有して権限を行使しているかにも応じてツールの使い分けは大事だと思います。

Container Registryを直接見ているわけではない

コンテナレジストリから直接コンテナイメージの最新版を取得するものもありますが、この方法では実際に見ているわけではありません。確実にコンテナが存在するか、確認するステップをコンテナレジストリごとに実装したほうが良さそうです。

GitHub Actionsの権限設定に関して

contentspull-requests の更新権限が必要になってきます。Actionsのパーミッション、GitHub Appなどに権限をアサインして使ってください。詳しくはこちら

後に実行されたコンテナイメージで上書きされる

CDツールには、コンテナイメージのタグの値をみて、Semantic Versioningなどの規則に従ってどちらが新しいバージョンか判別する仕組みがあります。
上記で示したworkflowはタグの値に関係なく、後に実行されたパイプラインでイメージタグを上書きします。
この挙動が問題であれば値を検証して、上書きすべきか判定する必要があります。

まとめ

このやり方を用いることで、GitOpsがGitHub上で完結して、非常にシンプルにKubernetesアプリケーションの継続的デリバリーが実践できるのではないのかなと思います。
CDツールのエラーもGitHub Actions上に集約できるので、普段のCIプロセスと同じ方法で実行結果やエラーの内容が確認できるのは非常に嬉しいですね。
色々なツールが多く存在し、目移りすることも多いKubernetesのツール選定ですが、身の丈にあったツールを利用してKubernetesアプリケーション開発の生産性を高めていきたいですね。

Facebook

関連記事 | Related Posts

We are hiring!

【Woven City決済プラットフォーム構築 PoC担当バックエンドエンジニア(シニアクラス)】/Toyota Woven City Payment Solution開発G/東京

Toyota Woven City Payment Solution開発グループについて私たちのグループはトヨタグループが取り組むWoven Cityプロジェクトの一部として、街の中で利用される決済システムの構築を行います。Woven Cityは未来の生活を実験するためのテストコースとしての街です。

【Toyota Woven City決済プラットフォームフロントエンドエンジニア(Web/Mobile)】/Toyota Woven City Payment Solution開発G/東京

Toyota Woven City Payment Solution開発グループについて我々のグループはトヨタグループが取り組むToyota Woven Cityプロジェクトの一部として、街の中で利用される決済システムの構築を行います。Toyota Woven Cityは未来の生活を実験するためのテストコースとしての街です。