├── .gitignore
├── helm
└── acme
│ ├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── service.yaml
│ └── deployment.yaml
│ ├── Chart.yaml
│ ├── .helmignore
│ └── values.yaml
├── src
├── images
│ ├── helm-logo.png
│ ├── jenkins-logo.png
│ └── kubernetes-logo.png
├── css
│ └── style.css
└── index.html
├── docker
└── Dockerfile
├── README.md
└── Jenkinsfile
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vagrant
3 | tmp
4 | logs
5 | build
6 |
--------------------------------------------------------------------------------
/helm/acme/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | You have successfully installed ACME!
2 |
--------------------------------------------------------------------------------
/helm/acme/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | description: A Helm chart for Kubernetes
3 | name: acme
4 | version: 0.1.0
5 |
--------------------------------------------------------------------------------
/src/images/helm-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eldada/jenkins-pipeline-kubernetes/HEAD/src/images/helm-logo.png
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.15.8
2 |
3 | LABEL Author "eldada@jfrog.com"
4 |
5 | COPY site /usr/share/nginx/html
6 |
--------------------------------------------------------------------------------
/src/images/jenkins-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eldada/jenkins-pipeline-kubernetes/HEAD/src/images/jenkins-logo.png
--------------------------------------------------------------------------------
/src/images/kubernetes-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eldada/jenkins-pipeline-kubernetes/HEAD/src/images/kubernetes-logo.png
--------------------------------------------------------------------------------
/src/css/style.css:
--------------------------------------------------------------------------------
1 | h2 {
2 | font-size: 200%;
3 | font-family: Helvetica;
4 | }
5 |
6 | * {
7 | font-size: 100%;
8 | font-family: Arial;
9 | }
10 |
--------------------------------------------------------------------------------
/helm/acme/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *~
18 | # Various IDEs
19 | .project
20 | .idea/
21 | *.tmproj
22 |
--------------------------------------------------------------------------------
/helm/acme/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for acme.
2 | # This is a YAML-formatted file.
3 | # Declare variables to be passed into your templates.
4 | replicaCount: 1
5 | imagePullSecrets:
6 | image:
7 | repository: acme
8 | tag: dev
9 | pullPolicy: Always
10 | service:
11 | type: ClusterIP
12 | externalPort: 80
13 | internalPort: 80
14 | resources:
15 | limits:
16 | cpu: 100m
17 | memory: 128Mi
18 | requests:
19 | cpu: 100m
20 | memory: 128Mi
21 |
--------------------------------------------------------------------------------
/helm/acme/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/* vim: set filetype=mustache: */}}
2 | {{/*
3 | Expand the name of the chart.
4 | */}}
5 | {{- define "acme.name" -}}
6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
7 | {{- end -}}
8 |
9 | {{/*
10 | Create a default fully qualified app name.
11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
12 | */}}
13 | {{- define "acme.fullname" -}}
14 | {{- $name := default .Chart.Name .Values.nameOverride -}}
15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
16 | {{- end -}}
17 |
--------------------------------------------------------------------------------
/helm/acme/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ template "acme.fullname" . }}
5 | labels:
6 | app: {{ template "acme.name" . }}
7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
8 | release: {{ .Release.Name }}
9 | heritage: {{ .Release.Service }}
10 | spec:
11 | type: {{ .Values.service.type }}
12 | ports:
13 | - port: {{ .Values.service.externalPort }}
14 | targetPort: {{ .Values.service.internalPort }}
15 | protocol: TCP
16 | name: {{ .Values.service.name }}
17 | selector:
18 | app: {{ template "acme.name" . }}
19 | release: {{ .Release.Name }}
20 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ACME
6 |
7 |
8 |
9 |
10 | ACME is up and running!
11 |
12 | Using the following tools:
13 |
14 |
15 |
16 |
17 |
ACME (eldada) version: __APP_VERSION__
18 |
19 |
20 |
--------------------------------------------------------------------------------
/helm/acme/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: extensions/v1beta1
2 | kind: Deployment
3 | metadata:
4 | name: {{ template "acme.fullname" . }}
5 | labels:
6 | app: {{ template "acme.name" . }}
7 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
8 | release: {{ .Release.Name }}
9 | heritage: {{ .Release.Service }}
10 | spec:
11 | replicas: {{ .Values.replicaCount }}
12 | template:
13 | metadata:
14 | labels:
15 | app: {{ template "acme.name" . }}
16 | release: {{ .Release.Name }}
17 | spec:
18 | {{- if .Values.imagePullSecrets }}
19 | imagePullSecrets:
20 | - name: {{ .Values.imagePullSecrets }}
21 | {{- end }}
22 | containers:
23 | - name: {{ .Chart.Name }}
24 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
25 | imagePullPolicy: {{ .Values.image.pullPolicy }}
26 | ports:
27 | - containerPort: {{ .Values.service.internalPort }}
28 | livenessProbe:
29 | httpGet:
30 | path: /
31 | port: {{ .Values.service.internalPort }}
32 | readinessProbe:
33 | httpGet:
34 | path: /
35 | port: {{ .Values.service.internalPort }}
36 | resources:
37 | {{ toYaml .Values.resources | indent 12 }}
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CI/CD to Kubernetes using Jenkins and Helm
2 | This project is an example of a complete CI/CD pipeline of a simple static web application from sources to deployed Kubernetes pods.
3 |
4 | ## Artifactory as Docker registry
5 | This project uses [Artifactory](https://jfrog.com/integration/artifactory-docker-registry/) as its Docker registry.
6 | You can [get a free trial](https://www.jfrog.com/artifactory/free-trial/) and try it out!
7 |
8 | Follow the documentation for setting up your Docker registry.
9 |
10 | ## Artifactory as Helm repository
11 | This project uses [Artifactory](https://jfrog.com/integration/helm/) as its Helm repository.
12 | You can [get a free trial](https://www.jfrog.com/artifactory/free-trial/) and try it out!
13 |
14 | Follow the documentation for setting up your Helm repository.
15 |
16 | ## Jenkins
17 | Setup a [Jenkins](https://jenkins.io/) running with
18 | - [Docker](https://www.docker.com/). Can build and push images
19 | - [Kubectl](https://kubernetes.io/). Kubernetes CLI that will link Jenkins with the Kubernetes cluster
20 | - [Helm](https://helm.sh/). Kubernetes package manager to simplify deployment of your Docker containers to Kubernetes
21 |
22 | There is a [GitHub example](https://github.com/eldada/jenkins-in-kubernetes) of such a Docker image, to be used in Kubernetes.
23 |
24 | ### Jenkins in Kubernetes
25 | Jenkins running in Kubernetes can be found in this [GitHub example](https://github.com/eldada/jenkins-in-kubernetes).
26 | - This project is used to build a Jenkins master that has the required tools (`docker`, `kubectl` and `helm`) already installed
27 | - You can deploy this Jenkins to Kubernetes using the helm chart in the same repository
28 | - Notice that this is an example, and should not be used for production
29 |
30 | ### General notes
31 | - The Jenkins pipeline example ([Jenkinsfile](Jenkinsfile)) is using calls to external cli tools such as `docker`, `kubectl`, `curl` and other shell commands.
32 | Some of these can be replaced with groovy code and functions or built in pipeline steps, but are implemented like this to demonstrate the simple use of these tools.
33 | - The `kubectl` and `helm` clients are **not** configured in this example. It's assumed the Jenkins instance is pre-configured, or run it in your Kubernetes cluster,
34 | where it picks up the local pod credentials to access the Kubernetes API.
35 |
36 | ## Build the web application
37 | You can build the web application directly by running `build.sh`. You can create the Docker image and run it locally. See the [build.sh](build.sh) options.
38 | ```bash
39 | # See options
40 | $ ./build.sh --help
41 | ```
42 |
43 | You can also pack and push the Docker image and Helm chart.
44 |
45 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | /*
2 | This is an example pipeline that implement full CI/CD for a simple static web site packed in a Docker image.
3 |
4 | The pipeline is made up of 6 main steps
5 | 1. Git clone and setup
6 | 2. Build and local tests
7 | 3. Publish Docker and Helm
8 | 4. Deploy to dev and test
9 | 5. Deploy to staging and test
10 | 6. Optionally deploy to production and test
11 | */
12 |
13 | /*
14 | Create the kubernetes namespace
15 | */
16 | def createNamespace (namespace) {
17 | echo "Creating namespace ${namespace} if needed"
18 |
19 | sh "[ ! -z \"\$(kubectl get ns ${namespace} -o name 2>/dev/null)\" ] || kubectl create ns ${namespace}"
20 | }
21 |
22 | /*
23 | Helm install
24 | */
25 | def helmInstall (namespace, release) {
26 | echo "Installing ${release} in ${namespace}"
27 |
28 | script {
29 | release = "${release}-${namespace}"
30 | sh "helm repo add helm ${HELM_REPO}; helm repo update"
31 | sh """
32 | helm upgrade --install --namespace ${namespace} ${release} \
33 | --set imagePullSecrets=${IMG_PULL_SECRET} \
34 | --set image.repository=${DOCKER_REG}/${IMAGE_NAME},image.tag=${DOCKER_TAG} helm/acme
35 | """
36 | sh "sleep 5"
37 | }
38 | }
39 |
40 | /*
41 | Helm delete (if exists)
42 | */
43 | def helmDelete (namespace, release) {
44 | echo "Deleting ${release} in ${namespace} if deployed"
45 |
46 | script {
47 | release = "${release}-${namespace}"
48 | sh "[ -z \"\$(helm ls --short ${release} 2>/dev/null)\" ] || helm delete --purge ${release}"
49 | }
50 | }
51 |
52 | /*
53 | Run a curl against a given url
54 | */
55 | def curlRun (url, out) {
56 | echo "Running curl on ${url}"
57 |
58 | script {
59 | if (out.equals('')) {
60 | out = 'http_code'
61 | }
62 | echo "Getting ${out}"
63 | def result = sh (
64 | returnStdout: true,
65 | script: "curl --output /dev/null --silent --connect-timeout 5 --max-time 5 --retry 5 --retry-delay 5 --retry-max-time 30 --write-out \"%{${out}}\" ${url}"
66 | )
67 | echo "Result (${out}): ${result}"
68 | }
69 | }
70 |
71 | /*
72 | Test with a simple curl and check we get 200 back
73 | */
74 | def curlTest (namespace, out) {
75 | echo "Running tests in ${namespace}"
76 |
77 | script {
78 | if (out.equals('')) {
79 | out = 'http_code'
80 | }
81 |
82 | // Get deployment's service IP
83 | def svc_ip = sh (
84 | returnStdout: true,
85 | script: "kubectl get svc -n ${namespace} | grep ${ID} | awk '{print \$3}'"
86 | )
87 |
88 | if (svc_ip.equals('')) {
89 | echo "ERROR: Getting service IP failed"
90 | sh 'exit 1'
91 | }
92 |
93 | echo "svc_ip is ${svc_ip}"
94 | url = 'http://' + svc_ip
95 |
96 | curlRun (url, out)
97 | }
98 | }
99 |
100 | /*
101 | This is the main pipeline section with the stages of the CI/CD
102 | */
103 | pipeline {
104 |
105 | options {
106 | // Build auto timeout
107 | timeout(time: 60, unit: 'MINUTES')
108 | }
109 |
110 | // Some global default variables
111 | environment {
112 | IMAGE_NAME = 'acme'
113 | TEST_LOCAL_PORT = 8817
114 | DEPLOY_PROD = false
115 | PARAMETERS_FILE = "${JENKINS_HOME}/parameters.groovy"
116 | }
117 |
118 | parameters {
119 | string (name: 'GIT_BRANCH', defaultValue: 'master', description: 'Git branch to build')
120 | booleanParam (name: 'DEPLOY_TO_PROD', defaultValue: false, description: 'If build and tests are good, proceed and deploy to production without manual approval')
121 |
122 |
123 | // The commented out parameters are for optionally using them in the pipeline.
124 | // In this example, the parameters are loaded from file ${JENKINS_HOME}/parameters.groovy later in the pipeline.
125 | // The ${JENKINS_HOME}/parameters.groovy can be a mounted secrets file in your Jenkins container.
126 | /*
127 | string (name: 'DOCKER_REG', defaultValue: 'docker-artifactory.my', description: 'Docker registry')
128 | string (name: 'DOCKER_TAG', defaultValue: 'dev', description: 'Docker tag')
129 | string (name: 'DOCKER_USR', defaultValue: 'admin', description: 'Your helm repository user')
130 | string (name: 'DOCKER_PSW', defaultValue: 'password', description: 'Your helm repository password')
131 | string (name: 'IMG_PULL_SECRET', defaultValue: 'docker-reg-secret', description: 'The Kubernetes secret for the Docker registry (imagePullSecrets)')
132 | string (name: 'HELM_REPO', defaultValue: 'https://artifactory.my/artifactory/helm', description: 'Your helm repository')
133 | string (name: 'HELM_USR', defaultValue: 'admin', description: 'Your helm repository user')
134 | string (name: 'HELM_PSW', defaultValue: 'password', description: 'Your helm repository password')
135 | */
136 | }
137 |
138 | // In this example, all is built and run from the master
139 | agent { node { label 'master' } }
140 |
141 | // Pipeline stages
142 | stages {
143 |
144 | ////////// Step 1 //////////
145 | stage('Git clone and setup') {
146 | steps {
147 | echo "Check out acme code"
148 | git branch: "master",
149 | credentialsId: 'eldada-bb',
150 | url: 'https://github.com/eldada/jenkins-pipeline-kubernetes.git'
151 |
152 | // Validate kubectl
153 | sh "kubectl cluster-info"
154 |
155 | // Init helm client
156 | sh "helm init"
157 |
158 | // Make sure parameters file exists
159 | script {
160 | if (! fileExists("${PARAMETERS_FILE}")) {
161 | echo "ERROR: ${PARAMETERS_FILE} is missing!"
162 | }
163 | }
164 |
165 | // Load Docker registry and Helm repository configurations from file
166 | load "${JENKINS_HOME}/parameters.groovy"
167 |
168 | echo "DOCKER_REG is ${DOCKER_REG}"
169 | echo "HELM_REPO is ${HELM_REPO}"
170 |
171 | // Define a unique name for the tests container and helm release
172 | script {
173 | branch = GIT_BRANCH.replaceAll('/', '-').replaceAll('\\*', '-')
174 | ID = "${IMAGE_NAME}-${DOCKER_TAG}-${branch}"
175 |
176 | echo "Global ID set to ${ID}"
177 | }
178 | }
179 | }
180 |
181 | ////////// Step 2 //////////
182 | stage('Build and tests') {
183 | steps {
184 | echo "Building application and Docker image"
185 | sh "${WORKSPACE}/build.sh --build --registry ${DOCKER_REG} --tag ${DOCKER_TAG} --docker_usr ${DOCKER_USR} --docker_psw ${DOCKER_PSW}"
186 |
187 | echo "Running tests"
188 |
189 | // Kill container in case there is a leftover
190 | sh "[ -z \"\$(docker ps -a | grep ${ID} 2>/dev/null)\" ] || docker rm -f ${ID}"
191 |
192 | echo "Starting ${IMAGE_NAME} container"
193 | sh "docker run --detach --name ${ID} --rm --publish ${TEST_LOCAL_PORT}:80 ${DOCKER_REG}/${IMAGE_NAME}:${DOCKER_TAG}"
194 |
195 | script {
196 | host_ip = sh(returnStdout: true, script: '/sbin/ip route | awk \'/default/ { print $3 ":${TEST_LOCAL_PORT}" }\'')
197 | }
198 | }
199 | }
200 |
201 | // Run the 3 tests on the currently running ACME Docker container
202 | stage('Local tests') {
203 | parallel {
204 | stage('Curl http_code') {
205 | steps {
206 | curlRun ("http://${host_ip}", 'http_code')
207 | }
208 | }
209 | stage('Curl total_time') {
210 | steps {
211 | curlRun ("http://${host_ip}", 'total_time')
212 | }
213 | }
214 | stage('Curl size_download') {
215 | steps {
216 | curlRun ("http://${host_ip}", 'size_download')
217 | }
218 | }
219 | }
220 | }
221 |
222 | ////////// Step 3 //////////
223 | stage('Publish Docker and Helm') {
224 | steps {
225 | echo "Stop and remove container"
226 | sh "docker stop ${ID}"
227 |
228 | echo "Pushing ${DOCKER_REG}/${IMAGE_NAME}:${DOCKER_TAG} image to registry"
229 | sh "${WORKSPACE}/build.sh --push --registry ${DOCKER_REG} --tag ${DOCKER_TAG} --docker_usr ${DOCKER_USR} --docker_psw ${DOCKER_PSW}"
230 |
231 | echo "Packing helm chart"
232 | sh "${WORKSPACE}/build.sh --pack_helm --push_helm --helm_repo ${HELM_REPO} --helm_usr ${HELM_USR} --helm_psw ${HELM_PSW}"
233 | }
234 | }
235 |
236 | ////////// Step 4 //////////
237 | stage('Deploy to dev') {
238 | steps {
239 | script {
240 | namespace = 'development'
241 |
242 | echo "Deploying application ${ID} to ${namespace} namespace"
243 | createNamespace (namespace)
244 |
245 | // Remove release if exists
246 | helmDelete (namespace, "${ID}")
247 |
248 | // Deploy with helm
249 | echo "Deploying"
250 | helmInstall(namespace, "${ID}")
251 | }
252 | }
253 | }
254 |
255 | // Run the 3 tests on the deployed Kubernetes pod and service
256 | stage('Dev tests') {
257 | parallel {
258 | stage('Curl http_code') {
259 | steps {
260 | curlTest (namespace, 'http_code')
261 | }
262 | }
263 | stage('Curl total_time') {
264 | steps {
265 | curlTest (namespace, 'time_total')
266 | }
267 | }
268 | stage('Curl size_download') {
269 | steps {
270 | curlTest (namespace, 'size_download')
271 | }
272 | }
273 | }
274 | }
275 |
276 | stage('Cleanup dev') {
277 | steps {
278 | script {
279 | // Remove release if exists
280 | helmDelete (namespace, "${ID}")
281 | }
282 | }
283 | }
284 |
285 | ////////// Step 5 //////////
286 | stage('Deploy to staging') {
287 | steps {
288 | script {
289 | namespace = 'staging'
290 |
291 | echo "Deploying application ${IMAGE_NAME}:${DOCKER_TAG} to ${namespace} namespace"
292 | createNamespace (namespace)
293 |
294 | // Remove release if exists
295 | helmDelete (namespace, "${ID}")
296 |
297 | // Deploy with helm
298 | echo "Deploying"
299 | helmInstall (namespace, "${ID}")
300 | }
301 | }
302 | }
303 |
304 | // Run the 3 tests on the deployed Kubernetes pod and service
305 | stage('Staging tests') {
306 | parallel {
307 | stage('Curl http_code') {
308 | steps {
309 | curlTest (namespace, 'http_code')
310 | }
311 | }
312 | stage('Curl total_time') {
313 | steps {
314 | curlTest (namespace, 'time_total')
315 | }
316 | }
317 | stage('Curl size_download') {
318 | steps {
319 | curlTest (namespace, 'size_download')
320 | }
321 | }
322 | }
323 | }
324 |
325 | stage('Cleanup staging') {
326 | steps {
327 | script {
328 | // Remove release if exists
329 | helmDelete (namespace, "${ID}")
330 | }
331 | }
332 | }
333 |
334 | ////////// Step 6 //////////
335 | // Waif for user manual approval, or proceed automatically if DEPLOY_TO_PROD is true
336 | stage('Go for Production?') {
337 | when {
338 | allOf {
339 | environment name: 'GIT_BRANCH', value: 'master'
340 | environment name: 'DEPLOY_TO_PROD', value: 'false'
341 | }
342 | }
343 |
344 | steps {
345 | // Prevent any older builds from deploying to production
346 | milestone(1)
347 | input 'Proceed and deploy to Production?'
348 | milestone(2)
349 |
350 | script {
351 | DEPLOY_PROD = true
352 | }
353 | }
354 | }
355 |
356 | stage('Deploy to Production') {
357 | when {
358 | anyOf {
359 | expression { DEPLOY_PROD == true }
360 | environment name: 'DEPLOY_TO_PROD', value: 'true'
361 | }
362 | }
363 |
364 | steps {
365 | script {
366 | DEPLOY_PROD = true
367 | namespace = 'production'
368 |
369 | echo "Deploying application ${IMAGE_NAME}:${DOCKER_TAG} to ${namespace} namespace"
370 | createNamespace (namespace)
371 |
372 | // Deploy with helm
373 | echo "Deploying"
374 | helmInstall (namespace, "${ID}")
375 | }
376 | }
377 | }
378 |
379 | // Run the 3 tests on the deployed Kubernetes pod and service
380 | stage('Production tests') {
381 | when {
382 | expression { DEPLOY_PROD == true }
383 | }
384 |
385 | parallel {
386 | stage('Curl http_code') {
387 | steps {
388 | curlTest (namespace, 'http_code')
389 | }
390 | }
391 | stage('Curl total_time') {
392 | steps {
393 | curlTest (namespace, 'time_total')
394 | }
395 | }
396 | stage('Curl size_download') {
397 | steps {
398 | curlTest (namespace, 'size_download')
399 | }
400 | }
401 | }
402 | }
403 | }
404 | }
405 |
--------------------------------------------------------------------------------