Skip to content

Commit c24d38a

Browse files
Preview environments (#271)
* started dynamic environments guide * Example application * added pictures * pull request preview * pipeline for close pr event
1 parent a0780bd commit c24d38a

13 files changed

+449
-110
lines changed

_data/home-content.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@
6565
icon: images/home-icons/guides.png
6666
url: ''
6767
links:
68-
# - title: CI/CD concepts
69-
# localurl: /docs/ci-cd-guide/concepts/
7068
- title: Build your app
7169
localurl: /docs/ci-cd-guides/packaging-compilation/
7270
- title: Building Docker images
@@ -79,7 +77,10 @@
7977
localurl: /docs/ci-cd-guides/microservices/
8078
- title: Production and Staging deployments
8179
new: true
82-
localurl: /docs/ci-cd-guides/environment-deployments/
80+
localurl: /docs/ci-cd-guides/environment-deployments/
81+
- title: Preview Environments
82+
new: true
83+
localurl: /docs/ci-cd-guides/preview-environments/
8384
- title: GitOps deployments
8485
new: true
8586
localurl: /docs/ci-cd-guides/gitops-deployments/

_data/nav.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@
128128
- title: "Pipelines for Microservices"
129129
url: "/microservices"
130130
- title: "Production and Staging deployments"
131-
url: "/environment-deployments"
131+
url: "/environment-deployments"
132+
- title: "Preview environments"
133+
url: "/preview-environments"
132134
- title: "GitOps deployments"
133135
url: "/gitops-deployments"
134136
- title: "Progressive Delivery"
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
---
2+
title: "Preview Environments"
3+
description: "Deploy Pull Requests to cluster namespaces"
4+
group: ci-cd-guides
5+
toc: true
6+
---
7+
8+
In the [previous guide]({{site.baseurl}}/docs/ci-cd-guides/environment-deployments/) we have seen how you can handle deployments to predefined environments (QA/Staging/production).
9+
10+
Another type of environments that you should manage is dynamic temporary environments for each pull request. For this type
11+
of environments it is best if you create dynamically an environment when a Pull Request is created and tear it down when the Pull Request is closed.
12+
13+
{% include image.html
14+
lightbox="true"
15+
file="/images/guides/preview-environments/dynamic-environments.png"
16+
url="/images/guides/preview-environments/dynamic-environments.png"
17+
alt="Dynamic Test environments"
18+
caption="Dynamic Test environments"
19+
max-width="90%"
20+
%}
21+
22+
This way each developer is working in isolation and can test their feature on its own. This pattern comes in contrast with the traditional way of reusing static preexisting environments.
23+
24+
{% include image.html
25+
lightbox="true"
26+
file="/images/guides/preview-environments/static-environments.png"
27+
url="/images/guides/preview-environments/static-environments.png"
28+
alt="Traditional static environments"
29+
caption="Traditional static environments"
30+
max-width="90%"
31+
%}
32+
33+
With Kubernetes you don't need to book and release specific test environments any more. Testing environments should
34+
be handled in a transient way.
35+
36+
## Preview environments with Kubernetes
37+
38+
There are many ways to create temporary environments with Kubernetes, but the simplest one is to use
39+
different namespaces, one for each pull request. So a pull request with name `fix-db-query` will
40+
be deployed to a namespace called `fix-db-query`, a pull request with name `JIRA-1434` will be deployed to a namespace called
41+
`JIRA-1434` and so on.
42+
43+
The second aspect is exposing the environment URL so that developers and testers can actually preview the application
44+
deployment either manually or via automated tests.
45+
46+
The two major approaches here are with host-based URLs or path based URLs.
47+
48+
* In host based urls, the test environments are named `pr1.example.com`, `pr2.example.com` and so on
49+
* with path based URLs, the test environments are named `example.com/pr1` , `example.com/pr2` and so on
50+
51+
Both approaches have advantages and disadvantages. Path based URLs are easier to setup but may not work with all applications (since they change the web context). Host based URLs are more robust but need extra
52+
DNS configuration for the full effect
53+
54+
In Kubernetes clusters, both ways can be setup via [an Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/).
55+
56+
## The example application
57+
58+
The application we will use can be found at [https://github.com/codefresh-contrib/unlimited-test-environments-source-code](https://github.com/codefresh-contrib/unlimited-test-environments-source-code). It is a standard Java/Spring boot application with the following characteristics.
59+
60+
* It has [integration tests]({{site.baseurl}}/docs/testing/integration-tests/) that can be targeted at any host/port. We will use those tests as smoke test that will verify the preview environment after it is deployed
61+
* It comes bundled in [a Helm chart](https://github.com/codefresh-contrib/unlimited-test-environments-manifests)
62+
* It has an ingress configuration ready for path based URLs
63+
64+
We are using [the Ambassador gateway](https://www.getambassador.io/) as an ingress for this example, but you can use any other compliant Kubernetes Ingress.
65+
66+
Here is [ingress manifest](https://github.com/codefresh-contrib/unlimited-test-environments-manifests/blob/main/simple-java-app/templates/ingress.yaml)
67+
68+
{% highlight yaml %}
69+
{% raw %}
70+
kind: Ingress
71+
apiVersion: extensions/v1beta1
72+
metadata:
73+
name: "simple-java-app-ing"
74+
annotations:
75+
kubernetes.io/ingress.class: {{ .Values.ingress.class }}
76+
77+
spec:
78+
rules:
79+
- http:
80+
paths:
81+
- path: {{ .Values.ingress.path }}
82+
backend:
83+
serviceName: simple-service
84+
servicePort: 80
85+
{% endraw %}
86+
{% endhighlight %}
87+
88+
The path of the application is configurable and can be set at deploy time.
89+
90+
## Creating preview environments for each pull request
91+
92+
Each time a Pull Request is created we want to perform the following tasks:
93+
94+
1. Compile the application and run unit tests
95+
1. Run security scans, quality checks and everything else we need to decided if the Pull request is valid
96+
1. Create a namespace with the same name as the pull request branch. Deploy the pull Request and expose it as a URL
97+
that has the same name as the branch as well
98+
99+
Here is an example pipeline that does all these tasks
100+
101+
{% include image.html
102+
lightbox="true"
103+
file="/images/guides/preview-environments/pull-request-preview-pipeline.png"
104+
url="/images/guides/preview-environments/pull-request-preview-pipeline.png"
105+
alt="Pull Request preview pipeline"
106+
caption="Pull Request preview pipeline"
107+
max-width="100%"
108+
%}
109+
110+
This pipeline has the following steps
111+
112+
1. A [clone step]({{site.baseurl}}/docs/codefresh-yaml/steps/git-clone/) to fetch the source code of the application
113+
1. A [freestyle step]({{site.baseurl}}/docs/codefresh-yaml/steps/freestyle/) that runs Maven for compilation and unit tests
114+
1. A [build step]({{site.baseurl}}/docs/codefresh-yaml/steps/build/) to create the docker image of the application
115+
1. A step that scans the source code for security issues with [Snyk](https://snyk.io/)
116+
1. A step that scans the container image [for security issues]({{site.baseurl}}/docs/testing/security-scanning/) with [trivy](https://github.com/aquasecurity/trivy)
117+
1. A step that runs [integration tests]({{site.baseurl}}/docs/testing/integration-tests/) by launching the app in a [service container]({{site.baseurl}}/docs/codefresh-yaml/service-containers/)
118+
1. A step for [Sonar analysis]({{site.baseurl}}/docs/testing/sonarqube-integration/)
119+
1. A step that clones [a second Git repository](https://github.com/codefresh-contrib/unlimited-test-environments-manifests) that has the [Helm chart]({{site.baseurl}}/docs/new-helm/using-helm-in-codefresh-pipeline/) of the app
120+
1. A step that deploys the source code to a new namespace.
121+
1. A step that [adds a comment on the Pull Request](https://codefresh.io/steps/step/kostis-codefresh%2Fgithub-pr-comment) with the URL of the temporary environment
122+
1. A step that runs smoke tests against the temporary test environment
123+
124+
Note that the integration tests and security scans are just examples of what you can do before the Pull Request is deployed. You can insert your own steps that check the contents of a Pull Request.
125+
126+
Here is the whole YAML definition
127+
128+
`codefresh.yml`
129+
{% highlight yaml %}
130+
{% raw %}
131+
version: "1.0"
132+
stages:
133+
- "prepare"
134+
- "verify"
135+
- "deploy"
136+
137+
steps:
138+
main_clone:
139+
title: "Cloning repository"
140+
type: "git-clone"
141+
repo: "codefresh-contrib/unlimited-test-environments-source-code"
142+
revision: "${{CF_REVISION}}"
143+
stage: "prepare"
144+
145+
run_unit_tests:
146+
title: Compile/Unit test
147+
stage: prepare
148+
image: 'maven:3.5.2-jdk-8-alpine'
149+
commands:
150+
- mvn -Dmaven.repo.local=/codefresh/volume/m2_repository package
151+
build_app_image:
152+
title: Building Docker Image
153+
type: build
154+
stage: prepare
155+
image_name: kostiscodefresh/spring-actuator-sample-app
156+
working_directory: ./
157+
tag: '${{CF_BRANCH}}'
158+
dockerfile: Dockerfile
159+
scan_code:
160+
title: Source security scan
161+
stage: verify
162+
image: 'snyk/snyk-cli:maven-3.6.3_java11'
163+
commands:
164+
- snyk monitor
165+
scan_image:
166+
title: Container security scan
167+
stage: verify
168+
image: 'aquasec/trivy'
169+
commands:
170+
- trivy image docker.io/kostiscodefresh/spring-actuator-sample-app:${{CF_BRANCH}}
171+
run_integration_tests:
172+
title: Integration tests
173+
stage: verify
174+
image: maven:3.5.2-jdk-8-alpine
175+
commands:
176+
- mvn -Dmaven.repo.local=/codefresh/volume/m2_repository verify -Dserver.host=http://my-spring-app -Dsonar.organization=kostis-codefresh-github
177+
services:
178+
composition:
179+
my-spring-app:
180+
image: '${{build_app_image}}'
181+
ports:
182+
- 8080
183+
readiness:
184+
timeoutSeconds: 30
185+
periodSeconds: 15
186+
image: byrnedo/alpine-curl
187+
commands:
188+
- "curl http://my-spring-app:8080/"
189+
sonar_scan:
190+
title: Sonar Scan
191+
stage: verify
192+
image: 'maven:3.8.1-jdk-11-slim'
193+
commands:
194+
- mvn -Dmaven.repo.local=/codefresh/volume/m2_repository sonar:sonar -Dsonar.login=${{SONAR_TOKEN}} -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=kostis-codefresh-github
195+
clone:
196+
title: "Cloning repository"
197+
type: "git-clone"
198+
repo: "codefresh-contrib/unlimited-test-environments-manifests"
199+
revision: main
200+
stage: "deploy"
201+
deploy:
202+
title: Deploying Helm Chart
203+
type: helm
204+
stage: deploy
205+
working_directory: ./unlimited-test-environments-manifests
206+
arguments:
207+
action: install
208+
chart_name: simple-java-app
209+
release_name: my-spring-app
210+
helm_version: 3.2.4
211+
kube_context: myawscluster
212+
namespace: ${{CF_BRANCH_TAG_NORMALIZED}}
213+
cmd_ps: '--create-namespace --wait --timeout 5m'
214+
custom_values:
215+
- 'image_tag=${{CF_BRANCH_TAG_NORMALIZED}}'
216+
- 'replicaCount=3'
217+
- 'ingress_path=/${{CF_BRANCH_TAG_NORMALIZED}}/'
218+
add_pr_comment:
219+
title: Adding comment on PR
220+
stage: deploy
221+
type: kostis-codefresh/github-pr-comment
222+
fail_fast: false
223+
arguments:
224+
PR_COMMENT_TEXT: "[CI] Staging environment is at https://kostis.sales-dev.codefresh.io/${{CF_BRANCH_TAG_NORMALIZED}}/"
225+
GIT_PROVIDER_NAME: 'github-1'
226+
run_smoke_tests:
227+
title: Smoke tests
228+
stage: deploy
229+
image: maven:3.5.2-jdk-8-alpine
230+
working_directory: "${{main_clone}}"
231+
fail_fast: false
232+
commands:
233+
- mvn -Dmaven.repo.local=/codefresh/volume/m2_repository verify -Dserver.host=https://kostis.sales-dev.codefresh.io/${{CF_BRANCH_TAG_NORMALIZED}}/ -Dserver.port=443
234+
{% endraw %}
235+
{% endhighlight %}
236+
237+
The end result of the pipeline is a deployment on the path that has the same name as the pull request branch. For
238+
example if my branch is named `demo` then a demo namespace is created on the cluster and the application
239+
is exposed on the `/demo/` context:
240+
241+
{% include image.html
242+
lightbox="true"
243+
file="/images/guides/preview-environments/demo-path.png"
244+
url="/images/guides/preview-environments/demo-path.png"
245+
alt="Temporary environment"
246+
caption="Temporary environment"
247+
max-width="100%"
248+
%}
249+
250+
The environment is also mentioned as a comment in the Pull Request UI in Gituhub:
251+
252+
{% include image.html
253+
lightbox="true"
254+
file="/images/guides/preview-environments/pull-request-comment.png"
255+
url="/images/guides/preview-environments/pull-request-comment.png"
256+
alt="Pull Request comment"
257+
caption="Pull Request comment"
258+
max-width="100%"
259+
%}
260+
261+
As explained it the [previous guide for Pull Requests]({{site.baseurl}}/docs/ci-cd-guides/pull-request-branches/), we want to make this pipeline applicable only
262+
to PR open event and PR sync events (which captures commits that happen on an existing pull request).
263+
264+
{% include image.html
265+
lightbox="true"
266+
file="/images/guides/preview-environments/pr-events.png"
267+
url="/images/guides/preview-environments/pr-events.png"
268+
alt="Git events for a Pull Request preview pipeline"
269+
caption="Git events for a Pull Request preview pipeline"
270+
max-width="100%"
271+
%}
272+
273+
Therefore you need to setup your [triggers]({{site.baseurl}}/docs/configure-ci-cd-pipeline/triggers/git-triggers/) with the same checkboxes shown in the picture above.
274+
275+
## Cleaning up temporary environments
276+
277+
Creating temporary environments is very convenient for developers but can be very costly for your infrastructure if you use a cloud
278+
provider for your cluster. For cost reasons and better resource utilization it is best if temporary environments are destroyed if they are
279+
no longer used.
280+
281+
While you can run a batch job, that automatically deletes old temporary environments, the optimal approach is to delete them as soon as
282+
the respective Pull Request is closed.
283+
284+
We can do that with a very simple pipeline that has only one step:
285+
286+
{% include image.html
287+
lightbox="true"
288+
file="/images/guides/preview-environments/pull-request-closed-pipeline.png"
289+
url="/images/guides/preview-environments/pull-request-closed-pipeline.png"
290+
alt="Pipeline when a Pull Request is closed"
291+
caption="Pipeline when a Pull Request is closed"
292+
max-width="100%"
293+
%}
294+
295+
Here is the pipeline definition:
296+
297+
`codefresh-close.yml`
298+
{% highlight yaml %}
299+
{% raw %}
300+
version: "1.0"
301+
steps:
302+
delete_app:
303+
title: Delete app
304+
type: helm
305+
arguments:
306+
action: auth
307+
helm_version: 3.2.4
308+
kube_context: myawscluster
309+
namespace: ${{CF_BRANCH_TAG_NORMALIZED}}
310+
commands:
311+
- helm delete my-spring-app --namespace ${{CF_BRANCH_TAG_NORMALIZED}}
312+
- kubectl delete namespace ${{CF_BRANCH_TAG_NORMALIZED}}
313+
{% endraw %}
314+
{% endhighlight %}
315+
316+
The pipeline just uninstall the Helm release for that namespace and then deletes the namespace itself.
317+
318+
To have this pipeline run only when a Pull Request is closed here is how your [trigger]({{site.baseurl}}/docs/configure-ci-cd-pipeline/triggers/git-triggers/) should look:
319+
320+
{% include image.html
321+
lightbox="true"
322+
file="/images/guides/preview-environments/close-events.png"
323+
url="/images/guides/preview-environments/close-events.png"
324+
alt="Git events for a Pull Request close pipeline"
325+
caption="Git events for a Pull Request close pipeline"
326+
max-width="100%"
327+
%}
328+
329+
Notice that with this setup that pipeline will run when the pull request was closed regardless of wether it was merged or not (which is exactly what you want as in both cases the test environment is not needed anymore).
330+
331+
332+
333+
334+
335+
## What to read next
336+
337+
* [How Codefresh pipelines work]({{site.baseurl}}/docs/configure-ci-cd-pipeline/introduction-to-codefresh-pipelines/)
338+
* [Codefresh YAML]({{site.baseurl}}/docs/codefresh-yaml/what-is-the-codefresh-yaml/)
339+
* [Pipeline steps]({{site.baseurl}}/docs/codefresh-yaml/steps/)
340+
* [Working with Docker registries]({{site.baseurl}}/docs/docker-registries/working-with-docker-registries/)
341+
* [Build step]({{site.baseurl}}/docs/codefresh-yaml/steps/build/)
342+
343+
344+
345+
346+

0 commit comments

Comments
 (0)