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
- This ensures it is clear for any user of the script what is expected from them
- If ENV variables are expected, their presence should be verified
- 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
- This includes assumptions on
pwd
when the script starts
- This includes assumptions on
- 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 publicly 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, you probably need 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
- Don't do installs in jobs if this can be prevented. Ensure these dependencies are in the image used by the job instead (see the ci/cta-ci-images repo)
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 absolutely 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 when applicable.
- 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.
- Keep the main container startup script as simple as possible. In ideal world, the only thing this script is doing is starting the relevant service.
- When possible, use initContainers to do any initialisation
- Do not hardcode in the manifest files what someone might reasonably want to change. Use templating and the
values.yaml
for this instead.
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)
│ ├── 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/
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.