taxin's notes

読書、勉強メモ etc.

CircleCI + KustomizeでKubernetes環境のCI/CD Pipelineを作る

はじめに

@taxin_ttと申します。

CI/CD Pipelineの実装+GCP / CircleCIの勉強も兼ねて、GKEのKubernetesクラスター上にサンプルアプリをDeployするCI/CD Pipelineを実装してみました。

構成

今回は、Circle CI + KustomizeでCI/CD Pipelineを実装しました。
(Deploy先のKubernetesクラスターもTerraformで作成できるようにしています。)

f:id:taxintt:20200617080141p:plain

大まかな処理の流れは下記の通りです。

  1. コードの変更をApp repo(Stgブランチ)にPush
  2. MasterブランチへのMergeをトリガーとしてContainer ImageをBuild
  3. BuildしたContainer ImageをContainer RegistryにPush
  4. Manifest repo内のImage tagの書き換え
  5. 修正したManifest ファイルをKubernetes Clusterに反映

このフローはWeaveworksが出しているGitOps PipelineのExampleとほぼ同じような構成にしています。
(Circle CIがクラスタにManifest repoの変更を反映する(Push)の方式をとっているので、実際にはGitOpsではなくCIOpsの構成になっています。) www.weave.works

レポジトリの構成

レポジトリはアプリ用のrepoとマニフェスト用のrepoに分けています。
動作確認自体はできていますが、未完成の部分も一部あります。

github.com

github.com

ブランチの構成

上記の2つのレポジトリでは、Prodction環境用のmasterブランチとStaging環境用のstgブランチの2つを用意しています。

最初に、 DeveloperがコードをstgブランチにPushします。
(実際には、さらに別のブランチを切ってstgブランチに対するPRをMergeするなどして、Staging環境用のブランチに変更を反映することになると思います。)

f:id:taxintt:20200617095603p:plain

App Repo (stgブランチ) へのPushをトリガーとして、Circle CIのJobが起動してManifest Repo (stgブランチ) のImage tagを書き換えます。

f:id:taxintt:20200617095824p:plain

App Repo (stgブランチ) の変更内容をmasterブランチにMergeします。
上記のMasterブランチへのMergeをトリガーとして、Manifest RepoのPR (stgブランチ → masterブランチ) を作成します。

f:id:taxintt:20200617101211p:plain

App repoのCircle CIのJob定義

それぞれのJobごとに分割して説明します。

Container ImageのBuildとRegistryへのPush

build_and_push_image では、Container ImageのBuildとRegistryへのPushを行います。
また、OrbsとしてSlackとの連携機能を利用しているので、Jobが完了した後に成功/失敗の通知をSlackで行います。

Container ImageをGCR(Google Container Registry)にPushするためには、Service Account(SA)を利用するため事前にGCPのコンソールでService Accountを作成しておきます。

circleci.com

Service AccountにはRegistryへのPush権限を付与する必要があるため、「ストレージ管理者」の権限を付与しておきます。
最後に、Service Accountの作成時にキーファイルをダウンロードして、その中身をbase64エンコードした上でCircle CIの環境変数(${ACCT_AUTH})として登録しておきます。

cloud.google.com

ImageのTagに関しては、直近のコミットハッシュをCircle CIの環境変数 ($CIRCLE_SHA1) から取得してTagとして利用します。

circleci.com

version: 2.1
orbs:
  slack: circleci/slack@3.4.2
jobs:
  build_and_push_image:
    docker:
      - image: google/cloud-sdk
    working_directory: /go/src/github.com/TaxiN/sample-app-ci
    steps:
      - setup_remote_docker:
          version: 18.06.0-ce
      - checkout
      - run:
          name: Authenticate for pushing image to GCR
          command: |
            echo ${ACCT_AUTH} | base64 -d > ${HOME}/account-auth.json
            gcloud auth activate-service-account --key-file=${HOME}/account-auth.json
            gcloud --quiet config set project ${GCP_PROJECT}
            gcloud --quiet config set compute/zone ${CLOUDSDK_COMPUTE_ZONE}
            gcloud --quiet auth configure-docker         
      - run:
          name: Build docker image and set image tag
          command: docker build -t ${GCR_REPO}/${IMAGE_NAME}:$CIRCLE_SHA1 .
      - run:
          name: Push image to repository
          command: docker push ${GCR_REPO}/${IMAGE_NAME}:$CIRCLE_SHA1
      - slack/status:
          include_project_field: true
          success_message: ':circleci-pass: Branch: $CIRCLE_BRANCH\nUser:$CIRCLE_USERNAME'
          failure_message: ':circleci-fail: Branch: $CIRCLE_BRANCH\nUser:$CIRCLE_USERNAME'
          webhook: ${SLACK_WEBHOOK}


マニフェストファイルのImage tagの書き換え

push_changes_to_manifest_repo のJobでは、Manifest repoにあるマニフェストファイルのImage tagの書き換えを行います。

マニフェストファイルはKustomizeを用いて管理しています。
Kustomizeではkustomize editコマンドでImage tagの書き換えを容易に行うことができます。

github.com

Image tagの書き換えを行った後に、変更内容をcommit + pushしています。
App repoではなく、cloneしてきたManifest repoに対して変更をpushするのでremoteの設定を変更しておきます。

また、Manifest repoへのpushができるようにManifestのGitHub repositoryでDeploy keyを作成して、Circle CI側に登録しておきます。
登録したDeploy keyはadd_ssh_keysのstepを実行することで、Job用のコンテナに対して鍵を登録します。

qiita.com

circleci.com

  push_changes_to_manifest_repo:
    docker:
      - image: google/cloud-sdk
    steps:
      - checkout
      - run:
          name: apt-get update for wget
          command: |
            apt-get update
            apt-get install -y wget
      - run:
          name: install kubectl
          command: |
            curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
            mv kubectl /usr/local/bin
            chmod +x /usr/local/bin/kubectl
      - run:
          name: install kustomize
          command: |
            wget https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv3.5.4/kustomize_v3.5.4_linux_amd64.tar.gz
            tar xvzf kustomize_v3.5.4_linux_amd64.tar.gz
            mv kustomize /usr/local/bin
            chmod +x /usr/local/bin/kustomize
      - run:
          name: set config of github repo
          command: |
            git config --global user.email "circleci-user@noreply.github.com"
            git config --global user.name "circleci-user"
      - run:
          name: clone kubernetes manifests repo
          command: |
            git clone --depth 1 -b stg git@github.com:TaxiN/sample-manifest-repo.git
            cd ./sample-manifest-repo
            git remote set-url origin git@github.com:TaxiN/sample-manifest-repo.git
      - run:
          name: edit kubernetes manifests
          command: |
            cd ./sample-manifest-repo/overlays/staging
            kustomize edit set image ${GCR_REPO}/${IMAGE_NAME}:$CIRCLE_SHA1
      - add_ssh_keys:
          fingerprints:
            - "<fingerprints>"
      - run:
          name: commit and push the changes
          command: |
            cd ./sample-manifest-repo
            git add .
            git commit -m "Updating image tag ${GCR_REPO}/${IMAGE_NAME}:$CIRCLE_SHA1"
            git branch --set-upstream-to=origin/stg stg
            git push origin stg


Manifest repoでのPull Request (stgmaster)の作成

create_pr_in_manifest_repo のJobでは、Manifest repoに対してPull Requestを作成します。
このJobはApp repoでのMasterブランチへのMergeをトリガーとして実行されます。

Pull Requestの作成には、hubを利用します。
--base--headでブランチを指定することでPRの設定を行います。

hub.github.com

create_pr_in_manifest_repo:
    docker:
      - image: google/cloud-sdk
    steps:
      - checkout
      - run:
          name: install hub
          command: |
            curl -sSLf https://github.com/github/hub/releases/download/v2.8.3/hub-linux-amd64-2.8.3.tgz | \
            tar zxf - --strip-components=1 -C /tmp/ && \
            mv /tmp/bin/hub /usr/local/bin/hub
      - run:
          name: set config of github repo
          command: |
            git config --global user.email "circleci-user@noreply.github.com"
            git config --global user.name "circleci-user"
      - add_ssh_keys:
          fingerprints:
            - "<fingerprints>"
      - run:
          name: create pull request
          command: |
            git clone --depth 1 -b stg git@github.com:TaxiN/sample-manifest-repo.git
            cd ./sample-manifest-repo
            hub pull-request \
              --message="Deploying image ${GCR_REPO}/${IMAGE_NAME}:$CIRCLE_SHA1 \
              Built from commit ${COMMIT_SHA} of repository sample-app-ci \
              Author: $(git log --format='%an <%ae>' -n 1 HEAD)" \
              --base=${CIRCLE_PROJECT_USERNAME}:master \
              --head=${CIRCLE_PROJECT_USERNAME}:stg


Manifest repoのCircle CIのJob定義

Manifest repoのJobでは、Master, stgブランチでの変更をトリガーとしてKubernetesクラスターに変更を反映します。
今回は一つのクラスタ内でprd, stgとそれぞれの環境用のNamespaceを作成して、そこにDeployします。

また、マニフェストファイルのディレクトリ構成として、overlays以下をstagingproductionディレクトリに分けて環境差分を切り出しています。
(公式が提供しているExampleのディレクトリ構成を参考に作成しました。)

github.com

GKE(Google Kubernetes Engine)へのDeployに必要な権限については、GCRへのImage pushと同様にService Accountを利用して権限を与えます。
GKEへのDeployの権限を付与する必要があるため、Service AccountにはKubernetes Developer」の権限を付与しておきます。
(権限周りも含めて、下記の記事は参考になりました。)

medium.com

version: 2.1
orbs:
  slack: circleci/slack@3.4.2
jobs:
  deploy_to_staging_cluster:
    docker:
      - image: google/cloud-sdk
    steps:
      - checkout
      - run:
          name: apt-get update for wget
          command: |
            apt-get update
            apt-get install -y wget
      - run:
          name: install kubectl
          command: |
            curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
            mv kubectl /usr/local/bin
            chmod +x /usr/local/bin/kubectl
      - run:
          name: install kustomize
          command: |
            wget https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv3.5.4/kustomize_v3.5.4_linux_amd64.tar.gz
            tar xvzf kustomize_v3.5.4_linux_amd64.tar.gz
            mv kustomize /usr/local/bin
            chmod +x /usr/local/bin/kustomize
      - run:
          name: set up google cloud sdk
          command: |
            if [ "${CIRCLE_BRANCH}" == "stg" ]; then
              apt-get install -qq -y gettext
              echo ${ACCT_AUTH} | base64 -d > ${HOME}/account-auth.json
              gcloud auth activate-service-account --key-file=${HOME}/account-auth.json
              gcloud --quiet config set project ${GCP_PROJECT}
              gcloud --quiet config set compute/zone ${CLOUDSDK_COMPUTE_ZONE}
              gcloud --quiet container clusters get-credentials ${GOOGLE_CLUSTER_NAME}    
            fi
      - run:
          name: deploy manifests to gke cluster
          command: |
            kustomize build ./overlays/staging | kubectl apply -n stg -f -
      - slack/status:
          include_project_field: true
          success_message: 'Branch: $CIRCLE_BRANCH\nUser:$CIRCLE_USERNAME'
          failure_message: 'Branch: $CIRCLE_BRANCH\nUser:$CIRCLE_USERNAME'
          webhook: ${SLACK_WEBHOOK}
  deploy_to_prodction_cluster:
    docker:
      - image: google/cloud-sdk
    steps:
      - checkout
      - run:
          name: apt-get update for wget
          command: |
            apt-get update
            apt-get install -y wget
      - run:
          name: install kubectl
          command: |
            curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
            mv kubectl /usr/local/bin
            chmod +x /usr/local/bin/kubectl
      - run:
          name: install kustomize
          command: |
            wget https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv3.5.4/kustomize_v3.5.4_linux_amd64.tar.gz
            tar xvzf kustomize_v3.5.4_linux_amd64.tar.gz
            mv kustomize /usr/local/bin
            chmod +x /usr/local/bin/kustomize
      - run:
          name: set up google cloud sdk
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              apt-get install -qq -y gettext
              echo ${ACCT_AUTH} | base64 -d > ${HOME}/account-auth.json
              gcloud auth activate-service-account --key-file=${HOME}/account-auth.json
              gcloud --quiet config set project ${GCP_PROJECT}
              gcloud --quiet config set compute/zone ${CLOUDSDK_COMPUTE_ZONE}
              gcloud --quiet container clusters get-credentials ${GOOGLE_CLUSTER_NAME}    
            fi
      - run:
          name: deploy manifests to gke cluster
          command: |
            kustomize build ./overlays/production | kubectl apply -n prd -f -
      - slack/status:
          include_project_field: true
          success_message: 'Branch: $CIRCLE_BRANCH\nUser:$CIRCLE_USERNAME'
          failure_message: 'Branch: $CIRCLE_BRANCH\nUser:$CIRCLE_USERNAME'
          webhook: ${SLACK_WEBHOOK}
workflows:
  version: 2
  build:
    jobs:
      - deploy_to_prodction_cluster:
          filters:
            branches:
              only: master
      - deploy_to_staging_cluster:
          filters:
            branches:
              only: stg


各環境以下のkustomization.yamlは下記のようになります。
namePrefixの部分は、環境に応じてprd- or stg-となります。
(下記のマニフェストファイルはprd環境のものです)

imagesの部分はkustomize edit set imageコマンドによって書き換えられた内容です。

kubectl.docs.kubernetes.io

patchesStrategicMergeの部分では、環境差分を含んだファイルを指定します。
deployment_patch.yamlの中で、Podのreplica数など環境によって差異がある内容を定義することで、Manifestファイルのbuild時に環境差異を反映したファイルを作成できます。

kubectl.docs.kubernetes.io

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namePrefix: prd-
commonLabels:
  app: test

bases:
- ../../base
patchesStrategicMerge:
- deployment_patch.yaml
images:
- name: gcr.io/sample-cicd-271901/test-go-image
  newTag: <commit_hash>


最終的に環境ごとのNamespaceにGCRにpushされたImageを用いたDeploymentが作成されます。
(下記はStaging環境の検証結果ですが、stg-のPrefixが付いているPod (Deployment) が作成されていることがわかります。)

$ kubectl get pod -n stg 
NAME                            READY   STATUS             RESTARTS   AGE
stg-test-app-7b4bdd896f-2kdrf   1/1     Running   0          13s

$ kubectl describe deployment -n stg 
Name:                   stg-test-app
Namespace:              stg
...
Pod Template:
  Labels:  app=test
  Containers:
   test-app-container:
    Image:        gcr.io/sample-cicd-271901/test-go-image:ad948427c121c88d29854ae4f47e3fde56b8ff17

今後やりたいこと

今回は必要最低限の機能しか実装していないので、追加で下記のような機能を組み込めるといいなと考えています。
(機能追加ができたら、その部分も記事にしようと思います)

  • 脆弱性スキャナー(Trivy?)のCIへの組み込み
  • Conftestを利用したManifestファイルのValidation
  • Open Policy Agentによるポリシーの適用

また、クラスタの作成に関してはTerraformを利用しましたが、その内容に関してもいずれ記事にしようと思います。