Disclaimer: This article is for developers in growing middle-sized companies. Why only that? As a developer, I honestly believe that things like GitOps, K8S, Infrastructure as Code is overengineering when it comes to smaller teams. Opposite big corporations with senior DevOps engineers on the teams solve problems at scale with hundreds of technologies and tools and hardly can reproduce the same results for other companies.
However, it’s a bit advanced for beginners and some prerequisites must be met:
- You understand Kubernetes concepts: service, pod etc.
- You can install node and npm without help.
- You’ve heard about Pulumi, Terraform or Crossplane and know when people use them.
- You understand what the deployment process in your company looks like.
In this article, we’d be adapting GitOps principles to make our life easier. Below is how this would be done with this article:
- Store desired state of our services in kubernetes cluster on Github;
- Provision k3s cluster in Civo with Pulimi;
- Define app deployment using typescript;
- Automatically sync desired state in the cluster using Pulumi kubernetes operator
Step 1: Civo
Civo offers you a cheap managed k3s cluster. K3s is a fully conformant production-ready Kubernetes distribution. It is similar to the AKS/EKS/GKE in common scenarios, where you do not need complex networks, customized runtimes, etc.
Sign up and copy api key from:
Settings -> Profile -> Security -> api key
That’s it. You get a cluster with 2 VMs 2GB RAM and 1vcore each at 16 USD/month. Cool, isn’t it?
Step 2: Pulumi
Pulumi solves the same problems as Terraform. However, in my opinion, it does it more smoothly with no hitch. Another benefits of Pulumi among others is that it works with commonly used programming languages to define resources (C#, python, typescript etc).
For example, Terraform goes with its own HCL YAML-based language (I’ve also tried terraform CDK but it’s still in beta and missing many libraries and conversions between TS, JS, HCL, json makes me crazy, but that’s not the topic of discourse here).
Choose your favorite language. Though I’m a .net developer I still have an opinion in relation to devops. Thus, for DevOps purposes TypeScript looks more promising: SDK gets faster updates and new releases, simple project structure, and has less code. But of course you can use any, even python (God forgive you).
Sign up on Pulumi, select your personal env (not organization) and create a new token. Save it, we’ll need it at step 4.
Install pulumi on your local machine, create a working directory (for storing the desired state of our application layer). Then, run the following commands:
$ pulumi login
$ pulumi new
Type: typescript-kubernetes
Project name: k8s-app-layer
Stack name: dev
Pulumi has great documentation and tutorials. It would be silly to paste it here. Anyway you’ll be reading official docs if you decide to use pulumi and never this article. My goal here is just to make you interested and show minimal working setup.
Step 3: GitOps
Let’s say you have 3 backend services (customers API, orders API and payment API) and 2 frontend apps (public available ecommerce platform and admin panel for employees), what you would do is to set up CI and push containers to the registry. After that developers often relax and hand over the rest of the work to someone else. Usually, this fellow then uses helm and go templating to deliver those container images to k8s. More often than not, this comes off looking awful with tons of .yaml, .gotmpl, .sh, .kubeconfig files littered all over. Nobody except the guy who did it seems to have a clue on what happens there.
Here we do the same, but the huge difference that sets us apart is that we try to make it a lot more developer-friendly so you need not have the other guy take over.
For demo purposes, and because we don’t have 3 backend and 2 frontend services, we will try to deploy a simple nginx. Let’s edit index.ts
import * as k8s from "@pulumi/kubernetes";
import * as kx from "@pulumi/kubernetesx";
const namespace = 'dev';
const pb = new kx.PodBuilder({
containers: [{ image: "nginx", ports: { http: 80 } }]
});
const deployment = new kx.Deployment("nginx", {
metadata: { namespace: namespace },
spec: pb.asDeploymentSpec()
});
const service = deployment.createService({
type: kx.types.ServiceType.ClusterIP,
ports: [{ port: 80, targetPort: 80, name: 'http' }]
});
const backendIngress = new k8s.networking.v1.Ingress('nginx-ingress', {
metadata: {
name: 'nginx-ingress',
namespace: namespace
},
spec: {
rules: [{
host: 'replace with your host',
http: { paths: [{ path: '/', pathType: 'Prefix', backend: { service: { name: service.metadata.name, port: { number: 80 } } } }] }
}]
}
});
The code is self-described. And, with autocomplete and syntax validation btw. In your project resources will certainly be different. In this example, however, we merely run the service and expose it to the internet.
This state will be applied automatically a bit later. Pulumi kubernetes operator will run it from inside the kubernetes cluster in the next step.
Note: it’s still a programming language. You can read from files, modify strings, request HTTP endpoints etc. Just a few CD tools can do it without stress.
We are good with that, now, push the changes to Git.
Just to make it clear. We’d be having 2 stacks on Pulumi:
- One for storing desired Kubernetes state (called k8s-app-layer i.e.,what we just pushed to Git)
- Another for our infrastructure (Civo k3s cluster, Pulumi Kubernetes operator and Civo firewall rules) in step 4.
Step 4. Provisioning infrastructure
Okay, let’s create another Pulumi project and define our infrastructure.
Type: typescript-kubernetes
Project name: infra-civo
Stack name: dev
Never call your projects the same name as existing providers (civo, azure) because this will cause conflicts in the config file.
After that:
$ npm add @pulumi/civo
$ pulumi config set civo:token `replace with civo api key from step 1` --secret
$ pulumi config set civo:region FRA1
Yes, Civo has its SDK just like other mature providers (e.g Amazon or Azure). Paste token from step 1 and define default region (otherwise you’ll have to pass a parameter in each resource you want to create).
$ pulumi config set pulumiAccessToken `replace with pulumi token from step 2` --secret
$ pulumi config set appProjectRepo `replace with a link to GitHub project with desired state from step 3`
pulumi config set appPulumiProject `replace with pulumi project name, in my case it's called "Bolotov" from the screenshot above) + "/k8s-app-layer"`
Here we’re adding parameters to configure pulumi kubernetes operator.
In plain terms, here how it works:
Pulumi kubernetes operator is an infinite loop, running inside kubernetes. It waits for new commits in the pointed git repository
appProjectRepo
. When a commit appears, the operator is trying to runpulumi up
, so defined resources will be created in kubernetes and knowledge about them will be saved in pulumi stackappPulumiProject
Now that we’re done with the configuration, let’s finally create our cluster.
You can find the source code on github. I’ll not paste it here. It looks pretty much similar to what we’ve seen in step 3.
- index.ts is an entry point for pulumi, here we define core infrastructure resources such as firewall and cluster itself.
- pulumi-k8s-operator is pulumi operator.
- pulumi-k8s-stack just defines where to find the desired state we want to have (link to github repo).
Just copy-paste these 3 files and run pulumi up
.
We’re done. Pulumi cli installed locally sends commands to civo to create a kubernetes cluster and then to deploy its operator. When it’s done the operator is fetching the desired state (pod, ingress etc) from github and applies it. When you change source code (adding new resources, bumping versions of docker images) operator is appling new changes automatically.
Result
Let’s make sure that everything is working as expected.
You’ll see the output in your console. But pulumi offers you a cool feature, every action with pulumi will be displayed in web UI.
Now let’s go to civo and download kubeconfig:
Connect to kubernetes cluster using kubectl
or any other tool (Lens in my case). And make sure that our desired state (Nginx service) was applied.
The final check is to ensure that Nginx is responding and publically available.
Conclusion
The first time I gave it a try, it took me one day to make everything work.
But can you imagine it? Just one day to run a scalable system in a managed kubernetes cluster following GitOps principles without the help of professionals. With Terraform I can spend an entire day fixing only -
/_
and spaces/tabs issues.
True, it can be implemented in a hundred other ways (for example terraform + helm + argocd). However, as the world progresses, new technologies will come to replace current ones (or business requirements will be changed). At that point, you’ll need to employ less complex codes and tools. That is, faster, easier, less headache for you and cheaper for your company.
Also true, it’s still hard and not a silver bullet: dynamic environments, ingress rules, tls, config and secrets management, fallbacks and rollbacks: something to implement pretty simple and something not. In spite of that, it is my opinion that you should consider this approach if this is your field.
Top comments (0)