こんにちは、テクマトリックスの酒井です。

新年度が始まって1ヶ月半が経ったところですが、この短い間に継続的インテグレーション (以下、CI) に Kubernetes を活用したいとのお問い合わせをいくつかいただきました。そこで今回は検証用としてローカル PC に Kubernetes クラスターを作成し、この上に CI 環境を構築する方法をご紹介します。

環境の概要

Git サーバーのリポジトリにソースコードをプッシュすると、パイプラインで指定したコンテナが Kubernetes 上で実行されてビルドが行われる、という CI 環境を作ります。環境の構成としては、 Windows 10 Pro のローカル PC 上で Hyper-V で仮想マシン (VM) を作成し、この VM で Kubernetes を実行します。そしてこの Kubernetes クラスター上に、 Git サーバーとして Gitea と、 CI サーバーとして Jenkins をインストールします。 VM は1台だけですが Git サーバーと CI サーバーに異なるドメインでアクセスするために nip.io を利用します。 CI 環境の構成図は次のようになります。

今回構築する CI 環境

Windows 以外のソフトウェアは無料で利用できます。またできる限りサクッと作るため、ほとんどの構築作業を Windows Powershell で行います。

準備

最初に、ローカル PC で Hyper-V を有効化することと、環境構築に利用するツールをインストールします。

Hyper-V の有効化

Windows Powershell を管理者として実行し、以下のコマンドを実行します。

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All

コマンドの出力に RestartNeeded : True が含まれていた場合、マシンを再起動する必要があります。

ツールのインストール

Kubernetes コマンドラインツールkubectl コマンド), Helm, Minikube, Git for Windows をインストールします。これらのインストールを簡単にするために Chocolatey Package Manager (以下、 Chocolatey )を利用します。

Chocolatey のインストール手順に従って Chocolatey をインストールします。 Windows Powershell を管理者として実行し、以下のコマンドを実行します。

Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

念のため Chocolatey がインストールされたことを確認します。

> choco --version
1.4.0

続いて kubectl, Helm, Minikube, Git for Windows をインストールします。

choco install -y kubernetes-cli kubernetes-helm Minikube git

ツールのインストール後、それぞれのツールがインストールされたことを確認します。

> kubectl version --client --short
Flag --short has been deprecated, and will be removed in the future. The --short output will become the default.
Client Version: v1.25.3
Kustomize Version: v4.5.7

> helm version --short
v3.11.3+g3232493

> minikube version
commit: 08896fd1dc362c097c925146c4a0d0dac715ace0

> git version
git version 2.40.1.windows.1

VM と Kubernetes クラスターの作成

Minikube による作成

Minikube を利用して Hyper-V の VM と Kubernetes クラスターを作成します。 Windows Powershell にて次のコマンドを実行します。

minikube start --driver=hyperv --cpus=3 --memory=4g

コマンドの完了まで少し時間がかかりますが、 Hyper-V VM の作成、 Kubernetes クラスターのセットアップ、さらに kubectl と Helm を利用するための設定が行われます。お手軽です。

内部ネットワーク環境向けの追加のオプション

企業内のネットワークのような内部ネットワーク環境で作業する場合、追加のオプションを指定する必要がある可能性があります。

内部ネットワークが 172.17.xxx.xxx の IP アドレスを利用している場合、 Docker が同じアドレス範囲を含むネットワークを作成することを避けるために、 minikube start コマンドに次のようなオプションを追加します。

--docker-opt bip=172.80.0.1/16 --docker-opt default-address-pool=base=172.90.0.0/16,size=24

コンテナからサーバー名を指定してネットワーク内のサーバー(ソースコードリポジトリなど)にアクセスする場合、 minikube start コマンドに次のようなオプションを追加して DNS サーバーを指定します。

--docker-opt dns="内部 DNS サーバーの IP アドレス"

イングレスコントローラーのインストール

ブラウザなどのクライアントから CI 環境へのアクセスを Gitea と Jenkins に振り分けるために、 Kubernetes クラスターにイングレスコントローラーをインストールします。イングレスコントローラーは Minikube のアドオンを有効にするだけで簡単にインストールできます。

minikube addons enable ingress

これで Kubernetes クラスターの準備ができました!

アプリケーションのインストール・設定

続いて Kubernetes クラスターにアプリケーションをインストールしていきます。

Gitea のインストール・設定

まず Gitea のドメイン名を決定します。次のコマンドを実行し、出力された Gitea のドメイン名をコピーします。ドメイン名には Hyper-V VM の IP アドレスが含まれます。ドメイン名の先頭の git は変更できます。

$GITEA_DOMAIN = "git.$(minikube ip).nip.io"
echo ${GITEA_DOMAIN}

次に、 Helm を利用して Gitea のインストールを設定するための gitea.yaml ファイルを作成します。ファイルの内容は以下の YAML のとおりです。 ${GITEA_DOMAIN} は先程コピーしたドメイン名に置き換えます。また必要であればパスワードも変更します。

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: "${GITEA_DOMAIN}"
      paths:
        - path: /
          pathType: Prefix
gitea:
  admin:
    username: gitea_admin
    password: "r8sA8CPHD9!bt6d"
  config:
    server:
      PROTOCOL: http
      DOMAIN: "${GITEA_DOMAIN}"
    webhook:
      ALLOWED_HOST_LIST: "*"
global:
  postgresql:
    auth:
      postgresPassword: "fvN1jUXIz2"

そして Helm を利用して、 Kubernetes クラスターに Gitea をインストールします。

helm repo add gitea-charts https://dl.gitea.io/charts/
helm upgrade gitea gitea-charts/gitea --install --namespace gitea --create-namespace --values gitea.yaml --wait

コマンドが完了すると、 http://${GITEA_DOMAIN}/ で Gitea の Web UI にアクセスできます。画面右上のサインインから、 gitea.yaml の gitea.admin.username と gitea.admin.password の値を使ってサインインできます。

なお、もし gitea.yaml に間違いがあって変更した場合、再度上記の helm upgrade コマンドを実行することで新しい設定が反映されます。

続いて Jenkins 用のユーザーとアクセストークンを作成します。 Gitea の Web UI を操作する代わりに gitea コマンドを利用します。

# jenkins ユーザーを作成
kubectl exec -it -n gitea gitea-0 -c gitea -- `
 su git -c 'gitea admin user create --username jenkins --random-password --must-change-password=false --email jenkins@ci.local'

# アクセストークンを生成
kubectl exec -it -n gitea gitea-0 -c gitea -- `
 su git -c 'gitea admin user generate-access-token --username jenkins --token-name jenkins --scopes repo'

アクセストークンを生成するコマンドを実行すると、コンソールに次のように出力されますので、出力されたアクセストークンをコピーします。

Access token was successfully created: 47b6a703c5c41c0761140fe2bfec69d1176d8cfb

これで Gitea のインストールと初期設定は完了です。

Jenkins のインストール・設定

続いて Jenkins のインストールに移ります。 Gitea と同様に、まず Jenkins のドメイン名を決定します。次のコマンドを実行し、出力された Jenkins のドメイン名をコピーします。ドメイン名の先頭の ci は変更できます。

$JENKINS_DOMAIN = "ci.$(minikube ip).nip.io"
echo ${JENKINS_DOMAIN}

次に、 Helm を利用して Jenkins のインストールを設定するための jenkins.yaml ファイルを作成します。ファイルの内容は次の YAML のとおりです。 ${JENKINS_DOMAIN}${GITEA_DOMAIN}${GITEA_ACCESS_TOKEN} は、それぞれ Jenkins のドメイン名、 Gitea のドメイン名、 Gitea の アクセストークンに置き換えます。

controller:
  ingress:
    enabled: true
    hostName: "${JENKINS_DOMAIN}"
  jenkinsUrl: http://${JENKINS_DOMAIN}/
  additionalPlugins:
    - gitea
    - job-dsl
    - pipeline-graph-view
    - pipeline-stage-view
    - timestamper
  JCasC:
    configScripts:
      gitea: |
        credentials:
          system:
            domainCredentials:
            - credentials:
              - giteaAccessToken:
                  scope: GLOBAL
                  token: "${GITEA_ACCESS_TOKEN}"
                  id: "gitea-access-token"
        unclassified:
          giteaServers:
            servers:
            - displayName: "http://${GITEA_DOMAIN}"
              serverUrl: "http://${GITEA_DOMAIN}"
              manageHooks: true
              credentialsId: "gitea-access-token"
      miscellaneous: |
        unclassified:
          globalDefaultFlowDurabilityLevel:
            durabilityHint: PERFORMANCE_OPTIMIZED
          timestamper:
            allPipelines: true

そして Helm を利用して、 Kubernetes クラスターに Jenkins をインストールします。

helm repo add jenkinsci https://charts.jenkins.io
helm upgrade jenkins jenkinsci/jenkins --install --namespace jenkins --create-namespace --values jenkins.yaml --wait

コマンドが完了すると、 http://${JENKINS_DOMAIN}/ で Jenkins の Web UI にアクセスできます。 admin ユーザーのパスワードは次のコマンドで取得できます。

kubectl exec --namespace jenkins -it svc/jenkins -c jenkins -- /bin/cat /run/secrets/additional/chart-admin-password

Jenkins の初期設定は Helm チャートと jenkins.yaml に記載されているため、 Web UI などで追加の初期設定を行う必要はありません。これで CI 環境のアプリケーションはすべてインストールされました。

リポジトリの作成とパイプラインの実行

ここまでは CI 環境の管理者としての作業でしたが、ここからは主に開発者としての作業になります。 Gitea に組織とリポジトリを作成し、 Jenkins パイプラインをリポジトリに追加します。最後に Jenkins でジョブを作成します。

Gitea の組織とリポジトリの作成

Gitea の Web UI の代わりに以下のコマンドを実行して Gitea に組織とリポジトリを作成します。組織の作成後、組織のチームに jenkins ユーザーを追加することにより、パイプラインの実行ステータスが Gitea のリポジトリに通知されるようになります。

$orgName       = 'exampleorg'       # 組織名
$repoName      = 'example-pipeline' # リポジトリ名
$giteaUsername = 'gitea_admin'      # Gitea の初期ユーザー名
$giteaPassword = 'r8sA8CPHD9!bt6d'  # Gitea の初期ユーザーのパスワード

# Gitea API にアクセスするための認証情報
$headers = @{ Authorization = "Basic "+ [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${giteaUsername}:${giteaPassword}")) }

# 組織を作成
$org = Invoke-RestMethod -Method 'POST' `
  -Uri "http://${GITEA_DOMAIN}/api/v1/orgs" `
  -Headers ${headers} `
  -ContentType 'application/json' `
  -Body ('{"username": "' + ${orgName} + '"}')

# 組織の Owners チームに jenkins を追加
$teams = Invoke-RestMethod -Method 'GET' `
  -Uri "http://${GITEA_DOMAIN}/api/v1/orgs/${orgName}/teams" `
  -Headers ${headers}
Invoke-RestMethod -Method 'PUT' `
  -Uri "http://${GITEA_DOMAIN}/api/v1/teams/$($teams[0].id)/members/jenkins" `
  -Headers ${headers}

# リポジトリ example-pipeline を作成。 README を自動生成
$repo = Invoke-RestMethod -Method 'POST' `
  -Uri "http://${GITEA_DOMAIN}/api/v1/orgs/${orgName}/repos" `
  -Headers ${headers} `
  -ContentType 'application/json' `
  -Body ('{"name":"' + ${repoName} + '","auto_init":true}')

echo ('クローン URL: ' + $repo.clone_url)

リポジトリにパイプラインコードを追加

作成したリポジトリに Jenkins パイプラインのコードを追加します。まず Gitea からリポジトリをクローンします。

git clone $repo.clone_url
cd example-pipeline

Git の作業ディレクトリに Jenkinsfile ファイルを作成します。ファイルの内容は Kubernetes プラグインのドキュメントに記載のとおりとします。これは Maven のバージョンと Busybox の使い方を出力します。

pipeline {
  agent {
    kubernetes {
      yaml '''
        apiVersion: v1
        kind: Pod
        metadata:
          labels:
            some-label: some-label-value
        spec:
          containers:
          - name: maven
            image: maven:alpine
            command:
            - cat
            tty: true
          - name: busybox
            image: busybox
            command:
            - cat
            tty: true
        '''
      retries 2
    }
  }
  stages {
    stage('Run maven') {
      steps {
        container('maven') {
          sh 'mvn -version'
        }
        container('busybox') {
          sh '/bin/busybox'
        }
      }
    }
  }
}

作成した Jenkinsfile ファイルをコミットおよびリポジトリにプッシュします。

git add Jenkinsfile
git commit -m 'Jenkinsfile を追加'
git push
# プッシュ時に Gitea のユーザー名とパスワードを入力します

Jenkins ジョブの作成

最後に、Jenkins に Gitea の組織に対応するフォルダを作成します。まず jenkins.yaml の controller.JCasC.configScripts にフォルダの設定を追加します。 ${GITEA_DOMAIN} は Gitea のドメイン名に置き換えます。

controller:
  # ...
  JCasC:
    configScripts:
      gitea: |
        ...
      miscellaneous: |
        ...
      # 以降を追加
      jobs: |
        jobs:
          - script: >
              organizationFolder('exampleorg') { // Gitea 組織名
                organizations {
                  gitea {
                    serverUrl("http://${GITEA_DOMAIN}")
                    credentialsId('gitea-access-token') 
                    repoOwner('exampleorg')      // Gitea 組織名
                    traits {
                      giteaBranchDiscovery {
                        strategyId(1)
                      }
                      giteaPullRequestDiscovery {
                        strategyId(1)
                      }
                      giteaForkDiscovery {
                        strategyId(1)
                        trust {
                          giteaTrustContributors() 
                        }
                      }
                    }
                  }
                }
              }

そして変更した設定を Jenkins に反映します。

helm upgrade jenkins jenkinsci/jenkins --install --namespace jenkins --create-namespace --values jenkins.yaml --wait

Jenkins の Web UI のページを開く(すでに開いている場合は再読み込みする)と、 exampleorg フォルダが追加されていることが確認できます。このフォルダに移動するとフォルダの中身はまだ空です。ページ左側の Scan Gitea Organization Now をクリックすると、 Jenkins は Gitea 組織をスキャンし、 example-pipeline リポジトリに対応するフォルダと、その中に main ブランチに対応するパイプラインジョブが作成されます。ページを再読み込みすると example-pipeline フォルダが表示されます。

exampleorg フォルダ、 main ジョブの順に選択して移動すると、パイプラインが実行されたことが確認できます。

パイプラインジョブ main の画面。パイプラインは1回実行された

Stage View の #1 の行の Run maven 列の薄緑色のセルにマウスカーソルを合わせ、ボップアップの Logs ボタンをクリックすると、 Maven や Busybox の実行ログを確認できます。

実行 #1 の Run maven ステージの実行ログの一部。 mvn -version コマンドが実行されている

なお、 Jenkins は Gitea 組織のスキャンと同時に、 Jenkinsfile が存在するリポジトリに Jenkins へのウェブフックを自動的に設定します。そのため、 example-pipeline リポジトリに変更がプッシュされるとパイプラインが自動的に実行されるようになっています。

これで検証用の CI 環境が完成です。お疲れ様でした。さまざまなコンテナイメージを利用してパイプラインを実行してみてください!

環境の停止・開始・削除

Minikube を利用して VM ごと停止・開始・削除できます。 VM を一度停止すると起動時に IP アドレスが変更されるため、 gitea.yaml ファイルと jenkins.yaml ファイルの変更と Helm リリースの更新、および、 Gitea のウェブフック URL の修正が必要です。もしかしたら環境を作り直す方が早いかもしれません。 VM の停止・開始・削除のコマンドは次のとおりです。

# VM の停止
minikube stop

# 停止中の VM の起動
minikube start

# gitea.yaml および jenkins.yaml の変更後に以下を実行
helm upgrade gitea gitea-charts/gitea --namespace gitea --values gitea.yaml --wait
helm upgrade jenkins jenkinsci/jenkins --namespace jenkins --values jenkins.yaml --wait

# VM の削除
minikube delete

まとめ

ローカル PC に Kubernetes クラスターと CI 環境を構築する方法をご紹介しました。この方法によって、 Kubernetes と CI 環境を構築し、 Kubernetes を活用する CI/CD パイプラインを気軽に試すことが可能になるかと思います。

今回構築した CI 環境はあくまで検証用であり、拡張性・可用性・保守性などの観点から本番運用には適しません。本番の CI 環境を構築したい方、また本番でなくとも CI 環境について相談したいことがございましたらぜひテクマトリックスにご相談いただければと思います。

参考

By tsakai

Jenkins関連のサービスやCloudBees製品を主に担当しています。 Certified CloudBees Jenkins Engineer (CCJE) および CloudBees CI DevOps Associate です。