Skip to content

Azure-Agst/K3S-Notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

Kubernetes Notes

1.) Install K3S

1.1.) Start by installing K3s & Kubectl

Installing k3s is relatively? simple.

sudo apt install linux-modules-extra-raspi nfs-common
# ...wait 30 minutes...
sudo reboot now

Now, if you're setting up a host, run:

# install k3s in host mode
curl -sfL https://get.k3s.io | sh -

# if you need to set up worker nodes, you'll need the node-token
sudo cat /var/lib/rancher/k3s/server/node-token

# in case you ever need to uninstall
/usr/local/bin/k3s-uninstall.sh

If setting up a worker node, modify the installation command and run to join:

# install k3s in agent (worker) mode. please replace `host_ip` and `token`.
curl -sfL https://get.k3s.io | K3S_URL=https://host_ip:6443 K3S_TOKEN=token sh -

# in case you ever need to uninstall
/usr/local/bin/k3s-agent-uninstall.sh

If you plan on managing from a separate machine, install kubectl. Here's some instructions.

1.2.) Copy config to proper locations

The default location of the config file is /etc/rancher/k3s/k3s.yaml, and it's owned by root by default. Copy it to ~/.kube/config on any of the machines you wish to connect to the cluster from and make sure it has good perms.

Your config should have the server address updated to point to the cluster. If attempting to connect on the machine hosting the cluster, make it 127.0.0.1. Otherwise, make it the most machine's IP. DNS resolves here, btw.

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: # ...snip...
    server: https://YOUR-HOST-IP-GOES-HERE:6443
# ...snip...

Check to see if kubectl works now:

$ kubectl get nodes
NAME       STATUS   ROLES                  AGE   VERSION
hostname   Ready    control-plane,master   59m   v1.22.7+k3s1

3.) Install Admin Dashboard

The Admin Dashboard is a pretty handy utility that allows you to visualize a lot of what's going on in your cluster. Here's how to install it.

On your master node, run the following commands:

GITHUB_URL=https://github.com/kubernetes/dashboard/releases
VERSION_KUBE_DASHBOARD=$(curl -w '%{url_effective}' -I -L -s -S ${GITHUB_URL}/latest -o /dev/null | sed -e 's|.*/||')
sudo k3s kubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/${VERSION_KUBE_DASHBOARD}/aio/deploy/recommended.yaml

This should install the dashboard for you. Now we just need to make a service account within the dashboard's namespace, in order to get an access token.

Make a file, dashboard-make-user.yaml, and put the following contents into it:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: username
  namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: username
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: username
  namespace: kubernetes-dashboard
---

Now, apply it using kubectl, then get the token:

kubectl create -f dashboard-make-user.yaml
kubectl -n kubernetes-dashboard describe secret username-token | grep '^token'

Cache this token somewhere secure, you'll need it pretty frequently when the dashboard logs you out occasionally.

You can now access the dashboard by opening a terminal on your local machine, using your local copy of kubectl to start a proxy on your machine, then navigating to a special link:

kubectl proxy

Your dashboard can now be found at: http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/

2.) Configure NFS

For many applications, it may be worthwhile to set up a persistent datastore somewhere, like on an NFS server. This section describes how to set up your cluster to use your NFS server as your default data store using Synology DSM as an example!

2.1) Set Up NFS Using Synology

Start by navigating to DSM in your browser and logging in. Then, go to Control Panel, and then enter the File Services menu. Go to the NFS tab, and enable the service, making sure the maximum protocol is set to NFSv4.

NFS Control Panel

Now, ne need to make a new shared folder for the PVC to use. Go to the Shared Folder settings, and create a new folder, like K3S_PVC or whatever. Just no spaces.

Making Folder

Click through the rest of the settings. User permissions don't really matter here, as NFS bypasses them, but they can be configured if you wish to connect to the PVC server via SMB/FTP/AFS/etc. for management later.

Once the folder is made, right click on it and select "Edit". Then, navigate to the "NFS Permissions" tab. Here's the interesting bit: NFS authenticates by checking client IPs against a whitelist, rather than traditional RBAC. We'll need to whitelist the CIDR corresponding to our cluster.

Configure Whitelist

Click Create, type in the CIDR into the first box, then leave everything else as-is and Save it. Copy the "Mount Path" value that shows up at the bottom, that'll be pretty important later! :)

2.2) Install nfs-subdir-external-provisioner Via Helm

Now we need to install our NFS provisioner!

If you don't have helm installed already, on your client that has access to the cluster, run the following:

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh

Now, add the repo for our provisioner into Helm's databases:

helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm repo update

Finally, install the provisioner package, replaving variables where applicable:

helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ 
    --set nfs.server=ip_to_your_synology \ 
    --set nfs.path=/mount/path/from/step/2.1 

Once it's applied, you can check the storageclasses available to your cluster:

$ kubectl get storageclass
NAME                   PROVISIONER
local-path (default)   rancher.io/local-path
nfs-client             cluster.local/nfs-subdir-external-provisioner

Now, lets swap nfs-client in to be our default datastore:

kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl patch storageclass nfs-client -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

We're now good to go!

2.3) Test making a PVC

Now, lets make a test PVC, that would be used with one of our services.

Make a file, pvc-example.yaml, and pyt the following into it:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-example
spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Mi

This yaml file creates a single PVC, pvc-example, which uses our nfs-client storage class, allows reading and writing, and requests at least 100 Mibibytes of storage.

Apply it, and you should see a new folder be made on the NFS server, that starts with pvc-example. Congrats! You've successfully configured NFS!

3.) Install Nginx to Host Static Content

3.1) Install NGINX

Installing Nginx is kind of complex, and involves several parts:

  • We need a namespace to house everything
  • We need a PVC to allow our cluster to serve our website on our NAS
  • We need a service definition to allow our service to interface with Traefik later
  • We need a deployment to actually spin up the server for us

Let's start with the namespace:

apiVersion: v1
kind: Namespace
metadata:
  name: nginx
  labels:
    name: nginx
---

Apply this before anything else, and it should make a namespace for you.

Now, make the PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Mi
---

Now, the service:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  labels:
    run: nginx
spec:
  ports:
    - port: 80
      protocol: TCP
  selector:
    app: nginx
---

And finally, the deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html-volume
        persistentVolumeClaim:
          claimName: nginx-pvc
---

Put all of those together in a single file and apply it! Your server should spin up and start serving your website, but we have no means of accessing it yet. Before we configure ingress, lets set up TLS.

3.2) HTTP Session Persistence

By default, K3S uses Traefik as its ingress controller, which we'll talk about in greater detail later. Essentially, this means that Traefik takes in all http(s) requests, checks them against Ingress rules you define, then routes that request to an internal service for further processing. For deployments with more than one replica, Traefik will perform some rudimentary "round-robin" load balancing on the requests. This can cause some issues with services that require authentication.

For example: Say Alice issues a request to authenticate with your service, which is handled by Pod A. After this request, Traefik advances its round-robin queue to prepare to send the next request to the next pod in sequence. Alice then requests to use her authentication to retrieve some data, and the request is forwarded to Pod B. Since Pod B did not authenticate Alice, her request is denied, and she may receive some undefined behavior instead.

This issue can be solved by issuing an affinity or "sticky session" cookie, a special cookie which lets Traefik know which pod originally handled the request and allows the client to maintain its session with that pod. This cookie lasts the duration of the session, after which the client's next request will go back through the load balancer.

Sticky session cookies are dead easy to implement. Go to the service definition and add the following, either through kubectl or the dashboard:

metadata:
  annotations:
    traefik.ingress.kubernetes.io/service.sticky.cookie: 'true'
    traefik.ingress.kubernetes.io/service.sticky.cookie.name: cookie-name

That's all you need! Now web sessions should work properly.

4.) Configure HTTPS

We'll dynamically configure our TLS certs using a useful tool, called cert-manager. Apply cert-manager's boilerplate using the following command:

kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.8.0/cert-manager.yaml

You should now have cert-manager installed! Now we need to configure it to grab certs for us. We'll be using an ACME service called Let's Encrypt to automatically get our TLS certificates. These certs only last for 90 days, but cert-manager automatically handles renewing them for us. Pretty simple!

Here's an example of a certificate issuer we can make in our cluster:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: youremailhere@example.com
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
      #...

The only part that's missing from this issuer definition is the solvers section, which brings us to the un-fun part of getting certs: ACME challenges.

When interacting with an ACME server, either by using cert-manager or certbot in your command line, you'll request a cert for a domain you control, and to prove you control this domain, the server will issue a challenge. These challenges take one of two forms: HTTP or DNS.

4.1) HTTP-01 Challenge

The HTTP-01 challenge requires you host a very specific file on your web server, typically in the .well-known/acme-challenge/ directory. The file name is the token that the ACME server issues you during your challenge, and the file contents are a concatenation of said token and a thumbprint of your account key.

HTTP-01 is an ideal challenge to use if you're already planning on publishing whatever service will be hosted under this domain. Cert-manager will handle adding the file and removing it automatically when it's time to renew. However, if you're hosting a service locally and don't plan on port-forwarding (or exposing the server by some other means like Cloudflare tunnel) then this method may be a royal pain.

Here's an example of a solver section for HTTP-01, assuming your service will be behind k3s's default Traefik ingress manager:

    solvers:
    - http01:
        ingress:
          class: traefik

For more on Configuring HTTP-01 solvers, see cert-manager's docs.

4.2) DNS-01 Challenge

The DNS-01 challenge requires for you to set a TXT record under the _acme-challenge key of whatever (sub)domain you're trying to verify. The contents of the TXT record is the token that the ACME server issues you during your challenge.

DNS-01 is the ideal challenge to use if you can't be bothered to expose an HTTP server every 90 days for the sake of renewing. Most DNS providers (i.e. Cloudflare) have a JSON API you can use to automatically update records, so passing this challenge is pretty simple and secure for LAN use.

The rest of this section is an example on how to configure DNS-01, assuming your domain is registered with Cloudflare.

First you'll need to generate an API token. The API token should be made in the Cloudflare dashboard by navigating to User Profile > API Tokens > API Tokens and making a token with the Zone:Edit perms, and your domain added as a valid zone.

Now, lets store that token in a secret within the cert-manager namespace. Make a file, and paste the contents, making sure to paste your actual API token.

apiVersion: v1
kind: Secret
metadata:
  namespace: cert-manager
  name: cloudflare-api-token-secret
type: Opaque
stringData:
  api-token: <API Token>

Apply, and now we can use that token in our solvers! Here's an example of a solver section using that token:

    solvers:
    - dns01:
        cloudflare:
          apiTokenSecretRef:
            name: cloudflare-api-token-secret
            key: api-token

For more on Configuring DNS-01 solvers, see cert-manager's docs. The DNS-01 section has subsections with configuration for each of the major DNS providers, like Cloudflare, DigitalOcean, Route53, Google Cloud DNS, etc.

4.3) Configuring your Issuers

Now that we know which challenge we're using and how they're implemented, let's configure our issuers that will automatically grab our certs for us. We'll make two: one for testing (staging) and one for production.

Make a file, letsencrypt-cert-issuers.yaml, and paste the following, making sure to replace your email and paste in your solver configuration for each. (Note that these issuers don't have to have identical solvers! If you want staging to use DNS-01 and prod to use HTTP-01, that can be configured.)

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: youremailhere@example.com
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
      #...
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: youremailhere@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      #...
---

Apply the file, and you should now have two cert issuers that use certbot's staging and prod ACME servers respectively!

4.4) Configuring an HTTPS Redirect

While we're here, we should really configure a redirect to force our traffic to use HTTPS. Here's a hack:

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: redirect-https
spec:
  redirectScheme:
    scheme: https
    permanent: true
---

Apply that! Now, we're about ready to get our Ingress configured!

4.5) Configure Ingress

Any data that comes through on 80 or 443 goes through our ingress controller, Traefik. Let's use an ingress definition to tell our router how to forward our data.

Make yet another yaml file and give it the following contents:

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
  annotations:
    kubernetes.io/ingress.class: "traefik"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
spec:
  rules:
  - host: yourdomainhere.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-service
            port:
              number: 80
  tls:
  - secretName: yourdomainhere-com-tls
    hosts:
    - yourdomainhere.com

Before we apply it, let's understand a few things:

  • Every instance of yourdomainhere.com should be replaced with your actual domain
    • The TLS secret name should also match your domain name, but must only have dashes and letters. I usually replace periods with dashes, as shown in the example.
  • The Path and PathType are SUPER sensitive to their configuration. Make sure this is configured as expected! Reference Located Here.
  • You don't have to have a host definition, but its useful!
    • You can use a local DNS server to have different domain names all point to your cluster, and then the ingress controller can use the domain attached to each request to route you to your various web interfaces, despite all apps being on a single set of IPs!
  • If this were a staging environment, we'd want to make sure that our cert-manager metadata was set to "letsencrypt-staging", the name of the class we defined earlier.

Now apply it, and your site should be live!

5.) Installing PiHole

5.1) Setting up the Basics

Let's start by setting up the basics: a namespace and PVC!

apiVersion: v1
kind: Namespace
metadata:
  name: pihole
  labels:
    name: pihole
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pihole-config
  labels:
    app: pihole
spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 500Mi
---

We'll also need a secret: the admin panel password!

apiVersion: v1
kind: Secret
metadata:
  name: pihole-webpassword
  namespace: pihole
data:
  password: YOUR_BASE64_ENCODED_PASS_HERE
type: Opaque

5.2) Deploying

Deploying Pihole is pretty fun! Look at how long this one is!

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pihole-deployment
  namespace: pihole
  labels:
    app: pihole
spec:
  selector:
    matchLabels:
      app: pihole
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: pihole
    spec:
      containers:
      - image: pihole/pihole
        imagePullPolicy: IfNotPresent
        name: pihole
        env:
        - name: ADMIN_EMAIL
          value: your@email.com
        - name: WEBPASSWORD
          valueFrom:
            secretKeyRef:
              key: password
              name: pihole-webpassword
        ports:
        - containerPort: 80
          name: pihole-http
          protocol: TCP
        - containerPort: 53
          name: dns
          protocol: TCP
        - containerPort: 53
          name: dns-udp
          protocol: UDP
        - containerPort: 443
          name: pihole-ssl
          protocol: TCP
        - containerPort: 67
          name: client-udp
          protocol: UDP
        volumeMounts:
        - mountPath: /etc/pihole
          name: config
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      volumes:
      - name: config
        persistentVolumeClaim:
          claimName: pihole-config
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
status: {}

5.3) Exposing The Services

Now, this case is interesting because we functionally have two services that we want to expose:

  • The first is the HTTP Admin portal, which we probably want to go thru traefik
  • The second is the actual DNS traffic on port 53, which we'd like to have each of our nodes serve

As we have two separate service formats, we need to define two services for the cluster to use. First, the web portal, and its ingress. Pretty easy!

apiVersion: v1
kind: Service
metadata:
  name: pihole-web
  namespace: pihole
  labels:
    run: pihole
spec:
  ports:
  - name: pihole-http
    port: 80
    protocol: TCP
  selector:
    app: pihole
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
  name: pihole-ingress
  namespace: pihole
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
spec:
  rules:
    - host: pihole.yourdomain.com
      http:
        paths:
          - path: /admin
            pathType: Prefix
            backend:
              service:
                name: pihole-web
                port:
                  number: 80
  tls:
    - hosts:
        - pihole.yourdomain.com
      secretName: pihole-yourdomain-com-tls

And the more complex service, which will actually expose our services on the cluster's actual IPs, is a LoadBalancer type:

apiVersion: v1
kind: Service
metadata:
  name: pihole-dns
  namespace: pihole
  labels:
    run: pihole
spec:
  type: LoadBalancer
  ports:
  - name: pihole-dns-tcp
    port: 53
    targetPort: 53
    protocol: TCP
  - name: pihole-dns-udp
    port: 53
    targetPort: 53
    protocol: UDP
  externalTrafficPolicy: Local
  sessionAffinity: None
  selector:
    app: pihole
---

This makes a load balancer which disperses the load over the three pods we have running!

Note that NodePort didnt work for me, and externalTrafficPolicy: Local means the actual IPs are logged in pihole. :)

A.) Appendix: References

About

Notes from when I redid my K3S cluster

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors