前回の記事では、各ツールによるマルチアーキテクチャーに対応したコンテナイメージのビルド方法について検証しました。
今回からはその応用として、マルチアーキテクチャービルドをCI/CDパイプラインに組み込んだ構成を試していきます。
本記事ではOpenShift Pipelines (Tekton Pipelines) を使用したCIパイプラインの構成について解説します。
GitOpsを実現するためのTekton TriggersやCDパイプラインの構成については別記事で追って解説していきます。
前提環境
コンテナ基盤はRed Hat OpenShift Service on AWS(ROSA) を使用します。
- Client Version: 4.10.12
- Server Version: 4.9.21
- Kubernetes Version: v1.22.3+fdba464
CIパイプラインは、
OpenShift Pipelines (Tekton Pipelines) をOperatorでInstallしておきます。
- Client version: 0.23.1
- Pipeline version: v0.28.3
ソースコードリポジトリとマニフェストリポジトリはGitLabでプロジェクトを分けて管理します。
ソースコードリポジトリに関しては、今回使用するJavaサンプルアプリのAcme Airのリポジトリをフォークして作成します。
マニフェストリポジトリに関しては、任意の名前でプロジェクトを作成しておきます。
CIパイプラインの構成
Tekton Pipelinesでは、それぞれの処理をTaskとして定義し、それを組み合わせることでPipelineを構成します。
今回のPipelineは、以下のようなTaskを順番に実行してきます。
- ソースコードリポジトリとマニフェストリポジトリをそれぞれClone
- ソースコードの静的解析
- ソースコードビルド&コンテナイメージビルド
- ビルドしたイメージの脆弱性スキャン
- アプリマニフェストの更新
- アプリマニフェストのPush
重要なのは3のコンテナイメージビルドで、これをマルチアーキテクチャービルドとして実装することで、AMD64とs390xに対応したイメージを作成します。
パイプライン作成手順
プロジェクト作成
最初に検証用のプロジェクトを作成します。ここに各リソースを展開していきます。
$ oc new-project pipeline-work
Now using project "pipeline-work" on server ...
Pipelineリソース作成
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: pipeline
spec:
workspaces:
- name: shared-workspace
- name: manifest-workspace
- name: sonar-settings
params:
- name: SOURCE_URL
type: string
- name: MANIFEST_URL
type: string
- name: SONAR_HOST_URL
type: string
- name: SONAR_PROJECT_KEY
type: string
- name: REGISTRY_PATH
type: string
- name: MULTI_ARCH_IMAGE
type: string
tasks:
- name: git-clone-source
taskRef:
kind: ClusterTask
name: git-clone
params:
- name: url
value: $(params.SOURCE_URL)
- name: subdirectory
value: ""
- name: deleteExisting
value: "true"
workspaces:
- name: output
workspace: shared-workspace
- name: git-clone-manifest
taskRef:
kind: ClusterTask
name: git-clone
params:
- name: url
value: $(params.MANIFEST_URL)
- name: subdirectory
value: ""
- name: deleteExisting
value: "true"
workspaces:
- name: output
workspace: manifest-workspace
- name: sonarqube-scanner
taskRef:
name: sonarqube-scanner
runAfter:
- git-clone-source
- git-clone-manifest
params:
- name: SONAR_HOST_URL
value: $(params.SONAR_HOST_URL)
- name: SONAR_PROJECT_KEY
value: $(params.SONAR_PROJECT_KEY)
workspaces:
- name: source-dir
workspace: shared-workspace
- name: sonar-settings
workspace: sonar-settings
- name: multi-arch-build
taskRef:
name: docker-buildx
runAfter:
- sonarqube-scanner
params:
- name: MULTI_ARCH_IMAGE
value: $(params.MULTI_ARCH_IMAGE)
- name: REGISTRY_PATH
value: $(params.REGISTRY_PATH)
workspaces:
- name: source
workspace: shared-workspace
- name: image-scan
taskRef:
name: trivy-scanner
runAfter:
- multi-arch-build
params:
- name: REGISTRY_PATH
value: $(params.REGISTRY_PATH)
- name: MULTI_ARCH_IMAGE
value: $(params.MULTI_ARCH_IMAGE)
workspaces:
- name: manifest-dir
workspace: shared-workspace
- name: update-manifest
params:
- name: MULTI_ARCH_IMAGE
value: $(params.MULTI_ARCH_IMAGE)
taskRef:
kind: Task
name: update-manifest
runAfter:
- image-scan
workspaces:
- name: manifest-dir
workspace: manifest-workspace
- name: push-manifest
params:
- name: MULTI_ARCH_IMAGE
value: $(params.MULTI_ARCH_IMAGE)
taskRef:
kind: Task
name: push-manifest
runAfter:
- update-manifest
workspaces:
- name: manifest-dir
workspace: manifest-workspace
.spec.tasksで実行するTaskを指定し、.spec.paramsで定義した変数を渡します。(変数の値はPipelineRunというインスタンス定義で指定します)
Taskリソース作成
続いてPipelineで実行されるTaskを作成します。
git-clone-sourceとgit-clone-manifestについてはTekton Hubで提供されているTaskを使用しますが、他の処理は適切なものがないので自分で作成します。
ソースコード静的解析
ツールはSonarQubeを使用します。
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: sonarqube-scanner
labels:
app.kubernetes.io/version: "0.1"
annotations:
tekton.dev/pipelines.minVersion: "0.12.1"
tekton.dev/categories: Security
tekton.dev/tags: security
tekton.dev/displayName: "sonarqube scanner"
tekton.dev/platforms: "linux/amd64"
spec:
description: >-
The following task can be used to perform static analysis on the source code
provided the SonarQube server is hosted
SonarQube is the leading tool for continuously inspecting the Code Quality and Security
of your codebases, all while empowering development teams. Analyze over 25 popular
programming languages including C#, VB.Net, JavaScript, TypeScript and C++. It detects
bugs, vulnerabilities and code smells across project branches and pull requests.
workspaces:
- name: source-dir
- name: sonar-settings
params:
- name: SONAR_HOST_URL
description: Host URL where the sonarqube server is running
default: ""
- name: SONAR_PROJECT_KEY
description: Project's unique key
default: ""
steps:
- name: sonar-properties-create
image: registry.access.redhat.com/ubi8/ubi-minimal:8.2
workingDir: $(workspaces.source-dir.path)
script: |
#!/usr/bin/env bash
replaceValues() {
filename=$1
thekey=$2
newvalue=$3
if ! grep -R "^[#]*\s*${thekey}=.*" $filename >/dev/null; then
echo "APPENDING because '${thekey}' not found"
echo "" >>$filename
echo "$thekey=$newvalue" >>$filename
else
echo "SETTING because '${thekey}' found already"
sed -ir "s|^[#]*\s*${thekey}=.*|$thekey=$newvalue|" $filename
fi
}
if [[ -f $(workspaces.sonar-settings.path)/sonar-project.properties ]]; then
echo "using user provided sonar-project.properties file"
cp -RL $(workspaces.sonar-settings.path)/sonar-project.properties $(workspaces.source-dir.path)/sonar-project.properties
fi
if [[ -f $(workspaces.source-dir.path)/sonar-project.properties ]]; then
if [[ -n "$(params.SONAR_HOST_URL)" ]]; then
replaceValues $(workspaces.source-dir.path)/sonar-project.properties sonar.host.url $(params.SONAR_HOST_URL)
fi
if [[ -n "$(params.SONAR_PROJECT_KEY)" ]]; then
replaceValues $(workspaces.source-dir.path)/sonar-project.properties sonar.projectKey $(params.SONAR_PROJECT_KEY)
fi
else
touch sonar-project.properties
echo "sonar.projectKey=$(params.SONAR_PROJECT_KEY)" >> sonar-project.properties
echo "sonar.host.url=$(params.SONAR_HOST_URL)" >> sonar-project.properties
echo "sonar.sources=." >> sonar-project.properties
fi
echo "---------------------------"
cat $(workspaces.source-dir.path)/sonar-project.properties
- name: sonar-scan
image: docker.io/sonarsource/sonar-scanner-cli:4.5@sha256:b8c95a37025f3c13162118cd55761ea0b2a13d1837f9deec51b7b6d82c52040a #tag: 4.5
workingDir: $(workspaces.source-dir.path)
command:
- sonar-scanner
- -Dsonar.login=xxx
- -Dsonar.password=xxx
ソースコードビルド & マルチアーキテクチャービルド
Dockerfileのマルチステージビルドを利用し、Mavenでソースコードをビルドした上で、WebSphere Libertyのイメージとしてビルドします。
事前にフォークしたAcme AirのリポジトリのDockerfileを以下のように編集しておきます。
FROM maven:3.8.4-jdk-11-slim AS build-stage
COPY . /project
WORKDIR /project/
RUN mvn clean install
FROM ibmcom/websphere-liberty:kernel-java8-ibmjava-ubi
COPY --chown=1001:0 --from=build-stage /project/src/main/liberty/config/server.xml /config/server.xml
COPY --chown=1001:0 --from=build-stage /project/src/main/liberty/config/server.env /config/server.env
COPY --chown=1001:0 --from=build-stage /project/src/main/liberty/config/jvm.options /config/jvm.options
COPY --chown=1001:0 --from=build-stage /project/target/acmeair-mainservice-java-5.0.war /config/apps/
RUN configure.sh
続いてTaskです。
マルチアーキテクチャービルドにはDocker Buildxを使用するため、Docker DaemonをSidecarで稼働させています。(こちらを参考)
またBuildxはビルドしたタイミングでそのままイメージをレジストリにPushする仕様となっています。
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
annotations:
tekton.dev/displayName: multiarch-build
tekton.dev/pipelines.minVersion: 0.12.1
tekton.dev/tags: docker, build-image, push-image, dind, buildx, multiarch-build
generation: 5
labels:
app.kubernetes.io/version: "0.1"
name: multi-arch build
spec:
params:
- description: ""
name: REGISTRY_PATH
type: string
- description: ""
name: MULTI_ARCH_IMAGE
type: string
- default: docker.io/library/docker:20.10.12
description: ""
name: BUILDER_IMAGE
type: string
- default: ./Dockerfile
description: ""
name: DOCKERFILE
type: string
- default: .
description: ""
name: CONTEXT
type: string
- default: --platform linux/amd64,linux/s390x --no-cache
description: ""
name: BUILD_EXTRA_ARGS
type: string
- default: --push
description: ""
name: PUSH_EXTRA_ARGS
type: string
results:
- description: ""
name: IMAGE_DIGEST
sidecars:
- args:
- --storage-driver=vfs
- --userland-proxy=false
- --debug
env:
- name: DOCKER_TLS_CERTDIR
value: /certs
image: docker:20.10.12-dind
name: server
readinessProbe:
exec:
command:
- ls
- /certs/client/ca.pem
periodSeconds: 1
resources: {}
securityContext:
privileged: true
volumeMounts:
- mountPath: /certs/client
name: dind-certs
steps:
- env:
- name: DOCKER_HOST
value: tcp://localhost:2376
- name: DOCKER_TLS_VERIFY
value: "1"
- name: DOCKER_CERT_PATH
value: /certs/client
image: $(params.BUILDER_IMAGE)
name: build
resources: {}
script:
"# install depends\n
apk add curl jq\n\n
# enable experimental buildx features\n
export DOCKER_BUILDKIT=1\n
export DOCKER_CLI_EXPERIMENTAL=enabled\n\n
# Download latest buildx bin from github\n
mkdir -p ~/.docker/cli-plugins/\n
BUILDX_LATEST_BIN_URI=$(curl -s -L https://github.com/docker/buildx/releases/latest | grep 'linux-amd64'
| grep 'href' | sed 's/.*href=\"/https:\\/\\/github.com/g; s/amd64\".*/amd64/g')\n
curl -s -L ${BUILDX_LATEST_BIN_URI} -o ~/.docker/cli-plugins/docker-buildx\n
chmod a+x ~/.docker/cli-plugins/docker-buildx\n\n
# Get and run the latest docker/binfmt tag to use its qemu parts\n
BINFMT_IMAGE_TAG=$(curl -s https://registry.hub.docker.com/v2/repositories/docker/binfmt/tags
| jq '.results | sort_by(.last_updated)[-1].name' -r)\n
docker run --rm --privileged docker/binfmt:${BINFMT_IMAGE_TAG}\n\n
docker context create tls-environment\n
# create the multibuilder\ndocker buildx create --name multibuilder --use tls-environment\n
docker buildx use multibuilder\n\n
# login to a registry\n
# build the containers and push them to the registry then display the images\n
docker buildx build $(params.BUILD_EXTRA_ARGS) -f $(params.DOCKERFILE) -t $(params.REGISTRY_PATH)$(params.MULTI_ARCH_IMAGE) $(params.CONTEXT) $(params.PUSH_EXTRA_ARGS)\n"
volumeMounts:
- mountPath: /certs/client
name: dind-certs
workingDir: $(workspaces.source.path)
volumes:
- emptyDir: {}
name: dind-certs
workspaces:
- name: source
ビルドしたイメージの脆弱性スキャン
脆弱性スキャンにはTrivyを使用します。
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: trivy-scanner
labels:
app.kubernetes.io/version: "0.1"
annotations:
tekton.dev/pipelines.minVersion: "0.12.1"
tekton.dev/categories: Security
tekton.dev/tags: CLI, trivy
tekton.dev/displayName: "trivy scanner"
tekton.dev/platforms: "linux/amd64"
spec:
description: >-
Trivy is a simple and comprehensive scanner for
vulnerabilities in container images,file systems
,and Git repositories, as well as for configuration issues.
This task can be used to scan for vulnenrabilities on the source code
in stand alone mode.
workspaces:
- name: manifest-dir
params:
- name: TRIVY_IMAGE
default: docker.io/aquasec/trivy@sha256:dea76d4b50c75125cada676a87ac23de2b7ba4374752c6f908253c3b839201d9
description: Trivy scanner image to be used
- name: REGISTRY_PATH
description: Path to be scanned by trivy.
type: string
- name: MULTI_ARCH_IMAGE
description: Image to be scanned by trivy.
type: string
steps:
- name: trivy-scan
image: $(params.TRIVY_IMAGE)
workingDir: $(workspaces.manifest-dir.path)
script: |
#!/usr/bin/env sh
cmd="trivy $* $(params.REGISTRY_PATH)$(params.MULTI_ARCH_IMAGE)"
echo "Running trivy task with command below"
echo "$cmd"
eval "$cmd"
アプリマニフェストの更新
アプリは以下のDeploymentリソースでデプロイします。
apiVersion: apps/v1
kind: Deployment
metadata:
name: acmeair-mainservice
spec:
replicas: 1
selector:
matchLabels:
name: acmeair-main-deployment
template:
metadata:
labels:
name: acmeair-main-deployment
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9080"
spec:
imagePullSecrets:
- name: gitlab-token
containers:
- name: acmeair-mainservice-java
image: registry.gitlab.com/hoge/acmeair-manifests/acmeair:run-jb98k
imagePullPolicy: Always
ports:
- containerPort: 9080
- containerPort: 9443
ただアプリイメージはビルドする度にtagを変更するため、マニフェストに対してもそれを反映する必要があります。
その処理を実行するTaskは以下です。
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: update-manifest
spec:
workspaces:
- name: manifest-dir
params:
- name: MULTI_ARCH_IMAGE
type: string
steps:
- name: modify-image-tag
image: docker.io/redhat/ubi8
workingDir: $(workspaces.manifest-dir.path)
script: |
sed -iE "s/image:.*$/image: registry.gitlab.com\/hoge\/acmeair-manifests\/$(params.MULTI_ARCH_IMAGE)/g" manifests-openshift/deploy-acmeair-mainservice-java.yaml && cat manifests-openshift/deploy-acmeair-mainservice-java.yaml |grep image:
マニフェストのPush
最後に更新したマニフェストファイルをリポジトリにPushします。
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: push-manifest
spec:
workspaces:
- name: manifest-dir
params:
- name: MULTI_ARCH_IMAGE
type: string
steps:
- name: modify-image-tag
image: docker.io/bitnami/git:2.36.1
workingDir: $(workspaces.manifest-dir.path)
script: |
git checkout -b dev
git config --global user.email "you@example.com"
git add manifests-openshift/deploy-acmeair-mainservice-java.yaml
git commit -m "Change the app image tag to $(params.MULTI_ARCH_IMAGE)"
git push origin dev -o merge_request.create -o merge_request.target=main
まとめ
ここまでPipelineとそれぞれのTaskを定義しました。次回以降Tekton TriggersとArgo CDを利用したGitOpsの実装までを実施します。