DEV Community

Cover image for A Complete Noob’s Guide to Kubernetes
Mike
Mike

Posted on • Originally published at restlessdev.com

A Complete Noob’s Guide to Kubernetes

This article first appeared on the RestlessDev blog.


(Photo By Kaboompics.com)

So you’re interested in deploying your app to Kubernetes but just don’t know how to get started. Been there. I was like you once, 3 days ago.

And now I’m qualified to tell you how to get it done. Well, semi-qualified.

What This Guide Is

This guide is a crash course in telling you what you need to know to get started in Kubernetes for a typical web app. This includes getting the site running with traffic going to it, as well as setting up a worker process for running non-web tasks in the background and a database. It also covers some of the terminology you’ll need to know. I caution you to read what I say here with a grain of salt; I’m describing my experience while also attempting to simplify things where I can so that Mere Mortals can get started with this very complicated and full-featured platform.

What This Guide Is Not

This guide won’t tell you how do deploy a Kubernetes cluster. This will vary a lot depending on which cloud provider you’re using, or if you’re trying to set it up on your own hardware. In my case I used Kube-Hetzner, which was very easy to use once I read through the GitHub readme and set up the right instance types for my desired cluster. It also doesn’t cover the process of containerizing your application. I’ve used Podman to build my images but it should work just as well with other containerization systems.

It also doesn’t get into maintenance of your cluster. To be honest, I’ll be learning that as well, as I’m just setting out on this journey myself.

Before you run, you need to crawl. This guide is will get you crawling like a grad student. That’s good, right? They have to be pretty fast.

Some Background

Like others out there, I’ve been through many different iterations on what the current “best practice” (ick) is when deploying web apps. Bare-metal servers (or just “servers” as we used to call them) gave way to virtual machines, which then gave way to platforms like Heroku, which gave way to Amazon’s version of Heroku, Elastic Beanstalk. Then containerization hit, and every cloud provider had their own way to turn a Docker image into a running website. Recently I’ve been using Amazon’s ECS to deploy sites; it’s been pretty relable once it was up and running, but I wasn’t always crazy about the added costs that seemed to pop up all over the place. A load balancer was how much? What’s a VPC and why is it so expensive? And don’t get me started about RDS pricing.

Kubernetes was something I had heard about, but it seemed so enterprise-y. I’m just a simple country boy, I don’t have an army of consultants to deploy my sites and keep them running. I ran through the minikube tutorial a few years back and got something simple running locally, but to go from that to an actual cluster seemed like a leap I wasn’t prepared to take.

Until last Thursday.

I’ve been working on something that just begged for a Real Deployment, and I wanted to get off of the vendor lock-in treadmill I’ve been on the last few years. Kubernetes answered the call.

Terminology

The first thing that I noticed about Kubernetes is all of the terminology you have to learn. It’s a huge platform, built to handle all different types of workloads with a lot of customization available. Fortunately, you don’t need to learn every little thing just to get started.

Here are the terms you really need to know.


Control Planes and Nodes

Control Planes are the brains of your cluster, and are the thing you actually talk to when using the kubectl command line interface. They generally don’t have to have a ton of resources, because they don’t run anything other than the API and other coordination and management software. Nodes are the part of the cluster that actually run your containers, so the amount of memory and compute power (and storage) they have does impact the capacity of your cluster. Many cloud providers seem to provide their own Control Planes free of charge as long as you use their Nodes; if you’re already on their platform it could work out and provide one less thing for you to maintain, but just keep in mind that they aren’t generally very expensive instance types. This isn’t the reason to choose one platform over another, in my opinion.

Kubectl

Kubectl is the command-line tool used to talk to your cluster. It has a rather simple and consistent interface, and once you get used to it, it’s pretty easy to get things done.

Namespaces

Namespaces are how Kubernetes sorts your various resources into logical buckets. In my case, I have a single namespace for my application and everything lives in it. There is a default namespace, but that’s how we get ants. Be a mensch and create a new namespace please.

Deployments

Deployments are how you instruct Kubernetes to turn your images into actual running containers, called Pods. Each deployment can deploy multiple pods with the same image if you need a load balancing situation. You can also use deployments to launch other services like databases and caching layers using their own images. You basically use deployments to describe what you want the cluster to look like, and then Kubernetes makes sure it happens.

Your running application will consist of several different Deployments; in my case I have one for my application API, one for my application cron runner, one for my database, and one for memcached.

Pods

“Pods” are Kubernetes-speak for running containers. You can connect to them directly to get a command-line interface for debugging, and can access their logs to see whatever information you want. They should generally be treated as ephemeral and non-permanent, outside of any Volumes that they use for persistent storage. The Kubernetes platform will replace Pods with fresh versions whenever it needs to to maintain the integrity of the Deployments.

Volumes

Volumes are used to provide a place to store data that needs to persist over time. They get attached to a Pod (or to several Pods if you want) and will live beyond that Pod‘s life cycle. For things like databases this is essential.

ConfigMaps and Secrets

ConfigMaps and Secrets are Kubernetes’ way of dealing with environment variables. ConfigMaps are a little easier to deal with because they are unencrypted and can be edited directly, so use them for environment variables that aren’t sensitive. Secrets are stored differently internally, but are unencrypted by Kubernetes and sent to Pods as regular environment variables when needed. They are good for putting things like API keys that you want to keep safe. You can have as many ConfigMaps and Secrets as you want, each with their own name; they are assigned to the Deployment in the Deployment‘s YAML file. In my case, my API and cron images are based on the same codebase, so I use the same ConfigMap and Secret for both.

If your images are in a private repository, you’ll also need a Secret to tell Kubernetes how to access them.

Services

Services are how you map ports within your Pods to the outside world. In my case I have a Service for my API but not for the cron runner, which just sorta does its thing in the background.

Ingress

Ingress is a layer on top of Services specifically for HTTP/HTTPS connections. In my case, it’s used for higher-level functions like getting certificates from Let’s Encrypt. You can also use it to route different paths to different Services and things like that, although I don’t use that.

ClusterIssuer

ClusterIssuer is used when registering SSL certificates.


That’s it. Those are the only terms you need to know to deploy an application on Kubernetes.

Deploying an Application

Do you like YAML? I hope so, because you’re going to be using it a lot. Let’s dive in.

Throughout this tutorial, I’ll use the following placeholders to indicate things you should replace with your own variation:

  • <namespace>: The namespace of the application. To make it simple, just try to use a standard lowercase slug-type string. my-namespace would be fine.
  • <app-name>: Kubernetes lets you assign each resource a name which needs to be unique within a namespace. For simplicity, I call my deployments things like my-app-api, my-app-cron, and my-app-db and my services things like my-app-api-service. You’re free to call yours whatever you want, but if you call yours something different just make sure to put the right name in the right place.
  • <*-username> and <*-password>: You may need to reference different usernames and passwords in your code. I’ll replace the * with an identifier to help you know what’s expected.

I’d suggest creating a directory that you’ll use for storing all of your YAML files as you’ll want to reapply them whenever you make changes. Throughout the tutorial I’ll assume you’re in the directory with all of your files.

It’s also worth noting that the names of your YAML files aren’t important, but the content in them is. I happen to call them the same name as the resources they reference, but you might go a different way. It’s cool, we can still be friends.


Side note: We’re going to be using kubectl a lot below, and kubectl needs to know how to talk to your cluster. In my case, my deployment process output a file called kubeconfig.yaml that had all of the relevant information. This file can either be supplied to kubectl as an argument (–kubeconfig) or by setting an environment variable (KUBECONFIG). I’ve opted for the latter, which is why you won’t see this important detail in all the examples.


1. Create the Namespace

First you’ll need to create your namespace. This is the bucket where everything else will live. To do this, make the following YAML:

namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: <namespace>
Enter fullscreen mode Exit fullscreen mode

Now apply this YAML to your cluster with the following command:

kubectl apply -f namespace.yaml
Enter fullscreen mode Exit fullscreen mode

We’re under way!

2. Deploy your Database

In this example, I’m using CloudNative-PG, a Kubernetes-oriented PostgreSQL distribution that has lots of nice features built in.

First we need to create the Secret to store your database credentials.

<app-name>-db-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: <app-name>-db-secret
  namespace: <namespace>
type: Opaque
stringData:
  username: <database-user>
  password: <database-password>
Enter fullscreen mode Exit fullscreen mode

Please remember to quote your password if it contains characters that make YAML unhappy.

Apply it like this:

kubectl apply -f <app-name>-db-secret.yaml
Enter fullscreen mode Exit fullscreen mode

Now you should be able to deploy the database. You know the drill, another YAML.

<app-name>-db.yaml

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: <app-name>-db
  namespace: <namespace>
spec:
  instances: 1
  primaryUpdateStrategy: unsupervised
  storage:
    storageClass: hcloud-volumes
    size: 20Gi
  bootstrap:
    initdb:
      database: <database-name>
      owner: <database-user>
      secret:
        name: <app-name>-db-secret
Enter fullscreen mode Exit fullscreen mode

One thing to notice is that the kind of this YAML file is Cluster. This goes a little beyond the scope of this tutorial, but my understanding is that you can create your own specs for YAML files that extend beyond the build in ones, as done here with the apiVersion referring to that CloudNative PG spec.

Now apply it.

kubectl apply -f <app-name>-db.yaml
Enter fullscreen mode Exit fullscreen mode

So one thing about databases is that you don’t really want them to be easy for bad guys to get to. With this deployment the database is running inside your cluster without an external IP address.

It so happens that you occasionally need to connect to your database, either to set it up initially, or maintain it, or just to run queries or updates. See how it’s doing. For this purpose we’ll want to set up a separate SSH host that we can use to create a tunnel into the database. This SSH server is typically called an SSH Bastion, I think because it sounds cool and a little Medieval. But also because it is a safe place to enter your private cluster.

ssh-bastion.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ssh-bastion
  namespace: <namespace>
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ssh-bastion
  template:
    metadata:
      labels:
        app: ssh-bastion
    spec:
      containers:
      - name: sshd
        image: linuxserver/openssh-server
        env:
        - name: PUID
          value: "1000"
        - name: PGID
          value: "1000"
        - name: TZ
          value: "UTC"
        - name: PASSWORD_ACCESS
          value: "true"
        - name: SUDO_ACCESS
          value: "true"
        - name: DOCKER_MODS
          value: "linuxserver/mods:openssh-server-ssh-tunnel"
        - name: USER_NAME
          value: <bastion-user>
        - name: USER_PASSWORD
          value: <bastion-password>
        ports:
        - containerPort: 2222
---
apiVersion: v1
kind: Service
metadata:
  name: ssh-bastion
  namespace: <namespace>
spec:
  selector:
    app: ssh-bastion
  ports:
  - port: 2222
    targetPort: 2222.
  type: LoadBalancer
Enter fullscreen mode Exit fullscreen mode

You may have noticed we cheated a bit here and had both a deployment and a service defined in the same YAML file. You can do that if you want.

kubectl apply -f ssh-bastion.yaml
Enter fullscreen mode Exit fullscreen mode

Give it a minute to deploy, and let’s check out our progress.

First, let’s see our pods:

kubectl get pods -n <namespace>
Enter fullscreen mode Exit fullscreen mode

You should see one for your database and another for your SSH bastion. Great work!

Now if we want to actually use our bastion, we need to figure out its external IP. To do that we need to look up the services.

kubectl get svc -n <namespace>
Enter fullscreen mode Exit fullscreen mode

You should see a line for ssh-bastion with a value in the External-IP column. Grab that number.

Open up another terminal window and enter this command:

ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no -p 2222 -N -L <open-local-port>:<app-name>-db-rw.<namespace>.svc.cluster.local:5432 <bastion-user>@<bastion-external-ip>
Enter fullscreen mode Exit fullscreen mode

So yeah, there are a lot of placeholders there. should be any local port that you will use to connect to your cluster’s database. I’m assuming you’re already running PostgreSQL on your local machine. If you aren’t, 5432 is fine. is that IP address I told you to grab earlier. The other placeholders you should already have.

As you’ll see, Kubernetes does a lot of magic with DNS inside your cluster. In this case, it creates a hostname for your read-write database at <app-name>-db-rw. Remember that for later when it comes to setting up your application’s ConfigMaps.

Once you run this command, you’ll need to enter your . It’ll look like your terminal hung. It hasn’t, it’s just opening up a tunnel for you and keeping it open.

Now you can go to your preferred PostgreSQL client and connect to your cluster database with the following parameters.

  • Hostname: localhost
  • Port: <open-local-port>
  • Username: <database-user>
  • Password: <database-password>

You can set up your application’s database and do any other configuration you need. When you’re done, kill that SSH tunnel and you’re good to go!

3. Deploy the Application

Now that we have the database working as planned, we can turn to the application.

My application consists of two parts, the API and a separate process that runs cron jobs on a regular schedule to monitor my system and tidy things up when needed. The actual website for my application is hosted outside of Kubernetes; it’s an SPA that will connect to this API to do its work.

Since the API and cron runner both share the same codebase, they have the same environment variables. Step 1 is to add the ConfigMap and Secret that the site code will use.

<app-name>-config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: <app-name>-config
  namespace: <namespace>
data:
  NODE_ENV: production
  LOG_LEVEL: info
  APP_DB_HOST: <app-name>-db-rw
  APP_DB_PORT: "5432"
  APP_DB_NAME: <database-name>
  PORT: "3000"
Enter fullscreen mode Exit fullscreen mode

<app-name>-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: <app-name>-secret
  namespace: <namespace>
type: Opaque
stringData:
  APP_DB_USERNAME: <database-user>
  APP_DB_PASSWORD: <database-password>
Enter fullscreen mode Exit fullscreen mode

Now apply each of these:

kubectl apply -f <app-name>-config.yaml
kubectl apply -f <app-name>-secret.yaml
Enter fullscreen mode Exit fullscreen mode

Quick Detour

Do you use a private image repository to store your images? If so you need to tell Kubernetes how to access it. This can be accomplished through adding a Secret. We’ll do it inline this time, as the Docker Hub YAML format is a little wonky. (For those interested, you need to base64-encode a JSON blob and place it in the YAML)

kubectl create secret docker-registry dockerhub-secret \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username=<docker-username> \
  --docker-password=<docker-access-token> \
  --docker-email=<docker-email> \
  --namespace=<namespace>
Enter fullscreen mode Exit fullscreen mode

With those in place, we’ll start by deploying the cron worker. (Remember to remove the imagePullSecrets key if you aren’t authenticating to Docker Hub)

<app-name>-cron.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <app-name>-cron
  namespace: <namespace>
spec:
  replicas: 1
  selector:
    matchLabels:
      app: <app-name>-cron
  template:
    metadata:
      labels:
        app: <app-name>-cron
    spec:
      imagePullSecrets:
      - name: dockerhub-secret
      containers:
      - name: cron
        image: docker.io/<docker-user>/<app-name>-cron:<tag-name>
        envFrom:
        - configMapRef:
            name: <app-name>-config
        - secretRef:
            name: <app-name>-secret
Enter fullscreen mode Exit fullscreen mode

Apply it like this:

kubectl apply -f <app-name>-cron.yaml
Enter fullscreen mode Exit fullscreen mode

Finally, we’ll deploy the API:

<app-name>-api.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <app-name>-api
  namespace: <namespace>
spec:
  replicas: 2
  selector:
    matchLabels:
      app: <app-name>-api
  template:
    metadata:
      labels:
        app: <app-name>-api
    spec:
      imagePullSecrets:
      - name: dockerhub-secret
      containers:
      - name: api
        image: docker.io/<docker-user>/<app-name>-api:<tag-name>
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef:
            name: <app-name>-config
        - secretRef:
            name: <app-name>-secret
Enter fullscreen mode Exit fullscreen mode

You’ll notice that we have 2 replicas specified in this deployment; this will launch 2 different pods and then load balance them for us for a little redundancy. In this example, our API publishes at port 3000 within the container. You may want to replace this (and other 3000s within this tutorial) with your own value if you do it differently.

And apply it like this:

kubectl apply -f <app-name>-api.yaml
Enter fullscreen mode Exit fullscreen mode

Because this set of Pods will need to listen on a port, we’ll need to add a service. We can do that like this:

<app-name>-api-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: <app-name>-api-service
  namespace: <namespace>
spec:
  selector:
    app: <app-name>-api
  ports:
    - protocol: TCP
      port: 3000      # The port exposed by the service
      targetPort: 3000  # The port your app listens on inside the container
  sessionAffinity: ClientIP
Enter fullscreen mode Exit fullscreen mode

And apply it like this:

kubectl apply -f <app-name>-api-service.yaml
Enter fullscreen mode Exit fullscreen mode

4. Ingress and Domain Pointing

So our site is all deployed. Now we just have to make it visible to the world.

In my case I needed to do two things to make this happen. First, I needed to set up the ClusterIssuer for my cluster.

certissuer-prod.yaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: <email-address>
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            class: traefik
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f certissuer-prod.yaml
Enter fullscreen mode Exit fullscreen mode

Finally, we need to add the Ingress.

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: <app-name>-ingress
  namespace: <namespace>
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  rules:
  - host: api.<app-domain>.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: <app-name>-api-service
            port:
              number: 3000
  tls:
    - hosts:
        - api.<app-domain>.com
      secretName: api-<app-domain>-tls
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f ingress.yaml
Enter fullscreen mode Exit fullscreen mode

Whew, we’re finally almost done. Just one more step: We need to point our domain to the new site. First, though, we need an IP address to point it to.

Run the following command:

kubectl get ingress -n <namespace>
Enter fullscreen mode Exit fullscreen mode

Under the Address column, you should see an IP address. Point an A record at that thang and you’re in business!

Some More Tips

That wasn’t so bad, was it?

Kubectl is also really easy to use once you get the hang of it. For the most part, you can get lists on any part of your cluster like this:

kubectl get <entity> -n <namespace>
Enter fullscreen mode Exit fullscreen mode

where can be pods, configmaps, secrets, deployments, svc or whatever. If you want to inspect something on that list, you can use describe:

kubectl describe <entity> <entity-name> -n <namespace>

# example
kubectl describe pod my-app-api-iwuqe8 -n my-app
Enter fullscreen mode Exit fullscreen mode

When working with pods, you can view their logs like this:

kubectl logs <pod-name> -n <namespace>
Enter fullscreen mode Exit fullscreen mode

Remember that pods are not the same as deployments; you’ll need to get your actual pod instance names from kubectl get pods to connect to the specific instances for logs, exec, and other pod-specific things.

You can get a bash shell on a pod with the following command:

kubectl exec -it <pod-name> -n <namespace> --  bash
Enter fullscreen mode Exit fullscreen mode

If necessary, you can also replace bash above with any other command that’s available on your pod.

If you want to make a small edit to a ConfigMap, you can do it like this:

kubectl edit configmap <configmap-name> -n <namespace>
Enter fullscreen mode Exit fullscreen mode

You can use a similar syntax for other entities, if needed.

You’ll also occasionally find yourself needing to restart your pods after you update a ConfigMap or Secret, or perhaps pushed out an update to your image (if you use the latest tag rather than versioning each release, for example) that you want to have reflected in your Pods. No problemo. You can do a patch to your pod like this:

kubectl patch deployment <deployment-name> -n <namespace> \ 
  -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"configmap-reload-timestamp\":\"$(date +%s)\"}}}}}"
Enter fullscreen mode Exit fullscreen mode

If you’ve pushed up a new tag version of your image and you’d like to patch your deployments to use it, you can use this command:

kubectl set image deployment/<app-name>-api api=docker.io/docker.io/<docker-user>/<app-name>-api:<new-tag> -n <namespace>
Enter fullscreen mode Exit fullscreen mode

If you’d like to update your configuration after changing any of your YAML files, you can also just reapply your updates using kubectl apply -f like you have been. Just make sure that you keep track of any changes you’ve made via edits or patches to make sure you don’t apply an older version of your configuration.

Whew!

Ok, that should be enough for now. Hopefully it wasn’t too overwhelming.

I’ve found that Kubernetes way of conceptualizing an application, once I internalized it, was actually easier than ECS. Each step served a particular purpose, and there wasn’t a lot of unnecessary boilerplate needed just to get things set up. Hopefully you’ll have a similar experience.

Til next time.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.