In agile teams, it is crucial to embrace a GitOps model to achieve a major maturity level of process development, thus evolving the product faster. The goals should be small in the way we could iterate over them. When I write about “maturity levels,” I refer to automation of traceability, configuration, and secret management, least privilege access.
Alexis Richardson coined GitOps at Weaveworks around 2017, where he explained its meaning. Some people could see the GitOps term as the next level in the process development, which could be a level up from the “DevOps” term in the process development. It is a new way of looking at the operation process. Before diving into something practical, we need to recall which are the principles of GitOps:
- Infrastructure as code (IaC): It allows us to treat the configuration of infrastructure and deployment of code in the same way we usually manage the software development process using a familiar tool: Git. You could find definitions such as “Git as a single source of truth,” which means using Git to operate almost everything, even configuration and secrets.
- Self-Service: GitOps flow aims to break the barrier by automating and making it self-service (a pull request and code review). In this way, we as developers are close to the operation or deployment of environments in a simple way.
- Declarative infrastructure configuration: In several references, they recommend using a pure declarative infrastructure configuration. It is an easy and simple way to map the state of our deployment and is more intuitive. This type of configuration lets us do code reviews, comments, and pull requests of configuration files.
- Observability: We need software agents to ensure correctness and alert on divergence in our system. We need alerts, health checks that inform us if something went wrong with some element or application, checking through Git the desired state of the system against the actual state, and doing the best to self-healing.
Taking this into account, you could improve stability, higher reliability, consistency, and standardization. Also, this could enhance the development experience making the deployment process transparent (with a commit approved, you could deploy in a simple way, because Git is the source of truth).
We could use these principles without using Kubernetes. You could choose the tech stack that adjusts more to your situation or platform. In this blog, we use minikube in a practical way to review subjects on GitOps and how Kubernetes (minikube) help us to understand the flow of GitOps.
Kubernetes was first developed by a team at Google. They called it Borg around 2003. By mid-2014, Google presented Borg in an open-source version called “Kubernetes.” They defined it as “an open-source reference implementation of container cluster management.” You can find Kubernetes abbreviated in several bibliographies like “k8s”, so from now on, I will refer to Kubernetes in this way.
The k8s cluster should subscribe to changes made in a repository. So the first requirement we have is having a Git repository. You could use github, gitlab, bitbucket. Second, you need to install minikube. This software lets you try basic commands from a k8s cluster and test the development workflow.
After this, you could see something like this:
You could choose how to use minikube. I mean, how k8s will compose the pod (we will see this term later), only you have to set up the driver.
If “vm-driver” is blank by default, minikube will use Docker. Once you created the cluster with a specific driver, you couldn’t start with another driver type (you’ll get an error when you do it). I will create the cluster with the docker driver, so I executed it without setting the driver. This will be a little slow the first time only because it is pulling all images. The next execution will be faster. You will see something like this:
After that, you need another tool called “kubectl” that lets you manage (create, update and delete) the k8s resources. Kubectl supports imperative commands as declarative commands. You could use your package manager or download and install in this way:
We will mention the elements in a k8s cluster. In a k8s cluster, you could find a master and nodes at the first level. At least one node should exist. A node represents a virtual or physical machine. Each node has elements like kubelet, kubelet proxy, Pods, and some add ons related to Dns, UI.
Listing Nodes in a cluster:
A Pod is the smallest unit in k8s cluster. They represent a running process in a cluster. You could have more than one container in a pod. In this example, we have docker containers in each pod because we selected that driver at the beginning when we init our cluster with minikube.
Creating a Pod (this command will add the pod to the default namespace later we explain this):
Another way to create a pod is using a yml file that defines our deployment in a declarative way. This is the recommended option because it follows one of the GitOps principles. When we talk about a declarative way, we define in a yml the desired state we want.
In an imperative way, we put steps one after another or execute commands in order to get the desired state, i.e., when I execute kubectl run and later kubectl delete.
The next links are yml examples. In a declarative way, the first one nginx-pod will create a pod using two docker images: nginx and docker/whalesay, but the final result is one docker container, showing “Hello kubernetes” in an HTML file. The second one will create another docker container with a mongodb instance. You shouldn’t mix declarative and imperative commands in a deployment.
Using a declarative way to create a Pod.
Listing Pods in a node with the default namespace:
Add -o wide to get more information or to specify a format like json or yaml.
Listing pods running in a cluster:
When I read this command for the first time, I related it to the command docker inspect. When we need information about the pod, we could get it with this command. The output is extensive and has a lot of information: the more relevant items are namespace, status could be pending or running, node, Start Time, Restart Count, and Event-related to it:
A Pod in “Running” state means that all containers successfully started, and the db Pod is ready to serve requests. If the db pod in your cluster is running, then we can try accessing it or executing a well-known “ps” command to prove that it is actually working. We’ll kill the mongodb process (change the deployment state) and we’ll observe that k8s will do the necessary to bring us the desired state defined in the yml file:
Executing a command inside a POD in the first container that found, use -c to specify a container (remember a pod could have multiple containers).
Accessing to the POD (running a shell inside the container):
Killing a process inside the container and getting pod status: you could see the Restarts column increase in one. These k8s features let us keep redundancy and stability. Remember the fourth principle where we have a software agent checking the actual state vs. the desired state of an application and making actions to achieve the desired state defined in the Yaml file.
To delete the pod: After this practical, we could delete the Pod to become a new practical and the last one. Also, you could delete the entire deployment, and the pod will be deleted too:
A deployment contains nodes, pods, use images, and all defined in a yml file called the manifest. The deployment could be organized by namespace. Also, you could use annotations to select objects/resources. The first part of a yml file declarative with a deployment looks like this:
We set three replicas, some metadata “environment” and “organization,” and we specified the container’s nginx image. The Yml file with this kind of definition is called “manifest.”
Creating a deployment: Download the file from:
Update a deployment:
If you edit declarative-deployment.yaml, you couldn’t see the change immediately in the k8s cluster. To see the changes edited in the file, you need to execute the command kubectl apply or kubectl patch. An alternative is using watch with this command kubectl to subscribe to changes made into the declarative-deployment.yaml. And when you update the deployment, you could see the output message change a little bit when you created the deployment.
In a GitOps flow, you have an element called controller that maintains an infinite loop and continuously reconciles the desired and actual state, checking if a change was made to this file. There are many patterns to implement a controller. K8s comes with built-in controllers, and you also could write it in different languages. The controller should use the API server to manage the cluster. It’s recommended that we have many simple controllers instead of one monolithic controller, i.e. use a controller for each rk8s resource to manage. And when we talk about automation, we need to include a new term: the GitOps operator that lets automate a task beyond what Kubernetes itself provides.
There are many tools to implement CI/CD. Some of these tools are graden.io, skaffold, Draft, Squash, Helm, Charts, Ksonnet, Fluxcd, Argo CD, kubespray. The path is the same. You should choose the option more suitable for the environment or kind of software application. There is no unique way or a magic equation to apply these principles. Each company has different needs and structures.