Kubernetes without Ansible is just wrong

Kubernetes without Ansible is just wrong
Photo by Matthew Waring / Unsplash

Introduction

If you're reading this, chances are you know Kubernetes and you've at least heard of Ansible.

While Kubernetes is a fantastic platform for managing containers, making it easier for you to deploy, scale, and manage your applications, it does come with limitations. As your cluster grows, so does the complexity of managing it. That's where Ansible comes in.

In this article I'm going to go into why using Kubernetes without Ansible is a recipe for disaster. Then I'll show you my proposal for a smoother process. Let's get into it.

Kubernetes Without Ansible

Kubernetes is great, but without some guidelines, it can quickly turn into a hot mess. How? Let's review a few approaches people use with their Kubernetes configs:

one-file-with-everything.yaml

This one is pretty self explanatory. You separate each Kubernetes resource with `---` and slap it all in one big file. When you want to deploy you run kubectl apply -f one-file-with-everything.yaml . When you want to remove it you run the same but with delete. It's all or nothing with this one.

Separate YAML files with Kustomize

You may have a folder structure like so:

project/
├─── deployment.yaml
├─── service.yaml    
├─── pvc.yaml    
├─── ...
└─── kustomize.yaml

Kustomize is useful and pulls everything together into one bundle. You can run kubectl apply -k . to deploy everything or you can remove or update individual files as normal too. Options are great.

The Problem

The one file approach is very restrictive and ... well it also gets really long so editing it is confusing and not a good experience. Using Kustomize is a step forward because it allows you more flexibility.

Where does the problem come in?

Well, you should probably put your application in a namespace right? That's a pretty simple thing so let's just make a readme file to keep track of that.

Now lets say you are using a private docker repository for your image. You'll need to save those credentials somewhere secure and document the multi-step process to add those docker credentials as a Secret in your namespace.

Now your team wants to use a Secrets Manager like Doppler or AWS Secrets Manager. Ok, that's more complicated still. And even if you document it all, now it will take a good while to run through all the steps and verify you've completed it successfully without fat fingering something. What happens when the application is no longer needed? Are you going to remember to delete any dangling resources like the secrets manager operator that lives outside that namespace? Probably not.

And this is just one example. What about backups? You may have that documented somewhere else but is it clear to the whole team where to look if something goes wrong? What if you need to perform a series of tasks ahead of deployments? All of these tasks become incredibly complex and time-consuming without a proper automation plan.

Ansible's role in Kubernetes

Kubernetes is great at adding intelligence to Docker and Ansible is excellent at simplifying and standardizing your usage of Kubernetes. With Ansible, you can easily manage all your Kubernetes resources, from deployments and updates to scaling and rollbacks.

Ansible is a bit like a bash script but it has some real superpowers. With its simple, human-readable language and easy-to-use modules, even someone with minimal automation experience can start using it immediately. Plus, it has a vast community of developers constantly creating and sharing new modules, making it an ever-growing tool that always stays relevant.

Another great advantage is Idempotency. If a step is already in the desired state, it won't re-run it.

It's also a lot easier than you think to get going too, so why not try?

My Suggested Ansible Pattern

Now we've seen how installing and uninstalling Kubernetes configs yourself can lead to problems and we've heard a little about Ansible. Here is the new structure for your configs that I'd propose:

Proposed Structure

project/
├─── deployment.yaml # k8s
├─── service.yaml    # k8s
├─── ...
└─── ansible/
     ├── install.yaml
     ├── uninstall.yaml
     ├── backup.yaml              # optional
     └── dangerous-uninstall.yaml # optional

You would have your standard Kubernetes configs in a folder as usual and just add a subfolder with your Ansible playbooks for installing and uninstalling the application. Those are neatly foldered away from your k8s files and you could apply any config or do them all using the Ansible playbooks.  Another benefit is everything lives happily under the project folder, and this could potentially live within a larger repository with more of your configs.

Don't need this application anymore? Run the uninstall and just delete the folder and it's fully gone.

There are other playbooks you may find useful but here are four I think should be standard:

  • Install: All encompassing script for everything needed to install the project, including making the namespace, adding secrets like Doppler, and Docker credentials for private repos
  • Uninstall: Removes everything, with the exception of persistent data that cannot be automatically backed up during uninstall
  • Backup: Creates a backup of data that should be persisted. This backup should be timestamped
  • Dangerous Uninstall: Removes persistent data that is not automatically backed up

Examples

As an example of using Ansible to manage applying or deleting your kubernetes configs, I've converted the configs I showed you for Uptime Kuma to fit this model. You can find the example project (and try running it) from my repo.

Here's what the ansible/install.yaml looks like:

- hosts: localhost
  collections:
    - kubernetes.core
  vars:
    - namespace: uptime-kuma
  tasks:
    - name: (uptime-kuma) Create namespace
      kubernetes.core.k8s:
        name: '{{ namespace }}'
        api_version: v1
        kind: Namespace
        state: present

    - name: (uptime-kuma) Add Basic Auth
      k8s:
        definition:
          api_version: v1
          kind: Secret
          metadata:
            namespace: '{{ namespace }}'
            name: basic-auth-secret
          state: present
          type: Opaque
          data:
            auth: "{{ lookup('ansible.builtin.template', '../auth') | b64encode }}"

    - name: (uptime-kuma) Create storage volume
      kubernetes.core.k8s:
        state: present
        namespace: '{{ namespace }}'
        src: ../pv.yaml

    - name: (uptime-kuma) Attach storage volume
      kubernetes.core.k8s:
        state: present
        namespace: '{{ namespace }}'
        src: ../pvc.yaml

    - name: (uptime-kuma) Deploy ingress
      kubernetes.core.k8s:
        state: present
        namespace: '{{ namespace }}'
        src: ../ingress.yaml

    - name: (uptime-kuma) Deploy service
      kubernetes.core.k8s:
        state: present
        namespace: '{{ namespace }}'
        src: ../service.yaml

    - name: (uptime-kuma) Deployment
      kubernetes.core.k8s:
        state: present
        namespace: '{{ namespace }}'
        src: ../deployment.yaml
ansible/install.yaml

We are running this on localhost, since that's where you should have your kubeconfig setup and pointed to your cluster.

From there you'll see references to kubernetes.core.k8s. That's the Ansible module that makes it a snap to work with kubernetes. A great resource for knowing what you can do with it can be found from the docs.

The first task up is to create the namespace for the project and it's pretty straightforward.

The second task is a bit more complex. It creates a basic auth secret on the project by loading the values from the auth file and encoding it with base64.

The following tasks are pretty similar and just ensure the various configs have been applied. The state of present lets Ansible know we want them to be there.

First time running ansible/install.yaml it should look similar to this:

Ansible is idempotent, so you can run install multiple times and it should only apply configs that are missing. The first run will show things are changing, which means they've been applied. Here's what it looks like if you run the install again.

This run it only verifies the configs you want are deployed, and will only install the ones that are missing.

The ansible/uninstall.yaml is almost the install in reverse, but also with state set to absent so Ansible knows to remove the configs if it sees them present.

- hosts: localhost
  collections:
    - kubernetes.core
  vars:
    - namespace: uptime-kuma
  tasks:
    - name: (uptime-kuma) Remove Basic Auth
      kubernetes.core.k8s:
        state: absent
        kind: Secret
        namespace: '{{ namespace }}'
        name: basic-auth-secret

    - name: (uptime-kuma) Undeploy ingress
      kubernetes.core.k8s:
        state: absent
        namespace: '{{ namespace }}'
        src: ../ingress.yaml

    - name: (uptime-kuma) Undeploy service
      kubernetes.core.k8s:
        state: absent
        namespace: '{{ namespace }}'
        src: ../service.yaml

    - name: (uptime-kuma) Deployment
      kubernetes.core.k8s:
        state: absent
        namespace: '{{ namespace }}'
        src: ../deployment.yaml

    - name: (uptime-kuma) Remove storage claim
      kubernetes.core.k8s:
        state: absent
        namespace: '{{ namespace }}'
        src: ../pvc.yaml
ansible/uninstall.yaml

Summary

In conclusion, using Kubernetes without Ansible is the wrong way to go about it. Make things easier for yourself before it gets completely out of hand trying to document how everything should be installed or uninstalled. It's easier to just document it in a repeatable script, that's easy to read and even easier to run.

While Kubernetes is a fantastic platform for managing containers, the complexity of managing your application increases as it grows. Ansible is the key to a successful Kubernetes journey, providing the automation and management tools you need to make your application run smoothly.

With Ansible, you can automate tasks, reduce human error, ensure consistency, and scale the number of applications you're managing.

In short, Kubernetes and Ansible are the perfect pair. Kubernetes provides the platform, and Ansible delivers the tools to manage it effectively.