Implement CI/CD for images with Jenkins

Updated at:

This topic describes how to use Jenkins to implement an end-to-end automated pipeline that builds images from source code, pushes them to a repository, and deploys applications. When you commit source code in GitLab, Container Registry automatically builds an image, Container Service for Kubernetes (ACK) pulls the image to deploy the application, and an event notification is sent to a DingTalk group.

Prerequisites

  • Git, GitLab, and Jenkins are installed.

    Note

    GitLab requires JDK 8. Some plugins cannot run on JDK 11.

  • An Alibaba Cloud Container Registry (ACR) Enterprise Edition instance is created and public access is enabled. For more information, see Create an Enterprise Edition instance and Configure access over the Internet.

  • A Container Service for Kubernetes (ACK) cluster is created in the same region as the ACR instance. For more information, see Create a managed Kubernetes cluster.

  • You have created a DingTalk robot and recorded its webhook URL and secret token. For more information, see Step 1: Create a DingTalk robot.

  • To use the delivery chain feature of ACR, you must upgrade your instance to ACR Advanced Edition. For more information, see Billing.

Implement image CI with Jenkins

When you commit source code to GitLab, Container Registry automatically builds an image from the source code. You can then perform a security scan on the image. After the scan is complete, you automatically receive an event notification in your DingTalk group.

  1. Create a project in GitLab.

    1. Log on to GitLab.

    2. In the top navigation bar, choose Projects > Your projects.

    3. On the Projects page, click New Project in the upper-right corner and then click Create blank project.

    4. On the Create blank project page, set Project name, Project URL, and Project slug. Set Visibility Level to Private and then click Create project.

      In this example, set both Project name and Project slug to java-web. For Project URL, select shoppingmall as the namespace.

    5. On your local machine, create a Dockerfile, pom.xml file, DemoApplication.java file, and HelloController.java file with the following content.

      • Dockerfile

        FROM registry.cn-hangzhou.aliyuncs.com/public-toolbox/maven:3.8.3-openjdk-8-aliyun AS build
        COPY src /home/app/src
        COPY pom.xml /home/app
        RUN ["/usr/local/bin/mvn-entrypoint.sh","mvn","-f","/home/app/pom.xml","clean","package","-Dmaven.test.skip=true"]
        FROM registry.cn-hangzhou.aliyuncs.com/public-toolbox/openjdk:8-jdk-alpine
        COPY --from=build /home/app/target/demo-0.0.1-SNAPSHOT.jar /usr/local/lib/demo-0.0.1-SNAPSHOT.jar
        EXPOSE 8080
        ENTRYPOINT ["java","-jar","/usr/local/lib/demo-0.0.1-SNAPSHOT.jar"]
      • pom.xml

        <?xml version="1.0" encoding="UTF-8"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
            <modelVersion>4.0.0</modelVersion>
            <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.6.1</version>
                <relativePath/> <!-- lookup parent from repository -->
            </parent>
            <groupId>com.example</groupId>
            <artifactId>demo</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <name>demo</name>
            <description>Demo project for Spring Boot</description>
            <properties>
                <java.version>1.8</java.version>
            </properties>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-freemarker</artifactId>
                </dependency>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <dependency>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                    <optional>true</optional>
                </dependency>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-test</artifactId>
                    <scope>test</scope>
                </dependency>
                <dependency>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-core</artifactId>
                    <version>2.13.2</version>
                </dependency>
            </dependencies>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                        <configuration>
                            <excludes>
                                <exclude>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </exclude>
                            </excludes>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </project>
      • DemoApplication.java

        package com.example.demo;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import java.util.TimeZone;
        @SpringBootApplication
        public class DemoApplication {
            public static void main(String[] args) {
                TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
                SpringApplication.run(DemoApplication.class, args);
            }
        }
      • HelloController.java

        package com.example.demo;
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.RestController;
        import javax.servlet.http.HttpServletRequest;
        import java.text.SimpleDateFormat;
        import java.util.Date;
        @RestController
        @Slf4j
        public class HelloController {
            @RequestMapping({"/hello", "/"})
            public String hello(HttpServletRequest request) {
                return "Hello World at " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
            }
        }
    6. Run the following commands to upload the build files to GitLab.

      cd java-web  # Enter the directory where the build files are located.
      git remote set-url origin http://8.218.20*.***/shoppingmall/java-web.git
      git push origin master
      * [new branch]      master -> master
  2. Configure a pipeline in Jenkins to build images.

    1. Configure the GitLab SSH key in Jenkins.

      1. Log on to Jenkins.

      2. In the left-side navigation pane, click Manage Jenkins.

      3. In the Security section, click Manage Credentials.

      4. In the Stores scoped to Jenkins section, click Jenkins in the Store column, and then click Global credentials.

      5. In the left-side navigation pane, click Add Credentials.

      6. Set Kind to SSH Username with private key, enter a Description and Username, select Enter directly, and then click OK.

        On the Global credentials page, Jenkins automatically generates a credential ID. Record the credential ID.

    2. Create a pipeline.

      1. In the left-side navigation pane, click New Item.

      2. Enter an item name, select Pipeline, and then click OK.

      3. Click the Build Triggers tab, select Build when a change is pushed to GitLab, and then select Push Events.

        Record the webhook URL displayed to the right of Build when a change is pushed to GitLab.

      4. Click Advanced, and then click Generate in the lower-right corner of the Secret token field.

        Jenkins generates a secret token. Record this token.

      5. Click the Pipeline tab, modify the following template based on your actual information, paste the content into the text box, and then click Save.

        def git_auth_id = "6d5a2c06-f0a7-43c8-9b79-37b8c266****"  # The credential ID.
        def git_branch_name = "master" # The branch name.
        def git_url = "git@172.16.1*.***:shoppingmall/java-web.git"   # The GitLab repository URL.
        def acr_url = "s*****-devsecops-registry.cn-hongkong.cr.aliyuncs.com"  # The image repository URL.
        def acr_username = "acr_test_*****@test.aliyunid.com"   # The Container Registry username.
        def acr_password = "HelloWorld2021"   # The Container Registry password.
        def acr_namespace = "ns"    # The namespace.
        def acr_repo_name = "test"   # The repository name.
        def tag_version = "0.0.1"   # The image tag.
        node {
            stage('checkout git repo') {
                checkout([$class: 'GitSCM', branches: [[name: "*/${git_branch_name}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth_id}", url: "${git_url}"]]])
            }
            stage('build image') {
                sh "sudo docker build -t java-web:${tag_version} ."
                sh "sudo docker tag java-web:${tag_version} ${acr_url}/${acr_namespace}/${acr_repo_name}:${tag_version}"
            }
            stage('push image') {
                sh "sudo docker login --username=${acr_username} --password=${acr_password} ${acr_url}"
                sh "sudo docker push ${acr_url}/${acr_namespace}/${acr_repo_name}:${tag_version}"
            }
        }
  3. Add the webhook URL to GitLab.

    1. Log on to GitLab.

    2. On the Projects page, click the project that you created.

    3. In the left-side navigation pane, choose Settings > Webhooks. Enter the webhook URL and secret token, deselect Enable SSL verification, and then click Add webhooks.

  4. Create an event notification.

    1. Log on to the Container Registry console.

    2. In the top navigation bar, select a region.

    3. In the left-side navigation pane, click Instances.

    4. On the Instances page, click the Enterprise Edition instance that you want to manage.

    5. In the left-side navigation pane of the instance details page, choose Instances > Event Notification.

    6. On the Event Rules tab, click Create Rule.

    7. In the Event Scope step, set Rule Name. Set Event Type to The image is scanned., select Scanned., set Effective Scope to Namespaces, select ns as the namespace, and then click Next.

    8. In the Event Notification step, set Notification Method to DingTalk, enter the webhook URL and secret token for DingTalk, and then click Save.

  5. Trigger an image build.

    Run the following commands to modify the HelloController.java file and commit the file to GitLab. This triggers an image build.

    vim java/com/example/demo/HelloController.java # Modify the HelloController.java file on your local machine based on your needs.
    git add . && git commit -m 'commit' && git push origin # Commit the file to GitLab.

    After a moment, on the details page of the Enterprise Edition instance, select Repository > Repositories in the left navigation bar. On the page that appears, click the target repository test. In the left navigation bar of the Repository page, click Tags. You can see that an image is generated on the Tags page.

  6. Configure a security scan.

    1. On the Tags page, find the target image tag and click Security Scan in the Actions column.

    2. On the Security Scan page, click Scan.

      After the security scan is complete, you will receive a notification in your DingTalk group.

Implement image CD with Jenkins

Building the image automatically triggers a delivery chain. Once finished, the delivery chain sends an HTTP request to Jenkins. This request triggers the Deployment in ACK to pull the latest image and redeploy the application.

  1. Create an application.

    1. Log on to the Container Service Management Console.

    2. In the left navigation pane, click Cluster.

    3. On the Cluster List page, click the name of the destination cluster or click Details in the Actions column.

    4. In the left navigation pane of the cluster management page, choose Workload > Deployments.

    5. On the Deployments page, select a Namespace and then click Create from YAML.

    6. In the Create from YAML panel, set Sample Template to Custom, paste the following content into the template editor, and then click Create.

      Note

      If you do not configure password-free image pulling, you must enable public access in Container Registry and set the repository to public. For more information, see Configure access over the Internet.

      apiVersion: apps/v1
      kind: Deployment
      metadata:
        creationTimestamp: null
        labels:
          app: demo
        name: demo
      spec:
        replicas: 3
        minReadySeconds: 5
        progressDeadlineSeconds: 60
        revisionHistoryLimit: 5
        selector:
          matchLabels:
            app: demo
        strategy:
          rollingUpdate:
            maxUnavailable: 1
          type: RollingUpdate
        template:
          metadata:
            annotations:
              prometheus.io/port: "9797"
              prometheus.io/scrape: "true"
            creationTimestamp: null
            labels:
              app: demo
          spec:
            containers:
            - image: s*****-devsecops-registry.cn-hongkong.cr.aliyuncs.com/ns/test:0.0.1 
              imagePullPolicy: Always
              name: demo
              ports:
              - containerPort: 8080
                name: http
                protocol: TCP
              readinessProbe:
                initialDelaySeconds: 5
                tcpSocket:
                  port: 8080
                timeoutSeconds: 5
              resources:
                limits:
                  cpu: "2"
                  memory: 512Mi
                requests:
                  cpu: 100m
                  memory: 64Mi
      status: {}
      ---
      apiVersion: v1
      kind: Service
      metadata:
        name: demo-svc
      spec:
        selector:
          app: demo
        ports:
          - protocol: TCP
            port: 80
            targetPort: 8080
      ---
      apiVersion: extensions/v1beta1
      kind: Ingress
      metadata:
        name: demo
        labels:
          app: demo
      spec:
        rules:
          - host: app.demo.example.com
            http:
              paths:
                - backend:
                    serviceName: demo-svc
                    servicePort: 80
      ---
                                      
    7. On the Deployments page, click the target application, demo, and then click the Access Method tab.

      On the Access Method tab, obtain the external endpoint.

    8. Add the following entry to your local hosts file.

      <External endpoint address> app.demo.example.com
    9. In a browser, enter app.demo.example.com in the address bar.

      Hello World 2021 xxx at 2021-12-06 20:51:36

      The page appears, indicating the application is deployed successfully.

  2. Create a trigger.

    1. On the Deployments page, click the name of the target application, demo.

    2. On the application details page, click the Triggers tab and then click Create Trigger.

    3. In the Create Trigger dialog box, set Action to Redeploy and click OK.

      On the Triggers tab, obtain the trigger URL.

  3. Create a pipeline.

    1. Log in to Jenkins.

    2. In the left-side navigation pane, click New Item.

    3. Enter an item name, select Pipeline, and then click OK.

    4. Click the Build Triggers tab and select Generic Webhook Trigger.

      Under Generic Webhook Trigger, the HTTP request trigger URL is in the JENKINS_URL/generic-webhook-trigger/invoke format. In this topic, Generic Webhook token is set to helloworld2021. Therefore, the webhook URL is JENKINS_URL/generic-webhook-trigger/invoke?token=helloworld2021.

    5. Click the Pipeline tab, modify the Generic Webhook token and trigger URL in the following template based on your actual information, paste the content into the text box, and then click Save.

      pipeline {
        agent any
        triggers {
          GenericTrigger(
           genericVariables: [
            [key: 'InstanceId', value: '$.data.InstanceId'],   
            [key: 'RepoNamespaceName', value: '$.data.RepoNamespaceName'],
            [key: 'RepoName', value: '$.data.RepoName'],
            [key: 'Tag', value: '$.data.Tag']
           ],
           causeString: 'Triggered on $ref',
           token: 'helloworld2021',   # The Generic Webhook token. Replace it with your actual token.
           tokenCredentialId: '',
           printContributedVariables: true,
           printPostContent: true,
           silentResponse: false,
           regexpFilterText: '$ref'
          )
        }
        stages {
          stage('Some step') {
            steps {
              sh "echo 'will print post content'"
              sh "echo $InstanceId"
              sh "echo $RepoNamespaceName"
              sh "echo $RepoName"
              sh "echo $Tag"
              sh "echo 'redeploy to ACK or you can deoloy to other platforms use before message'"
              sh "curl 'https://cs.console.aliyun.com/hook/trigger?token=g****'  # The trigger URL. Replace it with your actual URL.
              sh "echo 'done'"
            }
          }
        }
      }
  4. Create a delivery chain. For more information, see Create a delivery chain.

  5. Create an event rule.

    1. In the left-side navigation pane of the Enterprise Edition instance management page, choose Instances > Event Notification.

    2. On the Event Rules tab, click Create Rule.

    3. In the Event Scope step, set Rule Name. Set Event Type to The delivery chain is processed., select Success, set Effective Scope to Namespaces, select ns as the namespace, and then click Next.

    4. In the Event Notification step, set Notification Method to HTTP, enter the webhook URL that you obtained in Step 3, and then click Save.

  6. Run the following commands to modify the HelloController.java file and commit the code to GitLab. This triggers an image build.

    vim java/com/example/demo/HelloController.java # Modify the HelloController.java file on your local machine based on your needs.
    git add . && git commit -m 'add update' && git push origin   # Commit the file to GitLab.

    Building the image triggers the delivery chain. After the delivery chain is complete, it triggers the Deployment in ACK to pull the new image and redeploy the application.

  7. Run the following command to verify that the application is redeployed.

    curl app.demo.example.com

    The changed output indicates that the application was successfully redeployed.