前提
本記事では、前回の記事に続いて、OpenShift上のGitLabとSonarQube、Tekton Triggersを利用して、GitLabへのpushをトリガーに、Angularアプリのパイプラインを自動実行する仕組みを示す。
アーキテクチャ
リソース
作成するリソースは下記の通り。
Tekton Pipelines
Kubernetes CI/CDパイプラインの実装 (impress top gear) とOpenShift Pipelines のご紹介(Tekton Triggers編)を参考に作成。体系的に内容を理解したい場合は前者の本がおすすめ。
Pipeline
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: npm
spec:
params:
- name: url
type: string
- name: revision
type: string
workspaces:
- name: shared-workspace
tasks:
- name: git-clone
taskRef:
name: git-clone
kind: ClusterTask
params:
- name: url
value: "$(params.url)"
- name: revision
value: "$(params.revision)"
- name: sslVerify
value: "false"
workspaces:
- name: output
workspace: shared-workspace
- name: npm-test-install
runAfter:
- git-clone
taskSpec:
steps:
- name: npm-test-install
image: cypress/included
workingDir: /workspace/shared-workspace
script: |
#!/bin/sh
npm install
workspaces:
- name: shared-workspace
- name: npm-lint
runAfter:
- npm-test-install
taskSpec:
steps:
- name: npm-lint
image: cypress/included
workingDir: /workspace/shared-workspace
script: |
#!/bin/sh
npm run lint
workspaces:
- name: shared-workspace
- name: npm-test
runAfter:
- npm-lint
taskSpec:
steps:
- name: npm-test
image: cypress/included
workingDir: /workspace/shared-workspace
script: |
#!/bin/sh
npm run test:ci
workspaces:
- name: shared-workspace
- name: npm-sonar
runAfter:
- npm-test
taskSpec:
steps:
- name: npm-sonar
image: cypress/included
workingDir: /workspace/shared-workspace
script: |
#!/bin/sh
echo "SONAR_TOKEN: $SONAR_TOKEN"
echo "SONAR_HOST_URL: $SONAR_HOST_URL"
cat /workspace/shared-workspace/sonar-project.properties
npm run sonar:ci
env:
- name: SONAR_TOKEN
valueFrom:
secretKeyRef:
name: sonarqube-secret
key: SONAR_TOKEN
- name: SONAR_HOST_URL
valueFrom:
configMapKeyRef:
name: sonarqube-config
key: SONAR_HOST_URL
volumeMounts:
- name: sonarqube-config
mountPath: /workspace/shared-workspace/sonar-project.properties
subPath: sonar-project.properties
readOnly: true
volumes:
- name: sonarqube-config
configMap:
name: sonarqube-config
workspaces:
- name: shared-workspace
- name: npm-e2e-install
runAfter:
- npm-sonar
taskSpec:
steps:
- name: npm-e2e-install
image: mcr.microsoft.com/playwright:v1.49.1-noble
workingDir: /workspace/shared-workspace
script: |
#!/bin/sh
npm install
npx playwright install
workspaces:
- name: shared-workspace
- name: npm-e2e
runAfter:
- npm-e2e-install
taskSpec:
steps:
- name: e2e-test
image: mcr.microsoft.com/playwright:v1.49.1-noble
workingDir: /workspace/shared-workspace
script: |
#!/bin/sh
npm run e2e
workspaces:
- name: shared-workspace
Trigger Binding
GitLab webhookのbodyは下記を参照。
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
name: gitlab-binding
spec:
params:
- name: url
value: "$(body.repository.git_ssh_url)"
- name: revision
value: "$(body.ref)"
Trigger Template
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
name: gitlab-trigger-template
spec:
params:
- name: url
- name: revision
resourcetemplates:
- apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
generateName: npm-
spec:
pipelineRef:
name: npm
podTemplate:
securityContext:
fsGroup: 65532
runAsUser: 1000
runAsGroup: 1000
params:
- name: url
value: "$(tt.params.url)"
- name: revision
value: "$(tt.params.revision)"
workspaces:
- name: shared-workspace
persistentVolumeClaim:
claimName: shared-workspace
serviceAccountName: git-sa
EventListener
検証目的のため、下記のようにinterceptorsは実装していない。
参考:https://tekton.dev/docs/triggers/interceptors/#gitlab-interceptors
interceptors:
- gitlab:
secretRef:
secretName: gitlab-webhook
secretKey: webhook-secret
eventTypes:
- push
- cel:
filter: "body.ref == \"refs/heads/main\""
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: gitlab-listener
spec:
serviceAccountName: git-sa
triggers:
- name: gitlab-trigger
bindings:
- ref: gitlab-binding
template:
ref: gitlab-trigger-template
Role
下記URLを参考に作成。
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tekton-triggers-role
rules:
# EventListeners need to be able to fetch all namespaced resources
- apiGroups: ["triggers.tekton.dev"]
resources: ["eventlisteners", "triggerbindings", "triggertemplates", "triggers"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
# configmaps is needed for updating logging config
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
# Permissions to create resources in associated TriggerTemplates
- apiGroups: ["tekton.dev"]
resources: ["pipelineruns", "pipelineresources", "taskruns"]
verbs: ["create"]
- apiGroups: [""]
resources: ["serviceaccounts"]
verbs: ["impersonate"]
- apiGroups: ["policy"]
resources: ["podsecuritypolicies"]
resourceNames: ["tekton-triggers"]
verbs: ["use"]
RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tekton-triggers-rolebinding
subjects:
- kind: ServiceAccount
name: git-sa
roleRef:
kind: Role
name: tekton-triggers-role
apiGroup: rbac.authorization.k8s.io
ClusterRole
下記URLを参考に作成。
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: tekton-triggers-clusterrole
rules:
# EventListeners need to be able to fetch any clustertriggerbindings
- apiGroups: ["triggers.tekton.dev"]
resources: ["clustertriggerbindings", "clusterinterceptors"]
verbs: ["get", "list", "watch"]
ClusterRoleBinging
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: tekton-triggers-clusterbinding
subjects:
- kind: ServiceAccount
name: git-sa
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: tekton-triggers-clusterrole
GitLab
前回の記事同様、git cloneはsshで行う前提。
Deployment
内部IPの名前解決となるため、allow_local_requests_from_web_hooks_and_servicesとenforce_dns_rebinding_protectionなどの設定が必要。
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitlab
labels:
app: gitlab
spec:
replicas: 1
selector:
matchLabels:
app: gitlab
template:
metadata:
labels:
app: gitlab
spec:
containers:
- name: gitlab
image: docker.io/gitlab/gitlab-ce:17.7.0-ce.0
resources:
limits:
memory: "8Gi"
cpu: "1"
requests:
memory: "4Gi"
cpu: "500m"
securityContext:
runAsUser: 0
runAsGroup: 0
allowPrivilegeEscalation: true
privileged: true
ports:
- containerPort: 80
- containerPort: 443
- containerPort: 22
env:
- name: GITLAB_OMNIBUS_CONFIG
value: |
external_url '<RoutesのURL>';
gitlab_rails['gitlab_shell_ssh_port'] = 30222;
gitlab_rails['webhook_timeout'] = 60;
gitlab_rails['trusted_proxies'] = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
gitlab_rails['allow_local_requests_from_web_hooks_and_services'] = true;
gitlab_rails['allow_local_requests_from_system_hooks'] = true;
gitlab_rails['enforce_dns_rebinding_protection'] = false;
volumeMounts:
- name: gitlab-config
mountPath: /etc/gitlab/gitlab.rb
subPath: gitlab.rb
- name: gitlab-logs
mountPath: /var/log/gitlab
- name: gitlab-data
mountPath: /var/opt/gitlab
volumes:
- name: gitlab-config
configMap:
name: gitlab-config
- name: gitlab-logs
persistentVolumeClaim:
claimName: gitlab-logs-pvc
- name: gitlab-data
persistentVolumeClaim:
claimName: gitlab-data-pvc
Service
apiVersion: v1
kind: Service
metadata:
name: gitlab
spec:
type: NodePort
selector:
app: gitlab
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
nodePort: 30080
- name: https
protocol: TCP
port: 443
targetPort: 443
nodePort: 30443
- name: ssh
protocol: TCP
port: 22
targetPort: 22
nodePort: 30022
PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitlab-config-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitlab-logs-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitlab-data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
ConfigMap
envと内容が重複するため、どちらかで良いと思われる。
apiVersion: v1
kind: ConfigMap
metadata:
name: gitlab-config
data:
gitlab.rb: |
external_url '<RoutesのURL>'
gitlab_rails['gitlab_shell_ssh_port'] = 30022
gitlab_rails['webhook_timeout'] = 60
gitlab_rails['trusted_proxies'] = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
gitlab_rails['allow_local_requests_from_web_hooks_and_services'] = true
gitlab_rails['allow_local_requests_from_system_hooks'] = true
gitlab_rails['enforce_dns_rebinding_protection'] = false
Secret
apiVersion: v1
kind: Secret
metadata:
name: git-auth-secret
annotations:
tekton.dev/git-0: <gitlabのサービス名>
type: kubernetes.io/ssh-auth
data:
ssh-privatekey: <base64エンコードしたssh秘密鍵>
SonarQube
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: sonarqube
labels:
app: sonarqube
spec:
selector:
matchLabels:
app: sonarqube
template:
metadata:
labels:
app: sonarqube
spec:
containers:
- name: sonarqube
image: sonarqube:lts
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
ports:
- containerPort: 9000
env:
- name: SONAR_ES_BOOTSTRAP_CHECKS_DISABLE
value: "true"
- name: SONAR_SEARCH_JAVAOPTS
value: "-Xms512m -Xmx1g"
volumeMounts:
- name: sonarqube-data
mountPath: /opt/sonarqube/data
- name: sonarqube-logs
mountPath: /opt/sonarqube/logs
- name: sonarqube-extensions
mountPath: /opt/sonarqube/extensions
volumes:
- name: sonarqube-data
persistentVolumeClaim:
claimName: sonarqube-data
- name: sonarqube-logs
persistentVolumeClaim:
claimName: sonarqube-logs
- name: sonarqube-extensions
persistentVolumeClaim:
claimName: sonarqube-extensions
Service
apiVersion: v1
kind: Service
metadata:
name: sonarqube
spec:
type: NodePort
selector:
app: sonarqube
ports:
- port: 9000
targetPort: 9000
nodePort: 30090
PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarqube-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarqube-logs
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: sonarqube-extensions
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: sonarqube-config
data:
sonar-project.properties: |
sonar.projectName=angular
sonar.projectKey=angular
sonar.sources=src
sonar.tests=tests
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.junit.reportPaths=results/test/results.xml
sonar.exclusions=**/*.spec.ts,**/node_modules/**,**/tests/**
sonar.eslint.reportPaths=results/lint/eslint-report.json
SONAR_HOST_URL: http://sonarqube:9000
Secret
apiVersion: v1
kind: Secret
metadata:
name: sonarqube-secret
type: Opaque
data:
SONAR_TOKEN: <SonarQubeで発行したトークン>
Angular
package.json
{
"name": "my-first-angular-app",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test --code-coverage",
"test:ci": "ng test --no-watch --no-progress --browsers=ChromeHeadless --code-coverage",
"e2e": "playwright test",
"lint": "eslint 'src/**/*.{ts,html}' --output-file results/lint/eslint-report.json --format json",
"sonar": "node sonar-scanner.js",
"sonar:ci": "sonar-scanner -Dsonar.login=$SONAR_TOKEN -Dsonar.host.url=$SONAR_HOST_URL -X"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.0.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.0.6",
"@angular/cli": "^19.0.6",
"@angular/compiler-cli": "^19.0.0",
"@playwright/test": "^1.49.1",
"@types/jasmine": "~5.1.0",
"angular-eslint": "19.0.2",
"eslint": "^9.16.0",
"jasmine-core": "~5.4.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-junit-reporter": "^2.0.1",
"karma-webpack": "^5.0.1",
"puppeteer": "^23.11.1",
"sonarqube-scanner": "^4.2.6",
"typescript": "~5.6.2",
"typescript-eslint": "8.18.0"
}
}
karma.conf.js
module.exports = function (config) {
config.set({
// ベースパス(ファイルを解決する際の基準ディレクトリ)
basePath: '',
// フレームワークを指定(例: Jasmine, Mochaなど)
frameworks: ['jasmine', '@angular-devkit/build-angular'],
// ファイルやパターンを指定してテストに含める
files: [],
// ファイルやパターンを指定してテストから除外する
exclude: [],
// プリプロセッサを指定
preprocessors: {},
// レポート形式
reporters: ['progress', 'junit', 'coverage'],
// テスト結果を保存するディレクトリ
junitReporter: {
outputDir: require('path').join(__dirname, './results/test'),
outputFile: 'results.xml',
useBrowserName: false
},
// サーバーのポート
port: 9876,
// カラフルなログ出力を有効化
colors: true,
// ログレベル(例: config.LOG_INFO, config.LOG_DEBUGなど)
logLevel: config.LOG_INFO,
// ファイル変更を監視して自動でテストを再実行する
autoWatch: true,
// 使用するブラウザを指定
browsers: ['ChromeHeadless'],
// カスタムブラウザのランチャーを設定
customLaunchers: {
ChromeHeadlessCustom: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--remote-debugging-port=9222']
}
},
// シングルラン実行モード(デフォルト: false)
singleRun: false,
// 並列実行の数(デフォルト: 無制限)
concurrency: Infinity,
// タイムアウト設定
captureTimeout: 120000, // Chromeが接続を確立するまでのタイムアウト
browserDisconnectTimeout: 10000, // ブラウザが切断されるまでのタイムアウト
browserNoActivityTimeout: 60000, // テスト中のアクティビティがない場合のタイムアウト
// Angular CLIの構成をロード
client: {
clearContext: false, // Karmaの実行後、ブラウザのコンテキストをクリアしない
},
// プラグインの設定
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-coverage'),
require('karma-junit-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
],
coverageReporter: {
dir: require('path').join(__dirname, './coverage'), // 出力先ディレクトリ
reporters: [
{ type: 'html' }, // HTML形式のレポート
{ type: 'lcovonly', subdir: '.', file: 'lcov.info' } // lcov.infoを生成
],
fixWebpackSourcePaths: true
},
failOnEmptyTestSuite: false, // 空のテストスイートでもエラーにしない
});
};