はじめに
@taxin_ttと申します。
CI/CD Pipelineの実装+GCP / CircleCIの勉強も兼ねて、GKEのKubernetesクラスター上にサンプルアプリをDeployするCI/CD Pipelineを実装してみました。
構成
今回は、Circle CI + KustomizeでCI/CD Pipelineを実装しました。
(Deploy先のKubernetesクラスターもTerraformで作成できるようにしています。)
大まかな処理の流れは下記の通りです。
- コードの変更をApp repo(Stgブランチ)にPush
- MasterブランチへのMergeをトリガーとしてContainer ImageをBuild
- BuildしたContainer ImageをContainer RegistryにPush
- Manifest repo内のImage tagの書き換え
- 修正したManifest ファイルをKubernetes Clusterに反映
このフローはWeaveworksが出しているGitOps PipelineのExampleとほぼ同じような構成にしています。
(Circle CIがクラスタにManifest repoの変更を反映する(Push)の方式をとっているので、実際にはGitOpsではなくCIOpsの構成になっています。)
www.weave.works
レポジトリの構成
レポジトリはアプリ用のrepoとマニフェスト用のrepoに分けています。
動作確認自体はできていますが、未完成の部分も一部あります。
ブランチの構成
上記の2つのレポジトリでは、Prodction環境用のmaster
ブランチとStaging環境用のstg
ブランチの2つを用意しています。
最初に、 Developerがコードをstg
ブランチにPushします。
(実際には、さらに別のブランチを切ってstg
ブランチに対するPRをMergeするなどして、Staging環境用のブランチに変更を反映することになると思います。)
App Repo (stg
ブランチ) へのPushをトリガーとして、Circle CIのJobが起動してManifest Repo (stg
ブランチ) のImage tagを書き換えます。
App Repo (stg
ブランチ) の変更内容をmaster
ブランチにMergeします。
上記のMasterブランチへのMergeをトリガーとして、Manifest RepoのPR (stg
ブランチ → master
ブランチ) を作成します。
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を作成しておきます。
Service AccountにはRegistryへのPush権限を付与する必要があるため、「ストレージ管理者」の権限を付与しておきます。
最後に、Service Accountの作成時にキーファイルをダウンロードして、その中身をbase64でエンコードした上でCircle CIの環境変数(${ACCT_AUTH}
)として登録しておきます。
ImageのTagに関しては、直近のコミットハッシュをCircle CIの環境変数 ($CIRCLE_SHA1
) から取得してTagとして利用します。
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の書き換えを容易に行うことができます。
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
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 (stg
→ master
)の作成
create_pr_in_manifest_repo
のJobでは、Manifest repoに対してPull Requestを作成します。
このJobはApp repoでのMasterブランチへのMergeをトリガーとして実行されます。
Pull Requestの作成には、hubを利用します。
--base
と--head
でブランチを指定することでPRの設定を行います。
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
以下をstaging
とproduction
のディレクトリに分けて環境差分を切り出しています。
(公式が提供しているExampleのディレクトリ構成を参考に作成しました。)
GKE(Google Kubernetes Engine)へのDeployに必要な権限については、GCRへのImage pushと同様にService Accountを利用して権限を与えます。
GKEへのDeployの権限を付与する必要があるため、Service Accountには「Kubernetes Developer」の権限を付与しておきます。
(権限周りも含めて、下記の記事は参考になりました。)
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
コマンドによって書き換えられた内容です。
patchesStrategicMerge
の部分では、環境差分を含んだファイルを指定します。
deployment_patch.yaml
の中で、Podのreplica数など環境によって差異がある内容を定義することで、Manifestファイルのbuild時に環境差異を反映したファイルを作成できます。
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を利用しましたが、その内容に関してもいずれ記事にしようと思います。