Skip to content

Latest commit

 

History

History
635 lines (523 loc) · 14.7 KB

simple-kubernetes-example.md

File metadata and controls

635 lines (523 loc) · 14.7 KB

Walkthrough

Let's do something useful: generate some Kubernetes-bound YAML. We'll create the resource descriptors for a tiny Pod running nginx along with a Service.

We will recreate the hello-kerbi project from the examples folder. Read along or clone the folder to play with it locally. The directory structure by the end of the tutorial will be:

├───kerbifile.rb
├───pod-and-service.yaml.erb
├───consts.rb
├───helpers.rb
├───values
│   ├───values.yaml
│   └───production.yaml

1. Basic Pod & Service

Starting simple, almost with static YAML, only interpolating release_name:

{% tabs %} {% tab title="kerbifile.rb" %}

class HelloWorld < Kerbi::Mixer
  def mix
    push file("pod-and-service")
  end
end

Kerbi::Globals.mixers << HelloWorld

{% endtab %}

{% tab title="pod-and-service.yaml.erb" %}

apiVersion: v1
kind: Pod
metadata:
  name: "hello-kerbi"
  namespace: <%= release_name %>
spec:
  containers:
    - name: main
      image: nginx

---

apiVersion: v1
kind: Service
metadata:
  name: "hello-kerbi"
  namespace: <%= release_name %>
spec:
  selector:
    app: "hello-kerbi"
  ports:
    - port: 80

{% endtab %}

{% tab title="Output" %} {% code title="$ kerbi template demo ." %}

apiVersion: v1
kind: Pod
metadata:
  name: hello-kerbi
  namespace: demo
spec:
  containers:
  - name: main
    image: nginx

---

apiVersion: v1
kind: Service
metadata:
  name: hello-kerbi
  namespace: demo
spec:
  selector:
    app: hello-kerbi
  ports:
  - port: 80

{% endcode %} {% endtab %}

{% tab title="Kubernetes" %}

$ kerbi template demo . > manifest.yaml
$ kubectl apply -f manifest.yaml

{% endtab %} {% endtabs %}

A few Observations:

release_name gets its value - demo - from our command line argument

file("pod-and-service") omits the .yaml.erb extension and still works.

push ``file() is just passing an Array<Hash> returned by file(), explained here.

require ``"kerbi" is nowwhere to be found. That's normal, the kerbi executable handles it.

2. Adding Values

The whole point of templating engines is to modulate the output based on information passed at runtime. Like in Helm, the primary mechanism for this is values.

{% tabs %} {% tab title="values/values.yaml" %}

pod:
  image: nginx
service:
  type: ClusterIP

{% endtab %}

{% tab title="pod-and-service.yaml.erb" %}

apiVersion: v1
kind: Pod
metadata:
  name: "hello-kerbi"
  namespace: <%= release_name %>
spec:
  containers:
    - name: main
      image: <%= values[:pod][:image] %>

---

apiVersion: v1
kind: Service
metadata:
  name: "hello-kerbi"
  namespace: <%= release_name %>
spec:
  type: <%= values[:service][:type] %>
  selector:
    app: "hello-kerbi"
  ports:
    - port: 80

{% endtab %}

{% tab title="values/production.yaml" %}

service:
  type: LoadBalancer

{% endtab %} {% endtabs %}

Running kerbi template default . yields the output you would expect. We can also choose to also apply our production.yaml by using -f flag in the command:

$ kerbi template demo . -f production.yaml

This makes our Service become a LoadBalancer:

kind: Service
#...
spec:
  type: LoadBalancer
  #...

Finally, we can use --set to achieve the same effect, but without creating a new values file:

$ kerbi template demo . --set service.type=LoadBalancer

3. Patching After Loading

Suppose we want to add labels and annotations to our Pod and Service. Because this will happen a lot, we can use patched_with to patch several resources in one shot, rather than per-resource.

{% tabs %} {% tab title="kerbifile.rb" %}

class HelloWorld < Kerbi::Mixer
  def mix
    patched_with file("common/metadata") do
      push file("pod-and-service")
    end
  end
end

Kerbi::Globals.mixers << HelloWorld

{% endtab %}

{% tab title="common/metadata.yaml" %}

metadata:
  annotations:
    author: person
  labels:
    app: hello-kerbi

{% endtab %}

{% tab title="Output" %}

apiVersion: v1
kind: Pod
metadata:
  name: hello-kerbi
  namespace: demo
  annotations:
    author: person
  labels:
    app: hello-kerbi
spec:
  containers:
  - name: main
    image: nginx

---

apiVersion: v1
kind: Service
metadata:
  name: hello-kerbi
  namespace: demo
  annotations:
    author: person
  labels:
    app: hello-kerbi
spec:
  type: ClusterIP
  selector:
    app: hello-kerbi
  ports:
  - port: 80

{% endtab %} {% endtabs %}

Notice how patched_with() method accepts the same Hash | Array<Hash> as push() that we have been using up to now. This means you can use the same methods to construct arbitrarily complex patches.

4. Getting DRY & Organized

The great think about Kerbi is that it's just a normal Ruby program! You can do whatever makes sense for your project, such as DRYing up our ERB. We'll do three such things to inspire you:

  1. Start using an outer namespace - HelloKerbi - to prevent any name collisions
  2. Create a module to store constants - HelloKerbi::Consts
  3. Create a helper module we use in our template files - HelloKerbi::Helpers

{% tabs %} {% tab title="Improved kerbifile.rb" %} {% code title="kerbifile.rb" %}

require_relative 'consts'
require_relative 'helpers'

module HelloKerbi
  class MainMixer < Kerbi::Mixer
    include Helpers

    def mix
      patched_with file("common/metadata") do
        push file("pod-and-service")
      end
    end
  end
end

Kerbi::Globals.mixers << HelloKerbi::MainMixer

{% endcode %} {% endtab %}

{% tab title="Consts & Helpers" %} {% code title="consts.rb" %}

module HelloKerbi
  module Consts
    APP_NAME = "hello-kerbi"
  end
end

{% endcode %}

{% code title="helpers.rb" %}

module HelloKerbi
  module Helpers
    def img2alpine(img_name)
      return img_name if img_name.include?(":")
      "#{img_name}:alpine"
    end
  end
end

{% endcode %} {% endtab %}

{% tab title="Updated template File" %}

apiVersion: v1
kind: Pod
metadata:
  name: <%= HelloKerbi::Consts::APP_NAME %>
  namespace: <%= release_name %>
spec:
  containers:
    - name: main
      image: <%= img2alpine(values[:pod][:image]) %>

---

apiVersion: v1
kind: Service
metadata:
  name: <%= HelloKerbi::Consts::APP_NAME %>
  namespace: <%= release_name %>
spec:
  type: <%= values[:service][:type] %>
  selector:
    app: <%= HelloKerbi::Consts::APP_NAME %>
  ports:
    - port: 80

{% endtab %} {% endtabs %}

5. Interactive Console

Another thing that sets Kerbi apart is the ability to touch your code. Using the kerbi console command, we'll open up an IRB session do what we please with our code:

$ kerbi console

irb(kerbi):001:0> HelloKerbi
=> HelloKerbi

irb(kerbi):002:0> values
=> {:pod=>{:image=>"nginx"}, :service=>{:type=>"ClusterIP"}}

irb(kerbi):004:0> mixer = HelloKerbi::Mixer.new(values)
=> #<HelloKerbi::Mixer:0x000056438ba4c3b8 @output=[], @release_name="default", @patch_stack=[], @values={:pod=>{:image=>"nginx"}, :service=>{:type=>"ClusterIP"}}>

irb(kerbi):005:0> mixer.run
=> {:apiVersion=>"v1", :kind=>"Pod", :metadata=>{:name=>"hello-kerbi", :namespace=>"default", :annotations=>{:author=>"xavier"}, :labels=>{:app=>"hello-kerbi"}}, :spec=>{:containers=>[{:name=>"main", :image=>"nginx:alpine"}]}}
{:apiVersion=>"v1", :kind=>"Service", :metadata=>{:name=>"hello-kerbi", :namespace=>"default", :annotations=>{:author=>"xavier"}, :labels=>{:app=>"hello-kerbi"}}, :spec=>{:type=>"ClusterIP", :selector=>{:app=>"hello-kerbi"}, :ports=>[{:port=>80}]}}

6. State

You need a way to keep track of the values you use to generate your latest manifest. If you applied a templated manifest that used --set backend.image=2 and then later --set frontend.image=2, then the second invokation would revert backend.image to its default from values.yaml. Big problem.

Kerbi has an inbuilt state mechanism that lets you store the values it computes during $ kerbi template, and then retrieve those values again. Kerbi uses a ConfigMap (or Secret if you tell it to)in your cluster to store the data. Tell Kerbi to create that ConfigMap:

$ kerbi release init demo

namespaces/demo: Created
demo/configmaps/kerbi-db: Created

We now have one release:

{% tabs %} {% tab title="First Tab" %}

$ kerbi release list

NAME BACKEND   NAMESPACE RESOURCE      STATES LATEST
demo ConfigMap demo      kerbi-demo-db 0

{% endtab %}

{% tab title="Second Tab" %}

$ kerbi release status demo

1. Create Kubernetes client: Success
2. List cluster namespaces: Success
3. Target namespace demo exists: Success
4. Resource demo/cm/kerbi-demo-db exists: Success
5. Data from resource is readable: Success

{% endtab %} {% endtabs %}

6. Writing State

Now let's template again, but with a new option --write-state:

{% tabs %} {% tab title="Template Command" %}

$ kerbi template demo . \
        --set pod.image=ruby \
        --write-state @new-candidate \
        > manifest.yaml

{% endtab %}

{% tab title="manifest.yaml" %}

apiVersion: v1
kind: Pod
metadata:
  name: hello-kerbi
  namespace: demo
  annotations:
    author: person
  labels:
    app: hello-kerbi
spec:
  containers:
  - name: main
    image: nginx:alpine

---

apiVersion: v1
kind: Service
metadata:
  name: hello-kerbi
  namespace: demo
  annotations:
    author: person
  labels:
    app: hello-kerbi
spec:
  type: ClusterIP
  selector:
    app: hello-kerbi
  ports:
  - port

{% endtab %} {% endtabs %}

Let's use Kerbi's state inspection commands: list and show:

{% tabs %} {% tab title="List" %}

$ kerbi state list demo

TAG                 REVISION MESSAGE  ASSIGNMENTS  OVERRIDES  CREATED_AT
[cand]-brave-toner                    2            1          4 seconds ago

{% endtab %}

{% tab title="Show" %}

$ kerbi state show demo @candidate

--------------------------------------------
 TAG              [cand]-brave-toner
--------------------------------------------
 RELEASE          demo
--------------------------------------------
 REVISION
--------------------------------------------
 MESSAGE
--------------------------------------------
 CREATED_AT       2022-04-13 10:21:50 +0100
--------------------------------------------
 VALUES           pod.image: ruby           
                  service.type: ClusterIP
--------------------------------------------
 DEFAULT_VALUES   pod.image: nginx          
                  service.type: ClusterIP
--------------------------------------------
 OVERRIDDEN_KEYS  pod
--------------------------------------------

{% endtab %} {% endtabs %}

The meanings of special words like @candidate, @new-candidate, and @latest are covered in the State System guide.

7. Promoting the Candidate State

Now for the sake of realism, let's run kubectl apply -f manifest. That worked, so we feel good about these values. Let's promote our latest state:

{% tabs %} {% tab title="Promote" %}

$ kerbi state promote demo @candidate

Updated state[brave-toner].tag from [cand]-brave-toner => brave-toner

{% endtab %}

{% tab title="Retag" %}

$ kerbi state retag demo @latest 0.0.1

Updated state[0.0.1].tag from brave-toner => 0.0.1

{% endtab %} {% endtabs %}

The name of our state has changed:

{% tabs %} {% tab title="List" %}

$ kerbi state list demo

 TAG    MESSAGE  ASSIGNMENTS  OVERRIDES  CREATED_AT
 0.0.1           2            1          a minute ago

{% endtab %}

{% tab title="Show" %}

$ kerbi state show @latest

--------------------------------------------
 TAG              0.0.1
--------------------------------------------
 MESSAGE
--------------------------------------------
 CREATED_AT       2022-04-13 10:32:55 +0100
--------------------------------------------
 VALUES           pod.image: ruby           
                  service.type: ClusterIP
--------------------------------------------
 DEFAULT_VALUES   pod.image: nginx          
                  service.type: ClusterIP
--------------------------------------------
 OVERRIDDEN_KEYS  pod
--------------------------------------------

{% endtab %} {% endtabs %}

8. Retrieving State

It's finally time to make use of the state we saved. Let's template the manifest again, with a new value assignment, but also with the old pod.image=ruby assignment:

{% tabs %} {% tab title="Template" %}

$ kerbi template demo . \
        --read-state @latest \
        --write-state @new-candidate \
        > manifest.yaml

{% endtab %}

{% tab title="manifest.yaml" %}

apiVersion: v1
kind: Pod
metadata:
  name: hello-kerbi
  namespace: demo
  annotations:
    author: person
  labels:
    app: hello-kerbi
spec:
  containers:
  - name: main
    image: ruby:alpine

---

apiVersion: v1
kind: Service
metadata:
  name: hello-kerbi
  namespace: demo
  annotations:
    author: person
  labels:
    app: hello-kerbi
spec:
  type: LoadBalancer
  selector:
    app: hello-kerbi
  ports:
  - port: 80

{% endtab %} {% endtabs %}

We see in the manifest that the values from the old state (pod.image=ruby) were successfully applied which is what we wanted to do. Inspecting the state shows we have a new entry, as expected:

$ kerbi state list

TAG               REVISION   MESSAGE  ASSIGNMENTS  OVERRIDES  CREATED_AT
[cand]-warm-tap                       2            2          11 seconds ago
0.0.1                                 2            1          10 minutes ago

9. In Your CD Pipeline

Putting it all together, the following shows what a simple Kubernetes deployment script using Kerbi could look like.

{% code title="my-cd-pipeline.sh" %}

$ kerbi state init demo .

$ kerbi config set k8s-auth-type <your-strategy>

$ kerbi template demo . \
        --set <however you get your vars in> \
        --read-state @latest \
        --write-state @new-candidate \
        > manifest.yaml

$ kubectl apply --dry-run=server -f manifest.yaml \
  && kerbi state promote demo @candidate \
  && kubectl apply -f manifest.yaml  

{% endcode %}

10. Revisions

{% hint style="warning" %} Work in progress.

The following is a preview. The command group $ kerbi revision does not work. {% endhint %}

$ kerbi revision push 1.0.1