Post

Deploying K3s with Ansible - Part 2

Day 5 - Deploying Traefik with ansible

After creating the VMs and using the site.yml to create the k3s cluster, its time to deploy stuff on it. One of the reason I started down this road was to host this blog. I needed something easy ,fast and did not look terrible. I also wanted something I could secure with SSL certificates and would work with cloudflare. Kubernetes made sense because I could accomplish those things and learn a little bit about CI/CD pipelines and using the built in tool provided by gitlab. I’ll post on these topics more later, first lets talk about Traefik.

Before we deploy services like a webserver into the cluster we need something that can direct traffic from outside the cluster to specific service inside the cluster. Traefik can be use as a load balancer, API gateway, Kubernetes Ingress, and Certificate Management. I want to use it as an a Kubernetes Ingress, this will direct inbound traffic to the different services deployed behind the cluster based on the FQDN you use in DNS.

Part of the sites.yml playbook MetalLB is setup to perform the load balancing and if you look through the example group_vars you will notice that it is mentioned a few times and there are a pool of IPs assigned to it. Check out this post from technotim if you want to learn a little bit more about how metalLB and Traefik work together.

Automating Traefik helm install with Ansible

Ansible has a kubernetes module which is great because I can take all of the kubectl commands and helm command and just use Ansible task to make this a bit more repeatable. I can setup a nice workflow and re-use it as much as is needed.

I am using the block feature in ansible so I can block tasks together and run those with specific tags

Adding helm charts:

1
2
3
4
5
6
7
8
9
10
11
- block: 
  - name: Add Traefik Helm repository
    kubernetes.core.helm_repository:
      name: traefik
      repo_url: https://helm.traefik.io/traefik

  - name: Update Helm repositories
    kubernetes.core.helm_repository:
      name: traefik
      repo_url: https://helm.traefik.io/traefik
      force_update: yes

Create a namespace for traefik and install it with the helm charts. The traefik_values variables are all stored under ./roles/install-traefik/vars/main.yml. These are where you can make specific configurations to the helm charts when install applications like this. I will reuse this method a lot. If you would like route more than http and https requests this is where you would add those “entry points”. Under the ports section you can create entry points and what they should do. These values are always defined by the helm chart and you can find more details about them in the helm chart readme for the application you are installing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- name: Create Traefik namespace
    kubernetes.core.k8s:
      api_version: v1
      kind: Namespace
      name: traefik
      state: present

  - name: Install Traefik Helm chart
    kubernetes.core.helm:
      name: traefik
      chart_ref: traefik/traefik
      release_namespace: kube-system
      values: "{{ traefik_values }}"
      state: present

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
traefik_values:
  globalArguments:
    - "--global.sendanonymoususage=false"
    - "--global.checknewversion=false"

  additionalArguments:
    - "--serversTransport.insecureSkipVerify=true"
    - "--log.level=INFO"

  deployment:
    enabled: true
    replicas: 3
    annotations: {}
    podAnnotations: {}
    additionalContainers: []
    initContainers: []

  ports:
    web:
      redirectTo:
        port: websecure
        priority: 10
    websecure:
      tls:
        enabled: true
    gitlab-ssh:
      port: 22
      expose: true
      exposedPort: 22
      protocol: TCP
  ingressRoute:
    dashboard:
      enabled: false

  providers:
    kubernetesCRD:
      enabled: true
      ingressClass: traefik-external
      allowExternalNameServices: true
    kubernetesIngress:
      enabled: true
      allowExternalNameServices: true
      publishedService:
        enabled: false

  rbac:
    enabled: true

  service:
    enabled: true
    type: LoadBalancer
    annotations: {}
    labels: {}
    spec:
      loadBalancerIP: 192.168.30.80 # this should be an IP in the MetalLB range
    loadBalancerSourceRanges: []
    externalIPs: []

Create a default middleware:

middleware refers to a set of functionalities that can be applied to HTTP requests and responses as they pass through the Traefik proxy. Middleware allows you to modify, filter, or enhance incoming requests or outgoing responses, providing a way to implement various features and behaviors within your network infrastructure.

I want to use variables when configuring these apps so I decided to go with Jinja2 templates to facilitate this. I can define those variables in group_vars and leave the ability to make adjustments depending on the environment later on. Normally you would create a yaml file and store this information and call it up with a kubectl command.

1
2
3
4
5
6
- name: Apply default headers
    kubernetes.core.k8s:
      state: present
      definition: "{{ lookup('template', 'default-headers.j2') }}"

No variable in here, but you never know that could change some day.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: default-headers
  namespace: default
spec:
  headers:
    browserXssFilter: true
    contentTypeNosniff: true
    forceSTSHeader: true
    stsIncludeSubdomains: true
    stsPreload: true
    stsSeconds: 15552000
    customFrameOptionsValue: SAMEORIGIN
    customRequestHeaders:
      X-Forwarded-Proto: https

Traefik comes with a nice dashboard where you can easily validate you new ingress routes and if your certificate has been applied or not. The dashboard requires the password to be base64 encoded

1
2
3
4
5
6
7
8
9
10
11
12
13
- name: Generate base64-encoded admin password
    shell: htpasswd -nb admin password | openssl base64
    register: base64_password

  - debug:
      var: base64_password.stdout

  - name: Apply Traefik dashboard secret
    kubernetes.core.k8s:
      state: present
      definition: "{{ lookup('template', 'secret-dashboard.j2') }}"

The secret_dashboard.j2 is a secret that is stored in kubernetes and is used with the traefik dashboard login.

1
2
3
4
5
6
7
8
9
10
11
---

apiVersion: v1
kind: Secret
metadata:
  name: traefik-dashboard-auth
  namespace: traefik
type: Opaque
data:
  users: {{base64_password.stdout}}

Finally we can add a new middleware for the dashboard that uses the above secret we just created. We can also add the Traefik ingress route as well.

1
2
3
4
5
6
7
8
9
10
11
- name: Apply middleware
    kubernetes.core.k8s:
      state: present
      definition: "{{ lookup('template', 'middleware.j2') }}"
  
  - name: Apply Traefik ingress
    kubernetes.core.k8s:
      state: present
      definition: "{{ lookup('template', 'staging-ingress.j2') }}"
  tags: staging-install
1
2
3
4
5
6
7
8
9
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: traefik-dashboard-basicauth
  namespace: traefik
spec:
  basicAuth:
    secret: traefik-dashboard-auth

This ingress route is what will route outside requests into the traefik dashboard service, or container that is running inside the cluster. When installing from the helm chart one of the things defined was the ingress.class. This is created then and you will use that class for your ingress routes along with the entryPoints. The variables used here again are all defined in my group_vars. The staging_secret is the name of our staging certificate that we will get from LetEncrypt in the next step of the process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: traefik-dashboard
  namespace: traefik
  annotations: 
    kubernetes.io/ingress.class: traefik-external
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`traefik.{{ install_domain }}`)
      kind: Rule
      middlewares:
        - name: traefik-dashboard-basicauth
          namespace: traefik
      services:
        - name: api@internal
          kind: TraefikService
  tls:
    secretName: {{ staging_secret }}

This post is licensed under CC BY 4.0 by the author.