CI Guidelines¶
To ensure that the continuous integration setup is maintainable and flexible, there are a number of guidelines to follow. These are outlined below. When working on anything related to the CI, be sure to thoroughly read through this first.
Scripting¶
- Every script must have a copyright statement on top
- Scripts should take their input from flags and not from environment variables
- For each input flag, a script must provide a long-form version. E.g.
--image-tag
- If applicable, an additional short-form option can be provided. E.g.
-t
- If applicable, an additional short-form option can be provided. E.g.
- Each script that takes any sort of input must provide a
usage()
function - Keep scripts short and task-specific
- Scripts should follow the single-responsibility principle.
- If a script is doing too many things, split it into smaller scripts and call these scripts from a wrapper/parent script
- Scripts must not make any assumption on what the calling script is to ensure reusability
- Always validate (the existence of) input arguments to ensure proper behaviour
- Make sure scripts clean up after themselves. If temporary files are created that are no longer necessary after the script finishes, then using
trap
to remove these on script exit - Scripts should be able to handle errors gracefully
- Use tools like ShellCheck to catch potential bugs and improve script quality.
Container Images¶
- Be extremely careful never to install Oracle RPMs in any image that might ever be publically available!!
- We cannot distribute Oracle RPMs in any way, shape or form
- Never include any secrets in a Dockerfile
- Do not hardcode configuration values in a Dockerfile
- Do not do at runtime what can be done at buildtime
- No if-else branching in a Dockerfile. If this is needed, create a separate Dockerfile instead
- Naming for a Dockerfile should be
[<purpose>.]Dockerfile
- If there is only one Docker image, then naming this file
Dockerfile
is fine - Otherwise, prefix the purpose/name accordingly
- If there is only one Docker image, then naming this file
- If there are files only needed during the building of the image that are not needed at runtime, look into multi-stage builds to prevent these files from ending up in the final image
- Group similar commands into a single layer, but prevent overly complex
RUN
commands - Put frequently running/changing instructions towards the end of the Dockerfile to ensure earlier layers can still be cached effectively
- Remove temporary files, caches, and other unnecessary items during the build process to keep images lightweight
- Add comments to explain any non-trivial steps or design decisions in the Dockerfile
GitLab Pipeline¶
- Any job definitions go into
.gitlab/ci/*.gitlab-ci.yaml
- Similar jobs should be grouped into the same file.
- A single
.gitlab/ci/*.gitlab-ci.yaml
should not contain jobs belonging to multiple stages- The exception being different stages with the same prefix (e.g.
test:
orrelease:
)
- The exception being different stages with the same prefix (e.g.
- Use the following order for defining the job properties:
allow_failure
retry
stage
extends
tags
needs
dependencies
rules
image
variables
before_script
script
after_script
artifacts
release
- Use
needs
to specify which jobs a specific job is relying on. If there are none, specify an empty list.- This ensures that the job does not needlessly download all artifacts of jobs that came before
- It also ensures its correct position in the DAG
- Job naming should be all lowercase and contain only alphanumeric characters. Words can be split using hyphens:
example-job
- It is allowed to use a slash (
/
), colon (:
) or space in the name if it is required to group jobs together
- It is allowed to use a slash (
- In any script part, each line should be a separate entry in yaml. I.e. do not do
- |
and dump the entire script- This ensures the steps remain readable when the pipeline is executed
- For if-statement, using
- |
is fine to ensure readability. Try to keep any if-statements like this as short as possible
- Do not put secrets in any job definitions. Secrets should be defined as GitLab CI/CD variables and referenced as such
- Global pipeline variables are defined in
.gitlab-ci.yaml
- Jobs should refer to the image through a global variable to ensure that the images can be easily updated in a centralized placed
- A job should only run
on_success
(see therules
section) unless there is a very good reason not to - Try to prevent too much scripting logic ending up in a job definition. If applicable, create a script out of it
- Any calls to a script should use the long flag names (so
--namespace
instead of-n
) to improve readability - Jobs in a file should be grouped logically together. In general: default or general jobs go towards the top of the file, whereas edge-case/specific jobs go towards the bottom
Orchestration¶
- Don't do at runtime what can be done at build time.
- Least priviledge: don't give pods more priviledges than they need.
- For defining different configuration options, create an alternative
values.yaml
file, not a variation of an existing configmap (unless absolutel necessary). - Always check if things can be done natively in Helm/kubernetes before adding any scripting logic.
- Don't use plain pod definitions, use deployments/statefulsets/replicasets instead. This ensures pods can be easily redeployed.
- Restrict communication between pods with network policies to control ingress and egress.
- Store sensitive data in Kubernetes Secrets and reference them securely in pods.
- Ensure probes are correctly set up to monitor pod health and availability.
- Redirect application logs to
stdout
andstderr
so that logs can be easily collected by a logging agent - Add meaningful labels and annotations for organization, tracking, and automation (see the
common
chart). - Upgrade the chart version when changes are passed through.
Helm Structure¶
Adhere to the following directory structure per chart:
my-chart/
├── charts/ # Directory for dependent subcharts
├── templates/ # YAML templates for Kubernetes resources
│ ├── _helpers.tpl # Helper templates (e.g., for names, labels)
│ ├── subcomponent1/ # Additional resources for a subcomponent (optional)
│ ├── subcomponent2/
│ ├── deployment.yaml # Deployment manifest (optional)
│ ├── service.yaml # Service manifest (optional)
│ └── ingress.yaml # Ingress manifest (optional)
├── Chart.yaml # Chart metadata (name, version, etc.)
├── values.yaml # Default configuration values
├── values.schema.json # JSON schema for validating values (optional)
├── README.md # Documentation
Naming Guidelines¶
- Use lowercase letters
- Use hyphens (
-
) for separators - Keep names descriptive but concise: Names should indicate the purpose of the resource without being excessively long.
- Avoid special characters: Only use alphanumeric characters and hyphens.
Naming patterns:
- Chart Directory:
<name>/
- Example:
my-app/
,cta
- Values File:
values.yaml
(default values file) - Custom Values Files:
<env>[-<resource>]-values.yaml
- Example:
dev-values.yaml
,prod-values.yaml
,ci-catalogue-oracle-values.yaml
- Templates Directory:
templates/
- Helm release names:
<app-name>-<env>
- Example:
myapp-dev
,myapp-prod
Resource templates¶
Each resource should reside in its own separate file. Do not define multiple resources in a single file. If the resources are really tightly coupled, create a directory instead.
Stick to simple names for the main different resource types:
- Pod:
pod.yaml
- Deployment:
deployment.yaml
- Service:
service.yaml
- Ingress:
ingress.yaml
If required, prefix the above with a descriptive name.
For the following resource types, use a descriptive name, followed by the shortname of the resource type:
- ConfigMap:
<name>-configmap.yaml
- Secret:
<name>-secret.yaml
- Job:
<name>-job.yaml
- CronJob:
<name>-cronjob.yaml
If there already is a directory for said type (e.g. configmaps/
, secrets/
), then the suffix is no longer necessary and <name>.yaml
is sufficient.
In general, the templates/
directory should be split into separate components if relevant.
For example, instead of:
The resource names (not the file names, the name of the resource itself) should simply have a short descriptive name. In the case of jobs, postfix the name with -job
. This way it is clear during kubectl get pods
when something is a job. This also holds for cron jobs.