はじめに
@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に分けています。
動作確認自体はできていますが、未完成の部分も一部あります。
github.com
github.com
ブランチの構成
上記の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を作成しておきます。
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 (stg
→ master
)の作成
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
以下をstaging
とproduction
のディレクトリに分けて環境差分を切り出しています。
(公式が提供している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を利用しましたが、その内容に関してもいずれ記事にしようと思います。