GitOps¶
GitOps is a modern approach to managing and automating infrastructure and application deployments using Git as the single source of truth. It combines the power of version control with declarative infrastructure and application definitions to provide a reliable and auditable way to manage and deploy changes.
With GitOps, all configuration and deployment instructions are stored in a Git repository, allowing teams to track changes, collaborate, and roll back deployments easily. This approach promotes a Git-centric workflow, where changes made to the repository trigger automated deployments, ensuring consistency and reproducibility across environments.
RAIL is leveraging a PULL-style GitOps model, where the cluster pulls the desired state from a Git repository and applies it to the cluster. This model is in contrast to a PUSH-style model, where a CI/CD system pushes changes to the cluster.
The GitOps Repository¶
The GitOps Repository is a Git repository that contains all the configuration and deployment instructions for a team’s applications. It is the single source of truth for the team’s infrastructure and application deployments. The repository is organized into directories, with a base directory for the application and resources available for all clusters, and a directory for each cluster where the application is deployed. Using Kustomize, Flux will monitor the subdirectories for each cluster and apply the configuration accordingly.
The GitOps Repository is organized into the following directories:
├── .sops.pub.asc # A public PGP key you can use to sign secrets that will be decoded in the clusters by SOPS (mozilla Secret OPerationS)
├── .sops.yaml # A configuration file that tells SOPS which values to encrypt
├── base # This level contains apps and resources that's available for use in all clusters
│ ├── example-from-docs # This level represents this app and its set of resources. It must include kuztomization.yaml that includes the other manifest files
│ │ ├── deployment.yaml
│ │ ├── ingress.yaml
│ │ ├── kustomization.yaml # This file includes the other manifest files
│ │ └── service.yaml
│ └── README.md # Love your READMEs, and feed and exercise them regulary and keep them lean.
├── osl1-test # This directory level representes the configuration thats unique for a specific cluster
│ ├── example-from-docs # This directory level represents configuration for this app in the osl1-test cluster
│ └── RAIL # This is where the fluxcd controller finds deployments for this cluster
├── README.md
└── bgo1-prod # You guessed it, this is another cluster, for your production workloads
├── RAIL # ...and this is where the fluxcd controller finds deployments for this specific cluster
│ └── some_app
│ └── kustomization.yaml # Define a namespace and a fluxcd kustomization here in order to deploy some_app
└── some_app #
└── kustomization.yaml # This file includes the base directory and the other manifest files, enabling the resources in the cluster
Kustomizations¶
In order to make sense of the GitOps Repository structure we need to understand what Kubernetes Kustomizations are.
First we need to establish some basic Kubernetes terms:
- Resource
A somewhat abstract concept of the stuff might be realized or represented by a Kubernetes cluster.
- Resource Object
An JSON-style object that describes the desired state of a Resource as well as attributes that describe its current status. Resource Objects are what the Kubernetes API returns and can mutate from its resource endpoints. Resource Objects are identified by the triplet of attributes
(kind, metadata.namespace, metadata.name). The fieldapiVersionis also mandatory and determines together withkindhow the rest of the attributes are to be interpreted.- Resource Definition
The schema that specifies what attributes can be present for a Resource Object of a specific
kindandapiVersion.- Resource Configuration
An object that represent the desired state of an identified Resource. It is customary for many resource definitions to express the desired state within the attribute named
spec. The Resource Configuration is a partial Resource Object.- Manifest
An YAML file (or stream) that contains one or more YAML documents that each represent a single Resource Configuration. Multiple YAML documents in a YAML file are separated by 3 dashes
"---\n".
This is the structure of each YAML document (representing a Resource Configuration) in a Manifest:
apiVersion: v1
kind: ResourceType
metadata:
name: something
namespace: adm-it-xxx
spec:
...
A Kustomization is a directory in the file system that contains a file called kustomization.yaml that contains a single YAML document with the attributes:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- manifest.yaml
- kustomization_directory
- ...
The resources attribute is a list of paths which can either reference a
Manifest file (with .yaml extension) or another Kustomization directory. The
result of this is a new Manifest containing the concatenations of all the
referenced Manifests. There might be other attributes in the
kustomization.yaml file that modifies attributes within the resource
configurations contained or adds new resource configurations. Read the
documentation for kustomize for details on how to kustomize your Manifests.
You can run the command kubectl kustomize <directory> to stream the
generated Manifest to stdout.
GitOps Repository Structure¶
Each RAIL Team has an identifier on the form adm-it-xxx and they are given a
GitOps repository and a Kubernetes namespace (in each RAIL cluster) with the
same name. RAIL clusters synchronizes the resource configurations in the team’s
namespace from the kustomization found in the <cluster-name>/RAIL directory in
the main branch of the team’s GitOps repository.
The component within RAIL that does this synchronization is called Flux, and
more specifically the Flux Kustomization Controller. Its operation and status
can be monitored by watching the Flux Kustomization resource objects within your team’s namespace.
Run kubectl get ks or kubectl describe ks to list it or display details.
If the <cluster-name>/RAIL directory does not contain a kustomization.yaml
file, then Flux works as if it created one by running the equivalent of the command
kustomize create --autodetect --recursive in that directory. The effect is
that all manifests and kustomizations in that directory and all subdirectories
are automatically included.
It is recommended to run each application that the RAIL Team manages in their own sub-namespace. We achieve this by setting up the sub-namespaces with their own Flux Kustomization configurations from the bootstrap kustomization:
<cluster-name>/RAIL # contains configurations for the `adm-it-xxx` namespace
<cluster-name>/RAIL/app1.yaml # sets up the `adm-it-xxx-app1` namespace
<cluster-name>/RAIL/app2.yaml # sets up the `adm-it-xxx-app2` namespace
<cluster-name>/app1 # contains configurations for the `adm-it-xxx-app1` namespace
<cluster-name>/app1/ingress.yaml
<cluster-name>/app1/deploy.yaml
<cluster-name>/app2 # contains configurations for the `adm-it-xxx-app2` namespace
<cluster-name>/app2/ingress.yaml
<cluster-name>/app2/deploy.yaml
In addition teams usually want to run multiple instances of the same application
on the RAIL clusters without duplicating the configuration. We achieve this by
moving the app configuration to the base directory and setting up kustomizations
that import it and tweaks some settings for each instance.
For instance we end up with the following structure when we want to run multiple
instances of app1:
base
base/app1
base/app1/kustomization.yaml
base/app1/ingress.yaml
base/app1/deploy.yaml
<cluster-name>/RAIL
<cluster-name>/RAIL/app1.yaml
<cluster-name>/RAIL/app1-test.yaml
<cluster-name>/RAIL/app2.yaml
<cluster-name>/app1
<cluster-name>/app1/kustomization.yaml # imports `../../base/app1`
<cluster-name>/app1-test
<cluster-name>/app1-test/kustomization.yaml # imports `../../base/app1`
<cluster-name>/app2
<cluster-name>/app2/ingress.yaml
<cluster-name>/app2/deploy.yaml
Protect secrets with SOPS¶
Secrets can be added to namespaces by creating encrypted manifests in the GitOps Repository. Secrets encrypted with the SOPS tool using the published public PGP-key will be automatically decrypted inside the RAIL-cluster by Flux.
Preparation to do before you can start using sops:
install the
gnupgpackage — which gives you access to thegpg-command line utilityinstall the
sopspackage — which gives you access to thesops-command line utilityrun
gpg --import .sops.pub.ascfrom the root of the GitOps Repository
The last step makes the published public PGP-key for RAIL available when SOPS invokes gpg to encrypt variables within files.
The root of the GitOps repository also contains a .sops.yaml file that tells the sops tool
to encrypt files within this directory with this key.
Create a YAML-file (for instance a manifest containing a Secret) with values that you want to encrypt, like:
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
APP_CLIENT_ID: ...
APP_CLIENT_SECRET: ...
If you save this file as app-secret.yaml, then you can encrypt it by running:
sops encrypt --in-place app-secret.yaml
Check that the secrets are now replaced with a string that looks like
ENC[...unreadable stuff...] and that a sops attribute was appended to the top
level structure of the file. While testing run the command without the
--in-place option and inspect that you get the expected output on stdout.
Commit the file to Git with the assurance that no one besides the RAIL platform can retrieve the secret, even if they gain access to the repository.
Attention
It is handy to instruct sops how to decrypt files with keys that are shared
between members of your team. This allow you to change secrets in the encrypted
files directly by invoking sops edit app-secret.yaml
One way to do this at UiB is to set up a shared Azure Key Vault with a key and add this
line to the .sops.yaml-file at the root of your GitOps repository:
azure_keyvault: https://uib-adm-it-xxx.vault.azure.net/keys/sops-rail
SOPS will then manage to use this vault key after you run az login, which requires
that the azure-cli package is installed to make the az command available that provide
the command line interface to the Azure Cloud.
SOPS with secretGenerator¶
One problem with Secrets and ConfigMaps resources is that workloads that
depend on them do not automatically reload when the resource change. The
kustomization.yaml files have a solution for this; the secretGenerator
and the configMapGenerator elements. These make kustomize generate
secrets and configmaps with names that get a hash-suffix that reflects the
content in these resources. The workload manifest with references to these
resources are automatically patched with references to the hashed names, which
cause these manifests to change, and thus the corresponding workload resources
to reload, when secrets and configmaps change.
We can use SOPS with the secretGenerator with some caveats:
You can’t use
literalswith encrypted content as thekustomization.yamlfile itself is not decrypted before it’s used.For
envsmake the referenced files have.envextensionFor
filesmake both the local name and the referenced file name have the same extension; one of.env,.ini,.yamlor.jsonis known to work.Encrypt the referenced
.env,.ini,.yamlor.jsonfiles withsops encrypt --in-place <filename>.
Example kustomization.yaml file that demonstrates use of secretGenerator:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deploy.yaml
secretGenerator:
- name: app-files
files:
- foo.ini=foo.ini
- bar.json=bar.json
- name: app-vars
envs:
- secrets.env
This generates two secrets named app-files-<suffix> and app-vars-<suffix>.
In the deploy.yaml file you can reference these as if the names where app-files
and app-vars, but the generated Kustomization will have these names patched to
match the suffixed names.
You protect the secrets themselves by encrypting them with these commands:
sops encrypt --in-place foo.ini
sops encrypt --in-place bar.json
sops encrypt --in-place secrets.env
To make this work you might need to update the top level .sops.yaml file with
entries that also match extensions like .ini and .env.
Remove an app from RAIL¶
To remove an app from RAIL all that is needed is to remove all YAML manifest files that declare the app and then push the update to the GitOps repository. In actuality only the declaration of the sub-namespace needs to be removed but for clarity it is best to remove all YAML files that belong to the app. If you have some common manifests declared in base for test and production it is best to wait removing them from base until you are ready to remove from both test and production.
If you for some reason want to delete the namespace manually, run
kubectl -n adm-it-xxx delete subns adm-it-xxx-app. Be aware that if the
corresponding manifest has not been removed in GitOps, then the namespace will
be recreated in RAIL after a short while.