From their repository to your cluster - part 3

Running in your cluster

integration

In parts 1 and 2, we went through the process of choosing a tool and building it in a way we can easily maintain and deploy it. In this part, we’ll finally begin running it in our target (local in this case) cluster.

There are some parts about this specific tool’s configuration that we won’t go into - this is more about how to get something running in Kubernetes than it is about this particular tool.

You’ll remember that this is a simple tool designed to be run on a schedule. This means that in its current state, we need to find a way to schedule this task instead of running it as a long running process.

Fortunately, Kubernetes has a CronJob feature. This uses the familiar crontab job definition format, and creates jobs on a given scheduler. It's important to note that jobs will be created in the timezone of the kube-controller manager.

Namespaces

If you're not already familiar with Kubernetes namespaces, you should definitely read this page. Namespaces are an important component in separating users within a single Kubernetes environment, or deploying applications into a self contained environment. They'll become more important as we expand this series and look at tools like cert-manager. For this example, we'll use a namespace called cloudflare-ddns-client, based on the application name.

You can create this with kubectl create ns cloudflare-ddns-client. Once created, you need to ensure that it's explicitly set as a flag to all Kubernetes commands: —namespace=cloudflare-ddns-client or -n cloudflare-ddns-client or make it your default context by doing: kubectl config set-context --current --namespace=cloudflare-ddns-client

Application Components

This application requires a configuration file containing a secret (credentials to modify a CloudFlare DNS entry) to run, and the CronJob definition mentioned earlier. A more complex application would use Deployments for long running services, Services to allow components to interact with each other, and Ingress to allow connections to your environment. An example of this kind an application will be covered in an upcoming post.

Let's look at each of the components we're using for this simple example in more detail.

Secrets and credentials

The secret in this case is a token that is able to modify a DNS zone in Cloudflare - Cloudflare allows you to create multiple different tokens to ensure fine-grained access to their API, and in this case we've created a token that is allowed to list and read and write DNS entries in our domain.

Secrets are sensitive objects, and much like any other application environment, these must be protected. We would encourage you to read Aqua's page on this.

For production credentials, you should ensure that your cloud provider enables you to automatically encrypt the etcd storage using one of their services. Alternatively, you could consider a cloud-agnostic solution such as HashiCorp Vault or manually integrate with your cloud provider tooling to do this.

For this example, we'll use etcd.

Kubernetes secret contents need to be base64-encoded if you're going to manually create secrets, and it's important to note that you should use echo -n when creating these as a newline in the encoded secret can be a challenge to diagnose.

So assuming that our Cloudflare token is 'abc1234', we'd encode this with echo -n 'abc1234' | base64 and copy the resulting string. Our cloudflare-ddns-client-secret.yaml file will now be as follows:

---
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-token
type: Opaque
data:
  password: YWJjMTIzNA==

It's important to note that this is currently a file in your filesystem that contains data that is sensitive and it should not be commited to an SCM!

Configuration files

Kubernetes allows you to create a ConfigMap containing your configuration information, and then mount this so that it is treated like a file in pods. This way you can manage your config data in a way that doesn't require persistent storage across all hosts that pods will run on, but also not have to bake this into your images.

Our cloudflare-ddns-client-configmap.yaml will be quite simple as the application doesn't require much complex configuration.

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: cloudflare-ddns
  namespace: cloudflare-ddns-client
data:
  .cloudflare-ddns: |+
    [Cloudflare DDNS]auth_type = tokendomains = myhost.mydomain.com

Task definition

Last, but certainly not least is our task definition. In this case it will be a CronJob that runs a task on a scheduled basis, but it could be a Deployment or any other supported Kubernetes type.

For this example, we'll use this content in cloudflare-ddns-client-cronjob.yaml

---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: cloudflare-ddns-client-cronjob
spec:
  schedule: '0 */4 * * *'
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: cloudflare-ddns-client-cronjob
              image: wtpascoe/cloudflare-ddns-client:2.0
              volumeMounts:
                - name: config-cloudflare-ddns-client
                  mountPath: /home/cloudflare-ddns-client/.cloudflare-ddns
                  subPath: '.cloudflare-ddns'
              env:
                - name: API_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: cloudflare-token
                      key: password
          volumes:
            - name: config-cloudflare-ddns-client
              configMap:
                name: cloudflare-ddns
          restartPolicy: Never

There are a few important things to note here. Firstly, you can see that we reference both the secret and the configmap we defined earlier.

Secret

The secret is used to set an environment variable at runtime, which the application will then use for authentication. This snippet takes the value of the password key from the cloudflare-token secret and sets it as the API_TOKEN environment variable.

env:
- name: API_TOKEN
    valueFrom:
    secretKeyRef:
        name: cloudflare-token
        key: password

ConfigMap volume and use

The ConfigMap is defined as a volume which can then be mounted into a running pod. There are many different types of volumes available, but this uses configuration data instead of requiring persistent storage shared across all nodes.

This snippet defines a volume named config-cloudflare-ddns-client:

volumes:
  - name: config-cloudflare-ddns-client
configMap:
  name: cloudflare-ddns

With the volume defined, we can then include it in our container definition so that the content of the configmap appears in the filesystem where the application expects it to be:

containers:
  - name: cloudflare-ddns-client-cronjob
    image: wtpascoe/cloudflare-ddns-client:2.0
    volumeMounts:
      - name: config-cloudflare-ddns-client
        mountPath: /home/cloudflare-ddns-client/.cloudflare-ddns
        subPath: '.cloudflare-ddns'

Putting it all together

Deployment

We'll now use the 3 files created above to deploy this application and to verify that it's running. I've set my context to cloudflare-ddns-client but if you haven't, you should include --namespace cloudflare-ddns-client or kubectl -n cloudflare-ddns-client in each command. You may prefer this as it's more explicit and makes it clear which namespace you're impacting.

kubectl -n cloudflare-ddns-client create -f cloudflare-ddns-client-secret.yaml
kubectl -n cloudflare-ddns-client create -f cloudflare-ddns-client-configmap.yaml
kubectl -n cloudflare-ddns-client create -f cloudflare-ddns-client-cronjob.yaml

If you wanted to use (apply) all of the files in a folder instead of one at a time as above, you could instead run

kubectl -n cloudflare-ddns-client apply -f

Verification

Once you've done this, you can check the components in the cluster with kubectl -n cloudflare-ddns-client get all

Immediately after running, my output looks like this:

NAME                                           SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob.batch/cloudflare-ddns-client-cronjob   0 */4 * * *   False     0        <none>          29s

Wait, what? Where are the pods? Where are the secrets? Where are the ConfigMaps? We told kubectl to get all. There's a lengthy discussion about this on GitHub, but suffice to say that get all does not return everything. And there are no pods because the CronJob has not yet run.

We can get specific items with commands like kubectl -n cloudflare-ddns-client get secret or kubectl -n cloudflare-ddns-client get configmap. More importantly, we can look for actions and failures with kubectl -n cloudflare-ddns-client get events.

Forcing things to run instead of waiting for the schedule

There won't be much here until we've run for the first time, so let's speed the world up. We can trigger a job to run immediately by creating a job from a CronJob definition. In this example, let's run kubectl -n cloudflare-ddns-client create job --from=cronjob.batch/cloudflare-ddns-client-cronjob cloudflare-ddns-client-manual-001. --from refers to the job definition we've already created and the last argument is the name of this job - you can see this in the output when you run kubectl -n cloudflare-ddns-client get all now.

% kubectl -n cloudflare-ddns-client get all
NAME                                          READY   STATUS      RESTARTS   AGE
pod/cloudflare-ddns-client-manual-001-zqbbk   0/1     Completed   0          60s

NAME                                          COMPLETIONS   DURATION   AGE
job.batch/cloudflare-ddns-client-manual-001   1/1           6s         60s

NAME                                           SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob.batch/cloudflare-ddns-client-cronjob   0 */4 * * *   False     0        <none>          8m6s

There's now a job and a pod named based on the last argument we used above, and we can check the pod logs to see how things ran with kubectl -n cloudflare-ddns-client logs cloudflare-ddns-client-manual-001-zqbbk

Cleanup

Our application is configured, our manual run succeeded, so all that's left to do is some housekeeping and we're done. We can delete the job that ran with kubectl -n cloudflare-ddns-client job cloudflare-ddns-client-manual-001, and you'll see that this also deletes the associated pod.

Conclusion

In this series we've gone from needing a tool through selection, building and versioning to deployment into your environment. We hope you found this useful, and if you have questions or ideas for future content, please reach out to us on Twitter

Credits

Photo by Shane Aldendorff on Unsplash

About Us

DoneOps was founded by a group of seasoned DevOps personnel.

Social Media