├── Dockerfile ├── Jenkins ├── DEV2PRDPipeline.png ├── Dockerfile ├── Jenkins_helm_pipeline.png └── README.md ├── Jenkinsfile ├── README.md ├── charts └── newegg-nginx │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ └── service.yaml │ └── values.yaml ├── config.json └── index.html /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:centos7 2 | MAINTAINER judexzhu 3 | 4 | RUN yum -y update \ 5 | && yum clean all \ 6 | && yum install -y epel-release \ 7 | && yum install -y nginx iproute 8 | 9 | EXPOSE 80 10 | COPY index.html /usr/share/nginx/html/ 11 | CMD ["nginx", "-g", "daemon off;"] 12 | -------------------------------------------------------------------------------- /Jenkins/DEV2PRDPipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judexzhu/Jenkins-Pipeline-CI-CD-with-Helm-on-Kubernetes/b7ce91ab0aae9efe0d406c9020faf53bf65f7549/Jenkins/DEV2PRDPipeline.png -------------------------------------------------------------------------------- /Jenkins/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jenkins:latest 2 | 3 | USER root 4 | RUN apt-get update \ 5 | && apt-get install -y sudo apt-transport-https software-properties-common \ 6 | && curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add - \ 7 | && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"\ 8 | && apt-get update -y \ 9 | && apt-get install docker-ce -y \ 10 | && rm -rf /var/lib/apt/lists/* 11 | RUN echo "jenkins ALL=NOPASSWD: ALL" >> /etc/sudoers 12 | -------------------------------------------------------------------------------- /Jenkins/Jenkins_helm_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/judexzhu/Jenkins-Pipeline-CI-CD-with-Helm-on-Kubernetes/b7ce91ab0aae9efe0d406c9020faf53bf65f7549/Jenkins/Jenkins_helm_pipeline.png -------------------------------------------------------------------------------- /Jenkins/README.md: -------------------------------------------------------------------------------- 1 | Docker run : 2 | ```bash 3 | docker run -p 8080:8080 -p 50000:50000 -d --name jenkins -v /var/run/docker.sock:/var/run/docker.sock -v /etc/sysconfig/docker:/etc/sysconfig/docker -v ~/jenkins_home:/var/jenkins_home -v /usr/local/bin/helm:/usr/local/bin/helm -v ~/.kube:/root/.kube -v ~/.helm:/hlm -e HELM_HOME=/hlm --privileged myjenkins 4 | ``` 5 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | def kubectlTest() { 4 | // Test that kubectl can correctly communication with the Kubernetes API 5 | echo "running kubectl test" 6 | sh "kubectl get nodes" 7 | 8 | } 9 | 10 | def helmLint(String chart_dir) { 11 | // lint helm chart 12 | sh "/usr/local/bin/helm lint ${chart_dir}" 13 | 14 | } 15 | 16 | def helmDeploy(Map args) { 17 | //configure helm client and confirm tiller process is installed 18 | 19 | if (args.dry_run) { 20 | println "Running dry-run deployment" 21 | 22 | sh "/usr/local/bin/helm upgrade --dry-run --debug --install ${args.name} ${args.chart_dir} --set ImageTag=${args.tag},Replicas=${args.replicas},Cpu=${args.cpu},Memory=${args.memory},DomainName=${args.name} --namespace=${args.name}" 23 | } else { 24 | println "Running deployment" 25 | sh "/usr/local/bin/helm upgrade --install ${args.name} ${args.chart_dir} --set ImageTag=${args.tag},Replicas=${args.replicas},Cpu=${args.cpu},Memory=${args.memory},DomainName=${args.name} --namespace=${args.name}" 26 | 27 | echo "Application ${args.name} successfully deployed. Use helm status ${args.name} to check" 28 | } 29 | } 30 | 31 | 32 | 33 | 34 | node { 35 | 36 | // Setup the Docker Registry (Docker Hub) + Credentials 37 | registry_url = "https://index.docker.io/v1/" // Docker Hub 38 | docker_creds_id = "judexzhu-DockerHub" // name of the Jenkins Credentials ID 39 | build_tag = "1.0" // default tag to push for to the registry 40 | 41 | def pwd = pwd() 42 | def chart_dir = "${pwd}/charts/newegg-nginx" 43 | 44 | stage 'Checking out GitHub Repo' 45 | git url: 'https://github.com/judexzhu/Jenkins-Pipeline-CI-CD-with-Helm-on-Kubernetes.git' 46 | 47 | def inputFile = readFile('config.json') 48 | def config = new groovy.json.JsonSlurperClassic().parseText(inputFile) 49 | println "pipeline config ==> ${config}" 50 | 51 | stage 'Building Nginx Container for Docker Hub' 52 | docker.withRegistry("${registry_url}", "${docker_creds_id}") { 53 | 54 | // Set up the container to build 55 | maintainer_name = "judexzhu" 56 | container_name = "nginx-test" 57 | 58 | 59 | stage "Building" 60 | echo "Building Nginx with docker.build(${maintainer_name}/${container_name}:${build_tag})" 61 | container = docker.build("${maintainer_name}/${container_name}:${build_tag}", '.') 62 | try { 63 | 64 | // Start Testing 65 | stage "Running Nginx container" 66 | 67 | // Run the container with the env file, mounted volumes and the ports: 68 | docker.image("${maintainer_name}/${container_name}:${build_tag}").withRun("--name=${container_name} -p 80:80 ") { c -> 69 | 70 | // wait for the django server to be ready for testing 71 | // the 'waitUntil' block needs to return true to stop waiting 72 | // in the future this will be handy to specify waiting for a max interval: 73 | // https://issues.jenkins-ci.org/browse/JENKINS-29037 74 | // 75 | waitUntil { 76 | sh "ss -antup | grep 80 | grep LISTEN | wc -l | tr -d '\n' > /tmp/wait_results" 77 | wait_results = readFile '/tmp/wait_results' 78 | 79 | echo "Wait Results(${wait_results})" 80 | if ("${wait_results}" == "1") 81 | { 82 | echo "Nginx is listening on port 80" 83 | sh "rm -f /tmp/wait_results" 84 | return true 85 | } 86 | else 87 | { 88 | echo "Nginx is not listening on port 80 yet" 89 | return false 90 | } 91 | } // end of waitUntil 92 | 93 | // At this point Nginx is running 94 | echo "Docker Container is running" 95 | input 'You can Check the running Docker Container on docker builder server now! Click process to the next stage!!' 96 | // this pipeline is using 3 tests 97 | // by setting it to more than 3 you can test the error handling and see the pipeline Stage View error message 98 | MAX_TESTS = 3 99 | for (test_num = 0; test_num < MAX_TESTS; test_num++) { 100 | 101 | echo "Running Test(${test_num})" 102 | 103 | expected_results = 0 104 | if (test_num == 0 ) 105 | { 106 | // Test we can download the home page from the running django docker container 107 | sh "docker exec -t ${container_name} curl -s http://localhost | grep Welcome | wc -l | tr -d '\n' > /tmp/test_results" 108 | expected_results = 1 109 | } 110 | else if (test_num == 1) 111 | { 112 | // Test that port 80 is exposed 113 | echo "Exposed Docker Ports:" 114 | sh "docker inspect --format '{{ (.NetworkSettings.Ports) }}' ${container_name}" 115 | sh "docker inspect --format '{{ (.NetworkSettings.Ports) }}' ${container_name} | grep map | grep '80/tcp:' | wc -l | tr -d '\n' > /tmp/test_results" 116 | expected_results = 1 117 | } 118 | else if (test_num == 2) 119 | { 120 | // Test there's nothing established on the port since nginx is not running: 121 | sh "docker exec -t ${container_name} ss -apn | grep 80 | grep ESTABLISHED | wc -l | tr -d '\n' > /tmp/test_results" 122 | expected_results = 0 123 | } 124 | else 125 | { 126 | err_msg = "Missing Test(${test_num})" 127 | echo "ERROR: ${err_msg}" 128 | currentBuild.result = 'FAILURE' 129 | error "Failed to finish container testing with Message(${err_msg})" 130 | } 131 | 132 | // Now validate the results match the expected results 133 | stage "Test(${test_num}) - Validate Results" 134 | test_results = readFile '/tmp/test_results' 135 | echo "Test(${test_num}) Results($test_results) == Expected(${expected_results})" 136 | sh "if [ \"${test_results}\" != \"${expected_results}\" ]; then echo \" --------------------- Test(${test_num}) Failed--------------------\"; echo \" - Test(${test_num}) Failed\"; echo \" - Test(${test_num}) Failed\";exit 1; else echo \" - Test(${test_num}) Passed\"; exit 0; fi" 137 | echo "Done Running Test(${test_num})" 138 | 139 | // cleanup after the test run 140 | sh "rm -f /tmp/test_results" 141 | currentBuild.result = 'SUCCESS' 142 | } 143 | } 144 | 145 | } catch (Exception err) { 146 | err_msg = "Test had Exception(${err})" 147 | currentBuild.result = 'FAILURE' 148 | error "FAILED - Stopping build for Error(${err_msg})" 149 | } 150 | 151 | stage "Pushing" 152 | input 'Do you approve Pushing?' 153 | container.push() 154 | 155 | currentBuild.result = 'SUCCESS' 156 | 157 | } 158 | 159 | stage ('helm test') { 160 | 161 | // run helm chart linter 162 | helmLint(chart_dir) 163 | 164 | // run dry-run helm chart installation 165 | helmDeploy( 166 | dry_run : true, 167 | name : config.app.name, 168 | chart_dir : chart_dir, 169 | tag : build_tag, 170 | replicas : config.app.replicas, 171 | cpu : config.app.cpu, 172 | memory : config.app.memory 173 | ) 174 | 175 | } 176 | 177 | stage ('helm deploy') { 178 | 179 | // Deploy using Helm chart 180 | helmDeploy( 181 | dry_run : false, 182 | name : config.app.name, 183 | chart_dir : chart_dir, 184 | tag : build_tag, 185 | replicas : config.app.replicas, 186 | cpu : config.app.cpu, 187 | memory : config.app.memory 188 | ) 189 | 190 | } 191 | 192 | /////////////////////////////////////// 193 | // 194 | // Coming Soon Feature Enhancements 195 | // 196 | // 1. Add Docker Compose testing as a new Pipeline item that is initiated after this one for "Integration" testing 197 | // 2. Make sure to set the Pipeline's "Throttle builds" to 1 because the docker containers will collide on resources like ports and names 198 | // 3. Should be able to parallelize the docker.withRegistry() methods to ensure the container is running on the slave 199 | // 4. After the tests finish (and before they start), clean up container images to prevent stale docker image builds from affecting the current test run 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | What is CI/CD 2 | 3 | CI : Continuous integration 4 | 5 | CD: 6 | a. Continuous Delivery 7 | b. Continuous Deployment 8 | 9 | Preview 10 | 11 | 12 | 13 | 14 | In the last Kubernetes Videos I made, I mentioned about how to CI/CD in the Kubernetes , I recently just figured it out and this is how it works 15 | 16 | And as I said before , no MIS/IT need to directly access the K8S 17 | 18 |  19 | 20 | 21 | 22 | 23 | For this we need 24 | 25 | 1. Git repo( I used Github for convenience) 26 | 27 | 2. Jenkins Master Server 28 | 29 | 3. Docker runner 30 | 31 | 4. Docker repo(I used Dockerhub) 32 | 33 | 5. Helm 34 | 35 | 6. Kubernetes 36 | 37 | So I download the official Jenkins docker image , and custom it (add sudoer on Jenkins and install docker-ce on it , it’s a debian ) 38 | 39 | 40 | I need Jenkins to run the docker and Helm , but I need them to run on the host , not inside the container , so I used Docker out of Docker (DOOD) 41 | 42 | More reading : http://container-solutions.com/running-docker-in-jenkins-in-docker/ 43 | https://github.com/jpetazzo/dind?__hstc=137489263.675a44a4b91444112bda1dad12f882fa.1488914911985.1488914911985.1488919904442.2&__hssc=137489263.4.1488919904442&__hsfp=3543740620 44 | 45 | build the docker image in local with name myjenkins 46 | 47 | run the image with outside docker helm binary inside the container 48 | 49 | docker run -p 8080:8080 -p 50000:50000 -d --name jenkins -v /var/run/docker.sock:/var/run/docker.sock -v /etc/sysconfig/docker:/etc/sysconfig/docker -v ~/jenkins_home:/var/jenkins_home -v /usr/local/bin/helm:/usr/local/bin/helm -v ~/.kube:/root/.kube -v ~/.helm:/hlm -e HELM_HOME=/hlm --privileged myjenkins 50 | 51 | (it’s a really long cli, but it did the tricks ) 52 | 53 | No we have Jenkins right now , with IP:8080, we need config the Jenkins and install some plugins 54 | 55 | 56 | 57 | 58 | Install Puligns : Pipeline Suit, Github and Pipeline utility step 59 | 60 | 61 | 62 | 63 | 64 | 65 | What’s on the Git Hub 66 | 67 | 68 | How to create a pipeline job on Jenkins 69 | 70 | Create two credentials , one for github, one for dockerhub 71 | 72 | 73 | 74 | 75 | 76 | Create a pipeline 77 | 78 | Config only this 79 | 80 | 81 | 82 | 83 | How it works 84 | 85 | Before 86 | 1. Helm status 87 | 88 | 2. Docker hub 89 | 90 | 3. Kubernetes 91 | 92 | 4. Index.html version 93 | 94 | 5. Jenkins file build tag 95 | 96 | 97 | Running the pipeline job 98 | 99 | 100 | 101 | 102 | 103 | 104 | After 105 | 106 | We have the helm deploy and docker image on local 107 | 108 | 109 | 110 | Dockerhub 111 | 112 | 113 | On Kubernetes we have a new name space and new deployments 114 | 115 | 116 | 117 | And the ingress and website 118 | 119 | 120 | 121 | Deploy success 122 | 123 | Then how to continuous delivery/deployment 124 | 125 | Change the index.html and jenkinsfile buildtag from 1.0 to 2.0 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | And run the job again 134 | 135 | 136 | 137 | Helm status and docker images 138 | 139 | 140 | 141 | Dockerhub 142 | 143 | 144 | On kubernetes 145 | 146 | 147 | 148 | And the website changed 149 | 150 | 151 | 152 | Done 153 | 154 | Having a lot to show , but this is already too long , all the code is on my github ,feel free to ask me and review it online ,all ideas are appreciated 155 | 156 | Thanks 157 | -------------------------------------------------------------------------------- /charts/newegg-nginx/Chart.yaml: -------------------------------------------------------------------------------- 1 | name: newegg-nginx 2 | version: 1.0.0 3 | -------------------------------------------------------------------------------- /charts/newegg-nginx/README.md: -------------------------------------------------------------------------------- 1 | helm chart for CI/CD workflow with nignx 2 | -------------------------------------------------------------------------------- /charts/newegg-nginx/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "newegg-nginx.release_labels" }} 2 | app: {{ printf "%s-%s" .Release.Name .Chart.Name | trunc 63 }} 3 | version: {{ .Chart.Version }} 4 | release: {{ .Release.Name }} 5 | {{- end }} 6 | {{- define "newegg-nginx.full_name" -}} 7 | {{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 -}} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /charts/newegg-nginx/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "newegg-nginx.full_name" . }} 5 | labels: 6 | {{- include "newegg-nginx.release_labels" . | indent 4 }} 7 | spec: 8 | replicas: {{ default 1 .Values.Replicas }} 9 | template: 10 | metadata: 11 | labels: 12 | {{- include "newegg-nginx.release_labels" . | indent 8 }} 13 | spec: 14 | containers: 15 | - name: newegg-nginx 16 | image: "{{.Values.Image}}:{{.Values.ImageTag}}" 17 | imagePullPolicy: "{{.Values.ImagePullPolicy}}" 18 | ports: 19 | - containerPort: {{.Values.ContainerPort}} 20 | protocol: TCP 21 | resources: 22 | requests: 23 | cpu: "{{.Values.Cpu}}" 24 | memory: "{{.Values.Memory}}" 25 | -------------------------------------------------------------------------------- /charts/newegg-nginx/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: {{ template "newegg-nginx.full_name" . }} 5 | labels: 6 | {{- include "newegg-nginx.release_labels" . | indent 4 }} 7 | spec: 8 | rules: 9 | - host: {{.Values.DomainName}}.buyabs.corp 10 | http: 11 | paths: 12 | - path: / 13 | backend: 14 | serviceName: {{ template "newegg-nginx.full_name" . }} 15 | servicePort: 80 16 | -------------------------------------------------------------------------------- /charts/newegg-nginx/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "newegg-nginx.full_name" . }} 5 | labels: 6 | {{- include "newegg-nginx.release_labels" . | indent 4 }} 7 | spec: 8 | type: {{ default "ClusterIP" .Values.ServiceType | quote }} 9 | ports: 10 | - port: {{.Values.ServicePort}} 11 | targetPort: {{.Values.ContainerPort}} 12 | protocol: TCP 13 | selector: 14 | app: {{ template "newegg-nginx.full_name" . }} 15 | -------------------------------------------------------------------------------- /charts/newegg-nginx/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values 2 | # This is a YAML-formatted file. 3 | # Declare name/value pairs to be passed into your templates. 4 | # name: value 5 | 6 | ServiceType: NodePort 7 | Image: judexzhu/nginx-test 8 | Imagetag: 'latest' 9 | Replicas: 3 10 | ImagePullPolicy: "Always" 11 | Cpu: "10m" 12 | Memory: "128Mi" 13 | ContainerPort: 80 14 | ServicePort: 80 15 | DomainName: newegg-nginx 16 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "newegg-nginx", 4 | "replicas": "3", 5 | "cpu": "10m", 6 | "memory": "128Mi" 7 | }, 8 | "pipeline": { 9 | "enabled": true, 10 | "library": { 11 | "branch": "master" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |