Seite wählen

NETWAYS Blog

Application Management in Kubernetes

Working with Kubernetes means working with YAML. A lot of YAML. Some might say too much YAML. Your standard deployment usually consists of a workload resource, such as a Deployment or DaemonSet, in order to get some nice little Pods running. Remember, a Pod consists of one or more Containers and a Container is just a Linux process with some glitter. In addition, we might need a ConfigMap in order to store some configuration for our workload.

Now that our Pods are running we need some infrastructure around them. A Service resource will give us a stable name resolution for the IPs of our Containers (called Endpoints). We should also add some Network Policies, which control traffic flow (think Firewall).

Finally, we might have a Secret containing a TLS key/certificate and an Ingress resource, so that the IngressController knows where to find our workload. Remember, an IngressController is a Reverse Proxy.

So that is a lot of YAML we just wrote. a minimal example can easily have more than 200 lines [No citation needed]. Now multiply the environments we are running (i.e. development, staging, production) et voilà even more YAML. In order to reduce this repetition some tools have emerged in the Kubernetes ecosystem.

This article will give an overview of some of these tools. The focus will be on tools that use templates, overlays or functions to generate YAML. Tooling that leverages Custom Resources and Controllers are outside the scope of this article, but will be briefly mentioned in the conclusion. Remember, Controllers are just applications that watch the state of the cluster and make changes if the desired state and the current state differ.

Kustomize

One of the quickest ways to minimize repetition is „Kustomize“, since it is built into kubectl. Kustomize does not use templates or a custom DSL, unlike other solutions, instead it merges and overlays YAML to produce an output we can send to the Kubernetes API. Meaning, it is YAML all the way down.

In this and all other examples we will create a Service resource with a custom port and a namespace, just to keep it simple. To get started with kustomize we need a base YAML manifest, overlays containing the changes that are to be applied on the base and a kustomization.yml in which we describe what will happen:

ls -l

base/  # Contains the YAML with the Kubernetes Resources
overlays/  # Contains the YAML with which we customize the base Resources
kustomization.yml  # Describes what we want to customize

cat base/service.yml
---
apiVersion: v1
kind: Service
metadata:
  name: foobar-service
spec:
  selector:
    app.kubernetes.io/name: Foobar
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 80

cat base/kustomization.yml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
commonLabels:
  app: foobar
resources:
  - service.yml

In `overlays/` we now define what we want to change. The metadata/name of the resources are required so that kustomize knows where to customize. In this example we change the port from 8080 and add a namespace.

cat overlays/dev/kustomization.yml
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
  - ../../base
patchesStrategicMerge:
  - port.yml
namespace: foobar-prod

cat overlays/dev/port.yml
---
apiVersion: v1
kind: Service
metadata:
  name: foobar-service
spec:
  ports:
    - protocol: TCP
      port: 7777
      targetPort: 80

Using `kubectl kustomize` we can now generate the final full manifest with a specific overlay:

kubectl kustomize overlays/dev
...

kubectl kustomize overlays/prod

---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: foobar
  name: foobar-service
  namespace: foobar-prod
spec:
  ports:
  - port: 9999
    protocol: TCP
    targetPort: 80
  selector:
    app: foobar
    app.kubernetes.io/name: Foobar

`patchesStrategicMerge` is one of many built-in generators and transformers that can be used to modify the base resources, an extensive list can be found in the documentation. Kustomize offers a quick and easy way to structure YAML manifests, being built into kubectl lowers the barrier of entry. You can ship the kustomize code directly with your application for example.
However, it can be somewhat cumbersome to work with larger codebases and is not really suited for distribution.

Helm

Helm uses a template language to generate YAML that can then be sent to the Kubernetes API. Either by Helm itself or kubectl. It is widely used in the Kubernetes ecosystem. In the CNCF Cloud Native Survey 2020 63% of the 1,324 respondents named Helm as their preferred method for packaging.

In order to use Helm we need its CLI and a Helm Chart, which we either create ourselves or use an existing one. A Helm Chart consists of one or more templates and a Chart.yaml (containing metadata) and can then be packaged and hosted for others to download or be used locally.

The templates use the Go Template Language in which we have various options to customize data, such as variables, pipelines and functions. For example, we can define variables that can be filled by the user, i.e. „{{ .Values.service.port }}“ can be filled in by the user. This is done by creating a YAML-like file for the template. A values.yaml file serves as a place for defaults.

cat templates/service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: {{ include "foobar.fullname" . }}
  labels:
    {{- include "foobar.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      type: ClusterIP
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "foobar.selectorLabels" . | nindent 4 }}


cat values.yaml  # The defaults
---
service:
  port: 8080

Custom value files can now be created in order to change what is rendered:

cat prod.yaml  # A custom values file
---
service:
  port: 7777

The Helm CLI can then be used to either render the finale YAML manifest or directly talk to the Kubernetes API (creating the resources).

# helm template name-of-the-release path-to-helm-code -f path-to-values-file
helm template foobar-release foobar/ -f foobar/prod.yaml

# Now we can change the port depending on our value file
helm template foobar-release foobar/ -f foobar/prod.yaml | grep port:
   - port: 9999
helm template foobar-release foobar/ -f foobar/dev.yaml | grep port:
   - port: 7777

# Directly create the resources in the cluster
helm install foobar-release foobar/ -f foobar/prod.yaml

Getting started with Helm is usually quite simple and rewarding, it quickly reduces the amount of YAML and offers options for packaging ones applications. In my experience, things get tricky once the parameterized templates start growing. Trying to cover every option/edge-cases and maintaining sanity becomes a balancing act. Nonetheless working with Kubernetes you are likely to run into Helm, maybe even without knowing it.

Terraform

Terraform, a tool to write Infrastructure as Code, can also be used to deploy Kubernetes resources.

Terraform uses a DSL named HashiCorp Configuration Language (HCL) to describe the desired infrastructure, this code is then applied with the Terraform CLI. Storing the state of what is already deployed Terraform knows when to change what in a declarative way. It uses „Providers“ to bundle the logic to talk a specific API (such as Hetzner, AWS, or Kubernetes).

In this example we will use a very simple Terraform project structure, a more complex structure is possible however. In a central Terraform file we will describe the resource we want to create together with its variables:

provider "kubernetes" {
# Here we could specify how to connect to a cluster
}

# Default variables
variable "port" {
  type        = string
  description = "Port of the Service"
  default     = "8080"
}

# The resources template
resource "kubernetes_service" "foobar" {
  metadata {
    name = "foobar-example"
  }
  spec {
    selector = {
      app = "Foobar"
    }
    session_affinity = "ClientIP"
    port {
      port        = var.port
      target_port = 80
    }

    type = "ClusterIP"
  }
}

The previously defined variables can be used to change the values depending on our needs. Within tfvars file we can create different sets of variables for different deployments of the resource:

# tfvars contain the actual values that we want
cat terraform/prod.tfvars
port = "9999"

cat terraform/dev.tfvars
port = "7777"

We can now generate the YAML or talk to the Kubernetes API to directly create the resources.

terraform plan -var-file=dev.tfvars

  # kubernetes_service.foobar will be created
  + resource "kubernetes_service" "foobar" {

terraform plan -var-file=prod.tfvars

  # kubernetes_service.foobar will be created
  + resource "kubernetes_service" "foobar" {

Terraform offers a nice solution when you are already using Terraform to manage infrastructure. Having a one-stop-shop for deploying infrastructure and the applications on top. Introducing Terraform in the tool-chain might be a hurdle though, since tools like Helm might be more suited when it comes to working with Kubernetes.

kpt

kpt uses predefined functions to modify YAML manifests, instead of templates or YAML generators/overlays. These functions – i.e. setters and substitutions – are then applied in a declarative way to a base YAML manifest. All this is combined in a kpt Package, containing a Kptfile (Metadata and declaration of what functions are applied) and YAML manifests of Kubernetes resources.

Kptfiles are written in YAML and are similar to Kubernetes resources. Within the Kptfile we declare a pipeline that contains the functions to be applied, for example:

pipeline:
  mutators:
    - image: gcr.io/kpt-fn/set-labels:v0.1
      configMap:
        app: foobar
  validators:
    - image: gcr.io/kpt-fn/kubeval:v0.3

As we can see, the functions can have different types (mutators, validator, etc) and are inside an OCI Image. This OCI Image contains the code to execute the described function. For example, „set-label“ is a Golang tool which adds a set of labels to resources. You could also write your own.

ls foobar/

Kptfile  # Contains metadata and functions to apply
service.yaml # Kubernetes Manifest

cat Kptfile
---
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
  name: foobar-example
pipeline:
  mutators:
    - image: gcr.io/kpt-fn/apply-setters:v0.2.0
apiVersion: v1

cat service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: foobar-service
  labels:
    app: nginx
  annotations:
    internal.kpt.dev/upstream-identifier: '|Service|default|foobar-service'
spec:
  selector:
    app.kubernetes.io/name: Foobar
  ports:
    - protocol: TCP
      port: 8080 # kpt-set: ${foobar-service-port}
      targetPort: 80

Once we are done with our pipeline definition our kpt package is ready to deploy. We can directly talk to the Kubernetes API and deploy or just render the YAML.

kpt fn render --output unwrap

Packaging in kpt is based on Git forking. Once we are done with our package we can host it on a git server and users fork the package to use it. Users can then define their own pipelines to customize the manifests.

Note that kpt is still in development and subject to many major changes (the examples here are done with version 1.0.0-beta.24). This is currently my main issue with kpt, otherwise I think it is a very neat solution. Having the option to download and fully extend existing packages is very interesting, all while simply working with YAML manifests.

If you want to learn more about kpt, check out the „Kubernetes Podcast from Google“ Episode 99 (https://kubernetespodcast.com/episode/099-kpt/)

Conclusion

The shown Application Management tools are either templates, generators/overlays or functions to generate YAML. Another approach for managing applications is by using Custom Resources in Kubernetes that are managed by a custom Controller. Examples of such tools would be Acorn (https://acorn.io/) or Ketch (https://www.theketch.io/)

Personally, I do not think that these CRD based tools are an optimal solution.

The complexity they introduce might be fine if the benefit is worth it, however, my real issue is with adding an abstraction for the API. Meaning, you never learn to work with the API. While yes, developers might want to focus on the Application not the Infrastructure, it is still very likely that you will have to know how the API works (i.e. for monitoring or debugging). When learning how to write/render correct Kubernetes manifests for your application, you also learn how to work with Kubernetes resources.

Furthermore, some of the CRD based tools might have limitations when creating resources (i.e. the Ketch version at the time of writing requires you to create PVCs). Thus, depending on the solution you still need to learn how to write YAML.

That being said, it is true that template or DSL based tools have a similar downside. It can be quite tricky to cover all use-cases in a parameterized templates and thus be difficult to maintain. I recommend this design-proposal for further reading on the topic:

https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/declarative-application-management.md#parameterization-pitfalls

The application management ecosystem is still a very active and changing part of Kubernetes. We will probably see a couple of solutions come and go until it stabilizes, if ever. Helm will probably stick around due to its current popularity and the momentum this carries.

I personally will keep an eye on kpt, which seems a very nice balance between complexity and modularity, while still working with YAML. Oh how I love YAML⸮

References

Markus Opolka
Markus Opolka
Senior Consultant

Markus war nach seiner Ausbildung als Fachinformatiker mehrere Jahre als Systemadministrator tätig und hat währenddessen ein Master-Studium Linguistik an der FAU absolviert. Seit 2022 ist er bei NETWAYS als Consultant tätig. Hier kümmert er sich um die Themen Container, Kubernetes, Puppet und Ansible. Privat findet man ihn auf dem Fahrrad, dem Sofa oder auf GitHub.

Ansible can talk to your favorite API

Ansible is a powerful opensource config management and deployment tool, which can manage nearly any situtation. In many „DevOp“ scenarios we come across multiple platforms, which we need to combine. Mostly applications provide an REST Api or web connectors to manage resources, jobs and deployments within the product.
Ansible provides various modules which can execute commands at specific APIs, such as the vmware-guest-module to create virtual machines or the jenkins-job-module to manage jobs over the Jenkins API.
In cases where no module is available, we can use the module „uri“.

The module takes several parameters, of which the „url“ is the only required one. For this example I picked an example online API „http://dummy.restapiexample.com/“.
To get a list of all employees we use the method GET on , the header Accept: application/json and register the content.


- name: Make requests to example api
  hosts: localhost
  connection: local
  tasks:
    - name: list employees
      uri:
        method: GET
        url: "http://dummy.restapiexample.com/api/v1/employees"
        return_content: yes
        headers:
          Accept: application/json
      register: response

    - debug:
        msg: "{{ response.content }}"

# Result
TASK [list employees] *************************************************************************
ok: [localhost]

TASK [debug] **********************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "employee_age": "23",
            "employee_name": "test",
            "employee_salary": "46000",
            "id": "12008",
            "profile_image": ""
        }
    ]
}

Now we create a new user in our application, for this we talk to a different url and send a body with our user to create.
When the api accepts JSON I use a little trick to generate a valid json body out of yaml variables with the Ansible filter to_json

For this we create a variable with the same key value structure as the API expects it, in this case the structure looks like this {„name“:“test“,“salary“:“123″,“age“:“23″}.


- name: Make requests to example api
  hosts: localhost
  connection: local
  vars:
    data:
      chris:
        name: chris
        salary: 46000
        age: 27
      jessy:
        name: jessy
        salary: 70000
        age: 30
  tasks:
    - name: create employee
      uri:
        method: POST
        url: "http://dummy.restapiexample.com/api/v1/create"
        return_content: yes
        headers:
          Accept: application/json
        body_format: json
        body: "{{ item.value | to_json }}" //Render valid json from each dictionary in the variable data.
      with_dict: "{{ data }}"
      register: post_content

    - debug:
        msg: "{{ item.content }}"
      with_items: "{{ post_content.results }}"

# Result
ansible-playbook create_user.yaml

PLAY [Make requests to example api] ********************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [localhost]

TASK [create employee] *********************************************************************************
ok: [localhost] => (item={'value': {u'salary': 46000, u'age': 27, u'name': u'chris'}, 'key': u'chris'})
ok: [localhost] => (item={'value': {u'salary': 70000, u'age': 30, u'name': u'jessy'}, 'key': u'jessy'})

With this information given, you can now explore your own favorite API and hopefully reduce your daily tasks as simple Ansible playbooks.

Check out our Blog for more awesome posts and if you need help with Ansible send us a message!

Thilo Wening
Thilo Wening
Manager Consulting

Thilo hat bei NETWAYS mit der Ausbildung zum Fachinformatiker, Schwerpunkt Systemadministration begonnen und unterstützt nun nach erfolgreich bestandener Prüfung tatkräftig die Kollegen im Consulting. In seiner Freizeit ist er athletisch in der Senkrechten unterwegs und stählt seine Muskeln beim Bouldern. Als richtiger Profi macht er das natürlich am liebsten in der Natur und geht nur noch in Ausnahmefällen in die Kletterhalle.

Deployment, aber bitte ohne PXE

Foreman Logo
Es ist schon eine Weile her, dass ich zum Foreman Discovery-Plugin geschrieben hab. Dieses zählt immer noch zu den von mir am meisten genutzten Plugins und wäre mit seinen Neuerungen wie Discovery-Regeln und erweiterbaren Images sicher auch einen neuen Blogpost wert. Aber die häufigste Frage, die mir in dem Zusammenhang gestellt wird, ist „Ich kann/darf unsere DHCP-/PXE-Konfiguration nicht anpassen. Geht das auch ohne PXE?“.
Natürlich kann ich das Discovery-Image auch einfach lokal starten, aber ich möchte eine weitere Lösung in Form des Bootdisk-Plugins für Foreman vorstellen. Dieses bietet zwar nicht die Funktionen wie Discovery-Regeln und darauf basierendes automatisches Deployment, hat aber den Vorteil auch komplett ohne DHCP und TFTP auskommen zu können.
Die Installation ist einfach, da es sowohl für RPM-basierte als auch Deb-basierte Systeme als entsprechendes Paket vorliegt. Nach der Installation einfach noch Foreman durchstarten und es findet sich ein neuer Menü-Paket „Boot disk“ auf der Host-Ansicht. Dieser lässt es zu drei verschiedene Arten Installationsabbilder herunterzuladen. Voraussetzung sind hierfür die iPXE-Templates, die allerdings standardmäßig mitgeliefert werden.
Für Umgebungen in denen zwar DHCP genutzt werden kann, aber weder feste Reservierungen noch eine Anpassung der PXE-Konfiguration möglich ist, bietet sich das generische Installationsabbild an. Dieses enthält in ungefähr einem Megabyte Größe nur den Teil der notwendig ist um sich bei Foreman mit einer MAC-Adresse zu melden und die gewünschte Konfiguration zu erfragen. Danach wird der Installer für das entsprechende Betriebssystem aus dem TFTP-Verzeichnis von Foreman heruntergeladen und die Installation anhand von Kickstart, Preseed, etc. beginnt.
Für Umgebungen in denen gar kein DHCP zur Verfügung steht, können individuelle Boot-Medien heruntergeladen werden, die eine statische Netzwerkkonfiguration für die Installation enthalten. Nachdem von diesem gestartet wurde, wird wieder alles weitere nachgeladen und die Installation beginnt.
In Umgebungen oder auf Hardware bei der auch das Nachladen des Installers nicht klappt, können die individuellen „Full Images“ verwendet werden, welche auch noch Installer und Installationsanweisungen enthalten. Dies ist natürlich die aufwendigste Variante, da das Image bei jeder Änderung neu erstellt werden muss. Wobei sich natürlich die Frage stellt wie oft der Aufwand tatsächlich betrieben werden muss.
Um nun keine Hamster-Käufe bei CD-Rohlingen auszulösen, handelt es sich bei den Installationsabbildern um hybride Images, die auch direkt auf einen USB-Stick übertragen werden können. Somit kann jeder Foreman-Nutzer dieses Plugin schnell und einfach ausprobieren und wer noch kein Foreman benutzt, aber keine Lust mehr auf manuelle Installationen hat, sollte diesem ein Chance geben. Wer sich bei der Einrichtung helfen lassen möchte, findet diese natürlich bei uns! 😉

Dirk Götz
Dirk Götz
Principal Consultant

Dirk ist Red Hat Spezialist und arbeitet bei NETWAYS im Bereich Consulting für Icinga, Puppet, Ansible, Foreman und andere Systems-Management-Lösungen. Früher war er bei einem Träger der gesetzlichen Rentenversicherung als Senior Administrator beschäftigt und auch für die Ausbildung der Azubis verantwortlich wie nun bei NETWAYS.

Reminder für das morgige Foreman / OpenNebula Webinar

opennebula_foreman Ich möchte noch einmal alle auf das morgige Webinar zum Thema Foreman: OpenNebula orchestrieren aufmerksam machen. Dabei wollen wir neben dem von NETWAYS entwickelten Modul Foreman-One auch auf Foreman und OpenNebula eingehen.
Ziel wird es aber sein zu demonstrieren, wie über das Foreman-Webinterface eine virtuelle Maschine erzeugt werden kann.
Wer sich für das Webinar noch registrieren will hat bis morgen Früh um 10:30 Uhr noch Gelegenheit dazu. In unserem Webinar-Archiv finden sich bereits einige Webinare zum Thema OpenNebula und Foreman (im Zusammenspiel mit Puppet).
Bis morgen früh um 10:30 Uhr!

Christian Stein
Christian Stein
Manager Sales

Christian kommt ursprünglich aus der Personalberatungsbranche, wo er aber schon immer auf den IT Bereich spezialisiert war. Bei NETWAYS arbeitet er als Manager Sales und berät unsere Kunden in der vertrieblichen Phase rund um das Thema Monitoring. Gemeinsam mit Georg hat er sich Mitte 2012 auch an unserem Hardware-Shop "vergangen".

Wir sind auf der CeBIT 2013… wie immer halt.

Jaja… alle Jahre wieder CeBIT mit NETWAYS. Was soll man da noch großartig berichten?
Das sind genau die Blogposts, die ich aus tiefstem Herzen hasse, weil man im Grunde nur die CeBIT-Ankündigungs-Posts der vergangenen 378 Jahre kopieren muss, Jahreszahlen und Standnummern aktualisiert  und feddich is die Laube.
Das ist allerdings ausgesprochen langweilig. Sowohl für den Leser, als auch für den Menschen der schreibt. Wenn man den Anspruch hat wenigstens ein ganz kleines bisschen Abwechslung in den Blog zu bringen kann es sogar geradezu nervtötend sein derartige Standartmitteilungen verfassen zu müssen.
Okay – es gibt dann oft noch einen Absatz in dem die latest News stehen, über die man sich auf der Messe von Angesicht zu Angesicht informieren kann und die sind tatsächlich in jedem Jahr anders – ein Lichtblick auch für den Schreiber, der dieses Jahr zum Beispiel so schöne Dinge in die sprachlich angemessene Form gießen könnte wie:
Neben vielen Neuerungen in den Bereichen Open Source Monitoring auf Basis von Icinga und den entsprechenden Addons, werden wir auch Demos zu Puppet und Puppet Enterprise für Sie dabei haben. Auf die Präsentation einer von uns entwickelten  CMDB-Lösung freuen wir uns dabei ganz besonders.
So können Sie sich bei uns von Configuration Management über Deployment und Virtualisierung bis hin zum Monitoring über die aktuellsten Entwicklungen im Bereich Open Source Datacenter Solutions informieren und die verschiedenen Systeme in Demos kennenlernen.
Außerdem könnte ich noch schreiben, dass wir dieses Jahr zum ersten Mal mit OpenNebula im Schlepptau in Hannover sind und deshalb auch Neuigkeiten von unseren Partnern aus erster Hand mitbringen.
Ja, aber dann kommt normaler Weise auch schon wieder ein Absatz, der total langweilig ist und in dem steht wann man uns wo finden kann. Dieses Jahr z.B.  am:
05. – 09. März 2013
Halle 6
Block F16

Stand 330
Den Absatz hasse ich ganz besonders, weil hier kein Spielraum für literarische Extravaganzen und Spielereien ist und ich mich an der Stelle immer ganz besonders unterfordert  fühle.
Und entweder davor oder danach steht dann, dass man gerne auch einen Termin für ein Gespräch auf der CeBIT mit uns vereinbaren kann und sich dafür einfach nur mit entsprechendem Wunschtermin an unser Sales-Team wendet.
Außerdem wird im gleichen Atemzug auch immer darauf hingewiesen, dass wir gerne Freitickets zur Messe vergeben, die man ebenfalls per Mail anfordern kann so lange unser wohl kalkulierter Vorrat reicht.
Und ich mag diese CeBIT-Grafik nicht, die man dann einbauen muss. Schaut euch das doch mal an! Dieses agressive CeBIT-rot! Nicht zum aushalten!
CeBIT
Ganz ehrlich… wenn mich jemand fragen sollte, ob ich dieses Jahr den nervigen CeBIT-Post schreibe, dann werde ich mich einfach weigern.