Published on

CICD With Tekton and ArgoCD

Authors
  • avatar
    Name
    Sunway
    Twitter

In this blog, you can implement a ci/cd demo with tekton adn argocd

  • Flow 1: User do a push or pull_request operation to a private GitHub repo
  • Flow 2: GitHub Repo Webhook send a request to tekton eventListener
  • Flow 3: EventListener determines whether it needs to create a pipelineRun based on the triggerTemplate according to the conditions define in filter.
  • Flow 4: EventListener create a pipelineRun which contains 4 task: git clone, docker build and push, update helm chart image tag, send notification
  • Flow 5: Argocd auto-sync helm files in GitHub repo after task3(update helm chart image tag) run successfully
  • Flow 6: Argocd use the latest image to build a new pod and then delete the old one

1 Prerequisite

1.1 Install Tekton

# https://tekton.dev/docs/installation/pipelines/

# kubectl apply -f https://raw.githubusercontent.com/stakater/Reloader/master/deployments/kubernetes/reloader.yaml

kubectl apply --filename https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml
kubectl apply --filename https://storage.googleapis.com/tekton-releases/dashboard/latest/release.yaml
kubectl apply --filename https://storage.googleapis.com/tekton-releases/triggers/latest/release.yaml
kubectl apply --filename https://storage.googleapis.com/tekton-releases/triggers/latest/interceptors.yaml

# Tekton Client RPM(Redhat/CentOS...) install
rpm -Uvh  https://github.com/tektoncd/cli/releases/download/v0.38.0/tektoncd-cli-0.38.0_Linux-64bit.rpm

# Tekton Client DEB(Ubuntu/Debian...) install
wget https://github.com/tektoncd/cli/releases/download/v0.38.0/tektoncd-cli-0.38.0_Linux-64bit.deb
dpkg -i tektoncd-cli-0.38.0_Linux-64bit.deb && rm -f tektoncd-cli-0.38.0_Linux-64bit.deb

1.2 Intall ArgoCD

# https://argo-cd.readthedocs.io/en/stable/getting_started/

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Install ArgoCD CLI in x86_64 Linux
VERSION=$(curl -L -s https://raw.githubusercontent.com/argoproj/argo-cd/stable/VERSION)
wget https://github.com/argoproj/argo-cd/releases/download/v$VERSION/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/bin/argocd
rm argocd-linux-amd64

1.3 Prepare a demo git repo with helm or kustomize

git clone https://github.com/sunway910/cicd-demo

1.4 Secret Configuration

Create Docker Credential Secret

# If your repo is private, please generate the secret as below

# Tekton task kaniko use this secret to push image to a docker repo
kubectl create secret docker-registry docker-cred \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username="sunway" \
  --docker-password="sunway.run" \
  --docker-email="[email protected]"

Create GitHub Credential Secret

# Tekton use this secret to clone and push to a private git repo
# Please delete all configuration like github-cred in this blog if git repo is a public repo
kubectl create secret generic github-cred \
  --from-file=id_rsa=/root/.ssh/id_rsa \
  --from-file=known_hosts=/root/.ssh/known_hosts

Set argocd server insecure if you want

# set argocd tls: https://argo-cd.readthedocs.io/en/stable/operator-manual/tls/
# set insecure mode if you want
kubectl edit cm -n argocd argocd-cmd-params-cm

# add data in comfigmap as below
data:
  server.insecure: "true"

kubectl rollout restart deployment argocd-server -n argocd
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

ArgoCD Create non-admin user for dev-team

2 Build Pipeline

2.0 Create ServiceAccount

sunway123 is the secret should be set in your GitHub Repo Webhook

sa.yaml

apiVersion: v1
kind: Secret
metadata:
  name: github-triggers-secret
type: Opaque
stringData:
  secretToken: "sunway123"
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tekton-triggers-github-sa
secrets:
  - name: github-triggers-secret
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tekton-triggers-github-minimal
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"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tekton-triggers-github-binding
subjects:
  - kind: ServiceAccount
    name: tekton-triggers-github-sa
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: tekton-triggers-github-minimal
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: tekton-triggers-github-clusterrole
rules:
  - apiGroups: ["triggers.tekton.dev"]
    resources: ["clustertriggerbindings", "clusterinterceptors","interceptors"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: tekton-triggers-github-clusterbinding
subjects:
  - kind: ServiceAccount
    name: tekton-triggers-github-sa
    namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: tekton-triggers-github-clusterrole
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: pipeline-account
---
apiVersion: v1
kind: Secret
metadata:
  name: kube-api-secret
  annotations:
    kubernetes.io/service-account.name: pipeline-account
type: kubernetes.io/service-account-token
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: pipeline-role
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pipeline-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: pipeline-role
subjects:
  - kind: ServiceAccount
    name: pipeline-account

2.1 Task: Git Clone

Use command kubectl apply -f task-git-clone.yaml to add task: git-clone, you can also use command tkn hub install task git-clone to get task from hub and then customize the yaml file kubectl get task git-clone -o yaml > task-git-clone.yaml by yourself.

task-git-clone.yaml

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: git-clone
spec:
  workspaces:
    - name: output
      description: The git repo will be cloned onto the volume backing this workspace
    - name: ssh-directory
  params:
    - name: repo_url
      description: git repo url to clone
      type: string
    - name: revision
      description: git revision to checkout (branch, tag, sha, ref…)
      type: string
      default: main
    - name: submodules
      description: defines if the resource should initialize and fetch the submodules
      type: string
      default: "true"
    - name: depth
      description: performs a shallow clone where only the most recent commit(s) will be fetched
      type: string
      default: "1"
    - name: sslVerify
      description: defines if http.sslVerify should be set to true or false in the global git config
      type: string
      default: "true"
    - name: subdirectory
      description: subdirectory inside the "output" workspace to clone the git repo into
      type: string
      default: ""
    - name: deleteExisting
      description: clean out the contents of the repo's destination directory (if it already exists) before trying to clone the repo there
      type: string
      default: "false"
  results:
    - name: commit
      description: The precise commit SHA that was fetched by this Task
    - name: pipeline-start-time
      description: "The start time of the pipeline"
    - name: commit-info
      description: "The latest commit info"
  steps:
    - name: clone
      image: gcriotekton/pipeline-git-init:latest
      securityContext:
        runAsUser: 0 # This needs root, and git-init is nonroot by default
      script: |
        #!/usr/bin/env sh
        set -eu
        export TZ=UTC-8
        TZ='Asia/Hong_Kong' echo -n "$(date '+Date: %Y-%m-%d Time: %H:%M:%S')" > $(results.pipeline-start-time.path)
        if [[ -d $(workspaces.ssh-directory.path) ]]; then
          mkdir -p ~/.ssh
          cp $(workspaces.ssh-directory.path)/known_hosts ~/.ssh/known_hosts
          cp $(workspaces.ssh-directory.path)/id_rsa ~/.ssh/id_rsa
          chmod 700 ~/.ssh
          chmod 600 ~/.ssh/*
        fi
        CHECKOUT_DIR="$(workspaces.output.path)/$(params.subdirectory)"
        cleandir() {
          # Delete any existing contents of the repo directory if it exists.
          #
          # We don't just "rm -rf $CHECKOUT_DIR" because $CHECKOUT_DIR might be "/"
          # or the root of a mounted volume.
          if [[ -d "$CHECKOUT_DIR" ]] ; then
            # Delete non-hidden files and directories
            rm -rf "$CHECKOUT_DIR"
            # Delete files and directories starting with . but excluding ..
            rm -rf "$CHECKOUT_DIR"/.[!.]*
            # Delete files and directories starting with .. plus any other character
            rm -rf "$CHECKOUT_DIR"/..?*
          fi
        }
        if [[ "$(params.deleteExisting)" == "true" ]] ; then
          cleandir
        fi
        /ko-app/git-init \
          -url "$(params.repo_url)" \
          -revision "$(params.revision)" \
          -path "$CHECKOUT_DIR" \
          -sslVerify="$(params.sslVerify)" \
          -submodules="$(params.submodules)" \
          -depth="$(params.depth)"
        cd "$CHECKOUT_DIR"
        git log -1 > $(results.commit-info.path)
        # can checkout to a specific branch or revision
        git checkout "$(params.revision)"
        RESULT_SHA="$(git rev-parse HEAD | tr -d '\n')"
        EXIT_CODE="$?"
        if [ "$EXIT_CODE" != 0 ]
        then
          exit $EXIT_CODE
        fi
        # Make sure we don't add a trailing newline to the result!
        echo -n "$RESULT_SHA" > $(results.commit.path)

2.2 Task: Docker Build And Push

Use command kubectl apply -f task-kaniko.yaml to add task: kaniko-build-and-push, you can also use command tkn hub install task kaniko to get task from hub and then customize the yaml file kubectl get task kaniko -o yaml > task-kaniko.yaml by yourself.

task-kaniko.yaml

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: kaniko-build-and-push
spec:
  workspaces:
    - name: source
      description: Holds the context and Dockerfile
    - name: dockerconfig
      description: Includes a docker `config.json`
      optional: true
      mountPath: /kaniko/.docker
  params:
    - name: IMAGE
      description: Name (reference) of the image to build.
    - name: DOCKERFILE
      description: Path to the Dockerfile to build.
      default: ./Dockerfile
    - name: CONTEXT
      description: The build context used by Kaniko.
      default: ./
    - name: EXTRA_ARGS
      type: array
      default: [--ignore-path=/product_uuid]
    - name: BUILDER_IMAGE
      description: The image on which builds will run
      default: gcr.io/kaniko-project/executor:latest
  results:
    - name: IMAGE_DIGEST
      description: Digest of the image just built.
    - name: IMAGE_URL
      description: URL of the image just built.
    - name: image-build-end-time
      description: "The end time of the build task"
  steps:
    - name: build-and-push
      workingDir: $(workspaces.source.path)
      image: $(params.BUILDER_IMAGE)
      args:
        - $(params.EXTRA_ARGS)
        - --dockerfile=$(params.DOCKERFILE)
        - --context=$(workspaces.source.path)/$(params.CONTEXT) # The user does not need to care the workspace and the source.
        - --destination=$(params.IMAGE)
        - --digest-file=$(results.IMAGE_DIGEST.path)
      securityContext:
        runAsUser: 0
    - name: write-url
      image: docker.io/library/bash:5.1.4@sha256:c523c636b722339f41b6a431b44588ab2f762c5de5ec3bd7964420ff982fb1d9
      script: |
        set -e
        export TZ=UTC-8
        image="$(params.IMAGE)"
        echo -n "${image}" | tee "$(results.IMAGE_URL.path)"
        TZ='Asia/Hong_Kong' echo -n "$(date '+Date: %Y-%m-%d Time: %H:%M:%S')" > $(results.image-build-end-time.path)

2.3 Task: Update Image Tag In Helm Chart

Use command kubectl apply -f task-update-image-tag.yaml to add task: task-update-image-tag

task-update-image-tag.yaml

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: update-image-tag
spec:
  workspaces:
    - name: source
    - name: ssh-directory
  params:
    - name: commit_id
      description: result of git commit
    - name: project_path
      type: string
      description: which deployment path to update
    - name: git_branch
      description: git branch to checkout (branch, tag, sha, ref…)
      type: string
      default: main
  results:
    - name: pipeline-end-time
  steps:
    - name: update-image-tag
      image: alpine/git
      script: |
        #!/usr/bin/env sh
        apk add --no-cache bash
        export TZ=UTC-8
        exec bash << 'EOF'
          #!/usr/bin/env bash
          set -eu
          if [[ -d $(workspaces.ssh-directory.path) ]]; then
            mkdir -p ~/.ssh
            cp $(workspaces.ssh-directory.path)/known_hosts ~/.ssh/known_hosts
            cp $(workspaces.ssh-directory.path)/id_rsa ~/.ssh/id_rsa
            chmod 700 ~/.ssh
            chmod 600 ~/.ssh/*
          fi

          # git init only required in network file system pvc, do not required in disk storage pvc, cause nfs deny git search its parent working tree
          # git init
          # '[email protected]:sunway910/cicd-demo.git' can be a param
          git clone [email protected]:sunway910/cicd-demo.git
          cd cicd-demo
          git checkout "$(echo $(params.git_branch) | rev | cut -d'/' -f1 | rev)"

          # cd https://github.com/sunway910/cicd-demo/cicd-demo-helm
          cd "$(project_path)"
          # update image tag to the latest one
          # attention: please make sure only one `tag` in values.yaml, otherwise, `sed` will update all of them
          sed -i "s|tag:.*|tag: $(params.commit_id)|" values.yaml || { echo "Failed to update values.yaml"; exit 1; }


          git config --global user.email "[email protected]"
          git config --global user.name "Tekton Bot"
          git add .
          echo "Commit changes: skip ci: update image tag to $(params.commit_id)"
          git commit -m "skip ci: update image tag to $(params.commit_id)" || { echo "Failed to commit changes"; exit 1; }
          git push || { echo "Failed to push changes"; exit 1; }
          TZ='Asia/Hong_Kong' echo -n "$(date '+Date: %Y-%m-%d Time: %H:%M:%S')" > $(results.pipeline-end-time.path)
        EOF
      securityContext:
        runAsUser: 0

2.4 Task: Send Notification (Optional)

The last task can be used to send notification to slack/email or something else, here is an example of sending notification to lark:

Please kindly check your IM Tool Docs to get the Webhook URL and Request Body Format.

Otherwise, you can just skip this task and delete it in your pipeline finally task.

send-notification.yaml

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: send-notification-to-lark
  labels:
    app.kubernetes.io/version: "0.1"
  annotations:
    tekton.dev/pipelines.minVersion: "0.12.1"
    tekton.dev/displayName: "Send message to Lark Group"
    tekton.dev/categories: Messaging
    tekton.dev/tags: messaging
spec:
  description: >-
    These tasks post a simple message to a lark group.
    This task uses Incoming Webhooks of lark to send the message.
  params:
    - name: message_type
      type: string
      default: "interactive"
    - name: wide_screen_mode
      type: string
      default: "true"
    - name: pipeline_title
      type: string
      default: "Demo Pipeline"
    - name: pipeline_result
      type: string
      description: The result of the pipeline, can be "Succeeded" or "Failed"
      default: "Succeeded"
    - name: commit_info
      type: string
      default: "Author: Tekton Bot"
    - name: pipeline_result_link
      type: string
      default: "https://tekton-dashboard.sunway.run/#/pipelineruns"
    - name: github_commit_link
      type: string
      default: "https://github.com/sunway910/cicd-demo"
    - name: mention_user_open_id
      type: string
      default: "all"
    - name: pipeline_start_time
      type: string
      default: "2024-01-01 00:00:00"
    - name: pipeline_end_time
      type: string
      default: "2024-01-01 01:00:00"
    - name: webhook_url
      type: string
      default: ""
  steps:
    - name: lark-webhook
      image: docker.io/curlimages/curl:7.70.0@sha256:031df77a11e5edded840bc761a845eab6e3c2edee22669fb8ad6d59484b6a1c4 #tag: 7.70.0
      script: |
        #!/bin/sh
        set -e

        # if params.pipeline_result is "Succeeded", then use set content color to green, else set it to red
        # if params.pipeline_result is "Succeeded", then set pipeline_result_content to "The pipeline has completed successfully", else set it to "The pipeline has failed"
        # The color of the lark webhook card, can be red, orange, yellow, green, blue, purple, gray
        echo "pipeline_result: $(params.pipeline_result)"
        PIPELINE_RESULT="$(params.pipeline_result)"
        if [ "$PIPELINE_RESULT" = "Succeeded" ]; then
          pipeline_result_content="The pipeline has completed successfully"
          message_color="green"
        else
          pipeline_result_content="The pipeline has failed"
          message_color="red"
        fi
        echo "Send to webhook_url: $(params.webhook_url)"

        message_type="$(params.message_type)"
        wide_screen_mode="$(params.wide_screen_mode)"
        pipeline_title="$(params.pipeline_title)"
        pipeline_start_time="$(params.pipeline_start_time)"
        pipeline_end_time="$(params.pipeline_end_time)"
        mention_user_open_id="$(params.mention_user_open_id)"
        commit_info=$(echo -n "$(params.commit_info)" | awk 1 ORS='\\n')
        pipeline_result_link="$(params.pipeline_result_link)"
        github_commit_link="$(params.github_commit_link)"
        webhook_url="$(params.webhook_url)"

        json_data=$(cat <<EOF
          {
            "msg_type": "${message_type}",
            "card": {
              "config": {
                "wide_screen_mode": ${wide_screen_mode}
              },
              "header": {
                "title": {
                  "content": "${pipeline_title}",
                  "tag": "plain_text"
                },
                "template": "${message_color}"
              },
              "elements": [
                {
                  "tag": "div",
                  "fields": [
                    {
                      "is_short": true,
                      "text": {
                        "content": "**Start Time:**\n${pipeline_start_time}",
                        "tag": "lark_md"
                      }
                    },
                    {
                      "is_short": true,
                      "text": {
                        "content": "**End Time:**\n${pipeline_end_time}",
                        "tag": "lark_md"
                      }
                    }
                  ]
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "<at id=${mention_user_open_id}></at>${pipeline_result_content}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Commit Info:**\n ${commit_info}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "action",
                  "actions": [
                    {
                      "tag": "button",
                      "text": {
                        "content": "Pipeline CI Details",
                        "tag": "plain_text"
                      },
                      "url": "${pipeline_result_link}",
                      "type": "default"
                    }
                  ]
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "action",
                  "actions": [
                    {
                      "tag": "button",
                      "text": {
                        "content": "Github Commit Details",
                        "tag": "plain_text"
                      },
                      "url": "${github_commit_link}",
                      "type": "default"
                    }
                  ]
                }
              ]
            }
          }
        EOF
        )
        curl -X POST -H "Content-Type: application/json" -d "${json_data}" "$webhook_url"

2.5 Create Pipeline

Use command kubectl apply -f pipeline.yaml to create pipeline: cicd-demo-pipeline

customized pipeline.yaml file as below:

  • tasks fetch-from-git: params Dockerfile value is $(workspaces.source.path)/Dockerfile, modify the path if your Dockerfile is not in the root of your git repo.
  • tasks update-image-tag: params project_path value is cicd-demo-helm, modify the path if your helm/kustomize directory path.

pipeline.yaml

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: cicd-demo-pipeline
spec:
  params:
    # clone from which repo and revision
    - name: repo_url
      type: string
    - name: repo_revision
      type: string
    - name: git_branch
      type: string
    # push image to which registry and repository
    - name: image-registry
      default: docker.io/sunway
    - name: image-repo-name
      type: string
  workspaces:
    - name: git-source
    - name: docker-cred
    - name: github-cred
  tasks:
    - name: fetch-from-git
      taskRef:
        name: git-clone
      params:
        - name: repo_url
          value: $(params.repo_url)
        - name: revision
          value: $(params.repo_revision)
        - name: deleteExisting
          value: "true"
      workspaces:
        - name: output
          workspace: git-source
        - name: ssh-directory
          workspace: github-cred
    - name: build-image
      runAfter: [ fetch-from-git ]
      taskRef:
        name: kaniko-build-and-push
      params:
        - name: IMAGE
          value: $(params.image-registry)/$(params.image-repo-name):$(tasks.fetch-from-git.results.commit)
        - name: CONTEXT
          value: ""
        - name: DOCKERFILE
          value: $(workspaces.source.path)/Dockerfile
      workspaces:
        - name: source
          workspace: git-source
        - name: dockerconfig
          workspace: docker-cred
    - name: update-image-tag
      runAfter: [ build-image ]
      taskRef:
        name: update-image-tag
      params:
        - name: git_branch
          value: $(params.git_branch)
        - name: commit_id
          value: $(tasks.fetch-from-git.results.commit)
        # cd https://github.com/sunway910/cicd-demo/$project_path
        - name: project_path
          value: cicd-demo-helm
      workspaces:
        - name: source
          workspace: git-source
        - name: ssh-directory
          workspace: github-cred
  # delete finally task if you dont need to send notification to IM
  # finally task will not be executed if pipeline_start_time/pipeline_end_time is null
  finally:
    - name: send-notification-to-lark
      taskRef:
        name: send-notification-to-lark
      params:
        - name: webhook_url
          value: "https://open.larksuite.com/open-apis/bot/v2/hook/xxxxx"
        - name: pipeline_title
          value: "Demo Pipeline"
        - name: pipeline_result
          value: $(tasks.status)
        - name: commit_info
          value: $(tasks.fetch-from-git.results.commit-info)
        - name: pipeline_result_link
          value: "https://tekton-dashboard.sunway.run/#/pipelineruns"
        - name: github_commit_link
          value: https://github.com/sunway910/cicd-demo/commit/$(tasks.fetch-from-git.results.commit)
        - name: mention_user_open_id
          value: "all"
        - name: pipeline_start_time
          value: $(tasks.fetch-from-git.results.pipeline-start-time)
        - name: pipeline_end_time
          value: $(tasks.update-image-tag.results.pipeline-end-time)

2.6 Schedule Pipeline (Optional)

If you do not want pipeline affects other resources in kubernetes cluster, you can set pipeline run at a specific kubernetes node with label: pipeline=true:

kubectl label nodes <node-name> pipeline=true
kubectl get cm config-defaults -n tekton-pipelines -o yaml > /tmp/config-defaults-bk.yaml

All of pipelines configuration will extend from configmap config-defaults and then be scheduled on the node with label pipeline=true

config-default.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/instance: default
    app.kubernetes.io/part-of: tekton-pipelines
  name: config-defaults
  namespace: tekton-pipelines
data:
  _example: |
    # default-timeout-minutes contains the default number of
    # minutes to use for TaskRun and PipelineRun, if none is specified.
    default-timeout-minutes: "60"  # 60 minutes

    # default-service-account contains the default service account name
    # to use for TaskRun and PipelineRun, if none is specified.
    default-service-account: "default"

    # default-managed-by-label-value contains the default value given to the
    # "app.kubernetes.io/managed-by" label applied to all Pods created for
    # TaskRuns. If a user's requested TaskRun specifies another value for this
    # label, the user's request supercedes.
    default-managed-by-label-value: "tekton-pipelines"

    # default-pod-template contains the default pod template to use for
    # TaskRun and PipelineRun. If a pod template is specified on the
    # PipelineRun, the default-pod-template is merged with that one.
    default-pod-template: |
      nodeSelector:
        pipeline: "true"
    # default-affinity-assistant-pod-template contains the default pod template
    # to use for affinity assistant pods. If a pod template is specified on the
    # PipelineRun, the default-affinity-assistant-pod-template is merged with
    # that one.
    # default-affinity-assistant-pod-template:

    # default-max-matrix-combinations-count contains the default maximum number
    # of combinations from a Matrix, if none is specified.
    default-max-matrix-combinations-count: "256"

2.7 Create TriggerTemplate

Use command kubectl apply -f triggertemplate.yaml to create triggertemplate: cicd-demo-github-template

customized triggertemplate.yaml file as below, task update-image-tag will push image to docker.io/sunway/cicd-demo:$(params.commit_id):

  • image-registry: docker.io/sunway
  • image-repo-name: cicd-demo

triggerTemplate.yaml

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
  name: cicd-demo-github-template
spec:
  params:
    - name: gitrevision
    - name: gitrepositoryurl
    - name: gitbranch
  resourceTemplates:
    - apiVersion: tekton.dev/v1
      kind: PipelineRun
      metadata:
        generateName: tekton-sunway-cicd-demo-
      spec:
        pipelineRef:
          name: cicd-demo-pipeline
        taskRunTemplate:
          serviceAccountName: pipeline-account
        workspaces:
          - name: git-source
            volumeClaimTemplate:
              spec:
                accessModes:
                  - ReadWriteOnce
                resources:
                  requests:
                    storage: 10Gi
                #storageClassName: csi
          - name: docker-cred
            secret:
              defaultMode: 420
              items:
                - key: .dockerconfigjson
                  path: config.json
              secretName: docker-cred
          - name: github-cred
            secret:
              defaultMode: 420
              secretName: github-cred
        params:
          - name: repo_url
            value: $(tt.params.gitrepositoryurl)
          - name: repo_revision
            value: $(tt.params.gitrevision)
          - name: git_branch
            value: $(tt.params.gitbranch)
          - name: image-registry
            value: docker.io/sunway
          - name: image-repo-name
            value: cicd-demo

2.8 Create TriggerBinding

Use command kubectl apply -f triggerbinding.yaml to create triggerbinding: github-push-binding

triggerbinding.yaml

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
  name: github-push-binding
spec:
  params:
    - name: gitrevision
      value: $(body.head_commit.id)
    - name: gitrepositoryurl
      value: $(body.repository.ssh_url)
    - name: gitbranch
      value: $(body.ref)

2.9 Create EventListener

Use command kubectl apply -f eventlistener.yaml to create eventlistener: cicd-demo-github-listener

Trigger Condition:

  • 1: repo name is equal to cicd-demo
  • 2: commit message does not contain skip ci.
  • 3: push/pull_request on main/master branch

eventlistener.yaml

apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: cicd-demo-github-listener
spec:
  serviceAccountName: tekton-triggers-github-sa
  triggers:
    - name: cicd-demo-github-listener
      interceptors:
        - ref:
            name: "github"
          params:
            - name: "secretRef"
              value:
                secretName: github-triggers-secret
                secretKey: secretToken
            - name: "eventTypes"
              value: [ "push", "pull_request" ]
        - ref:
            # Common Expression Language
            name: "cel"
          params:
            - name: "filter"
              # https://github.com/sunway910/cicd-demo
              value: "body.repository.name == 'cicd-demo' && !body.head_commit.message.contains('skip ci') && (body.ref == 'refs/heads/main' || body.ref == 'refs/heads/master')"
      bindings:
        - ref: github-push-binding
      template:
        ref: cicd-demo-github-template

2.10 Create ArgoCD Notification (Optional)

use kubectl apply -f ... to update configmap: argocd-notifications-cm

then: kubectl rollout restart deployment argocd-notifications-controller -n argocd

argocd-notifications-cm.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  service.webhook.team-a-hook: |
    url: "https://open.larksuite.com/open-apis/bot/v2/hook/123456"
    headers:
      - name: Content-Type
        value: application/json
  service.webhook.team-b-hook: |
    url: "https://open.larksuite.com/open-apis/bot/v2/hook/654321"
    headers:
      - name: Content-Type
        value: application/json
  context: |
    argocdUrl: https://argocd.sunway.run
  trigger.on-deployed: | # application is healthy after being synced
    - description: Application is synced and healthy. Triggered once per commit.
      send: [app-status-change]
      when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'
  trigger.on-sync-running: | # start syncing
    - description: Application is being synced
      send: [app-status-change]
      when: app.status.operationState.phase in ['Running']
  trigger.on-sync-succeeded: | # syncing has succeeded
    - description: Application syncing has succeeded
      send: [app-status-change]
      when: app.status.operationState.phase in ['Succeeded']
  trigger.on-health-degraded: |
    - description: Application has degraded
      send: [app-degraded]
      when: app.status.health.status == 'Degraded'
  trigger.on-sync-failed: |
    - description: Application syncing has failed
      send: [app-degraded]
      when: app.status.operationState.phase in ['Error', 'Failed']
  trigger.on-sync-status-unknown: |
    - description: Application status is 'Unknown'
      send: [app-degraded]
      when: app.status.sync.status == 'Unknown'
  template.app-status-change: |
    webhook:
      team-a-hook:
        method: POST
        body: |
          {
            "msg_type": "interactive",
            "card": {
              "config": {
                "wide_screen_mode": true
              },
              "header": {
                "title": {
                  "content": "Prod Application Status Changed",
                  "tag": "plain_text"
                },
                "template": "green"
              },
              "elements": [
                {
                  "tag": "div",
                  "text": {
                    "content": "<at id=all></at>",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Application Name:** {{.app.metadata.name}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Sync Status:** {{.app.status.operationState.phase}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Application Status:** {{.app.status.health.status}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Time:** {{.app.status.operationState.startedAt}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "action",
                  "actions": [
                    {
                      "tag": "button",
                      "text": {
                        "content": "Prod ArgoCD Deployment Details",
                        "tag": "plain_text"
                      },
                      "url": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
                      "type": "default"
                    }
                  ]
                }
              ]
            }
          }
      team-b-hook:
        method: POST
        body: |
          {
            "msg_type": "interactive",
            "card": {
              "config": {
                "wide_screen_mode": true
              },
              "header": {
                "title": {
                  "content": "Prod Application Status Changed",
                  "tag": "plain_text"
                },
                "template": "green"
              },
              "elements": [
                {
                  "tag": "div",
                  "text": {
                    "content": "<at id=all></at>",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Application Name:** {{.app.metadata.name}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Sync Status:** {{.app.status.operationState.phase}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Application Status:** {{.app.status.health.status}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Time:** {{.app.status.operationState.startedAt}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "action",
                  "actions": [
                    {
                      "tag": "button",
                      "text": {
                        "content": "Prod ArgoCD Deployment Details",
                        "tag": "plain_text"
                      },
                      "url": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
                      "type": "default"
                    }
                  ]
                }
              ]
            }
          }
  template.app-degraded: |
    webhook:
      team-a-hook:
        method: POST
        body: |
          {
            "msg_type": "interactive",
            "card": {
              "config": {
                "wide_screen_mode": true
              },
              "header": {
                "title": {
                  "content": "Alert Prod Application Degraded",
                  "tag": "plain_text"
                },
                "template": "red"
              },
              "elements": [
                {
                  "tag": "div",
                  "text": {
                    "content": "<at id=all></at>",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Application Name:** {{.app.metadata.name}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Sync Status:** {{.app.status.operationState.phase}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Application Status:** {{.app.status.health.status}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Time:** {{.app.status.operationState.startedAt}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "action",
                  "actions": [
                    {
                      "tag": "button",
                      "text": {
                        "content": "Prod ArgoCD Deployment Details",
                        "tag": "plain_text"
                      },
                      "url": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
                      "type": "default"
                    }
                  ]
                }
              ]
            }
          }
      team-b-hook:
        method: POST
        body: |
          {
            "msg_type": "interactive",
            "card": {
              "config": {
                "wide_screen_mode": true
              },
              "header": {
                "title": {
                  "content": "Alert Prod Application Degraded",
                  "tag": "plain_text"
                },
                "template": "red"
              },
              "elements": [
                {
                  "tag": "div",
                  "text": {
                    "content": "<at id=all></at>",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Application Name:** {{.app.metadata.name}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Sync Status:** {{.app.status.operationState.phase}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Application Status:** {{.app.status.health.status}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "div",
                  "text": {
                    "content": "**Time:** {{.app.status.operationState.startedAt}}",
                    "tag": "lark_md"
                  }
                },
                {
                  "tag": "hr"
                },
                {
                  "tag": "action",
                  "actions": [
                    {
                      "tag": "button",
                      "text": {
                        "content": "Prod ArgoCD Deployment Details",
                        "tag": "plain_text"
                      },
                      "url": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
                      "type": "default"
                    }
                  ]
                }
              ]
            }
          }
ArgoNotificationSubscription

3 Create Ingress

3.1 Create EventListener Ingress

GitHub Repo Webhook will send request to this ingress: git-listener-cicd-demo.sunway.run

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: pipeline-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: git-listener-cicd-demo.sunway.run
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: cicd-demo-github-listener
                port:
                  number: 8080

3.2 Create Tekton Dashboard Ingress

Access to https://tekton-dashboard.sunway.run and watch the tasks progress in pipelineRun after push/pull_request to GitHub repo...

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tekton-dashboard-ingress
  namespace: tekton-pipelines
spec:
  ingressClassName: nginx
  rules:
    - host: tekton-dashboard.sunway.run
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: tekton-dashboard
                port:
                  number: 9097

Optional: How to protect your tekton-dashboard in public network?

3.2 Create ArgoCD Dashboard Ingress

Access to https://argocd.sunway.run with user:admin and password: kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-ingress
  namespace: argocd
spec:
  ingressClassName: nginx
  rules:
    - host: argocd.sunway.run
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  # set port to 443 if you do not disable tls in argocd-server in 1.4 Secret Configuration
                  number: 80

4 ArgoCD Configuration

4.1 Add Others Kubernetes Clusters

Add Clusters which your want to deploy, so that you can deploy service on multiple kubernetes cluster ArgocdAddCluster
  • Step 1: Login to Argo CD Make sure that you have updated the cluster details to the kubeconfig file and logged into Argo CD using Argo CD CLI, if not run the following command to log in to Argo CD.
argocd login https://argocd.sunway.run --username admin --password axDaGIswqYaaHxBs
  • Step 2: Get the Context of the Cluster

Once you have logged in to Argo CD, you need to find the context of the cluster you need to add to Argo CD.

Run the following command to get the context from the kubeconfig file

# execute this on others kubernetes cluster and then get context name
kubectl config get-contexts -o name
  • Step 3: Add the Cluster To add the cluster to Argo CD, use the context of the running cluster you got from the previous step on the below command
# If you set proxy like cloudflare before others kubernetes api-server, you have to trust the proxy server certificate. Otherwise, you shouldn't set up a proxy on your api-server
argocd cluster add --kubeconfig <path-of-kubeconfig-file> --kube-context string <cluster-context> --name <cluster-name>

This command will create a service account Argo CD-manager on the cluster you specify in the above command with full cluster privileges, so make sure you have the required permissions on the cluster.

4.2 Create A Git Repository

connect to a private repo via ssh as below

ArgocdNewRepo

4.3 Create An Application as below

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cicd-demo
spec:
  destination:
    name: ''
    namespace: app
    server: https://kubernetes.default.svc
  source:
    path: cicd-demo-helm
    repoURL: https://github.com/sunway910/cicd-demo
    targetRevision: main
  sources: []
  project: default
  syncPolicy:
    automated:
      prune: false
      selfHeal: false
ArgoCDCreateApp1 ArgoCDCreateApp2

5 Github Webhook

Set github webhook to tekton eventListener as below

Payload URL: https://git-listener-cicd-demo.sunway.run/github
Content type: application/json
Secret: sunway123
TektonGitWebhook

6 Testing

You can see your application after you create your own application at 4.3

Application indicate processing cause argocd cant check the ingress health, it is a normal status if you use nginx ingress or something else. [ ref1, ref2 ]

ArgocdAppProcessing

After you check application is healthy, you can push code to your GitHub repo and then check pipelineRun on Tekton Dashboard.

ArgoCD will auto sync the image tag changes in values.yaml when pipelineRun done.

Argocd will check changes every 180 seconds, but you can also click sync manually to sync immediately.