1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenShiftで始めるCI/CD:GitLabとSonarQube、Tekton Triggersで実現するパイプライン自動化

Last updated at Posted at 2025-01-02

前提

本記事では、前回の記事に続いて、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,  // 空のテストスイートでもエラーにしない
    });
  };

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?