Deploy nginx container with Let's Encrypt certificate on K3s Kubernetes distribution
Introduction
This guide demonstrates how to create nginx container in K3s Kubernetes distribution with valid Let's Encrypt certificate for HTTPS. Let's Encrypt certificate will be obtained using DNS challenge method with a domain registered on Cloudflare.
OS used: Debian 12
Software used: K3s 1.30.5, Helm 3.16.2, cert-manager 1.16.1
Source
- Official Kubernetes documentation
Running simple container
In order to run nginx container few Kubernetes files need to be created. Order in which Kubernetes elements are created is important (for example Persistent Volume Claim must be created before Deployment). Following files will be created with Kubernetes objects listed in each file:
01_nginx-test-namespace.yml- Namespace
cloudflare-api-token-secret.yml- Secret with Cloudflare API Token
02_lets-encrypt-cert.yml- Issuer for certificate
03_nginx-test-volume.yml- Persistent Volume
- Persistent Volume Claim
04_nginx-test-deployment.yml- Deployment
- Service
- Ingress
Note
cloudflare-api-token-secret.yml doesn't have a number because in production it's better to store this file in other location designated for secrets and remember to apply it before 02_lets-encrypt-cert.yml.
Creating namespace
First create folder in which you will put your Kubernetes files:
$ mkdir -m 700 ~/test-stack
Next prepare file with new namespace which will be used for grouping Kubernetes elements:
$ vim ~/test-stack/01_nginx-test-namespace.yml
apiVersion: v1
kind: Namespace
metadata:
name: test-space
Installing Helm and cert-manager
Install cert-mananger using Helm package manager for Kubernetes with following commands (commands taken from Official Helm documentation and Official cert-manager documentation):
- Export variable
KUBECONFIGfor Helm to work correctly with K3s under your user by putting it in.bashrc:
$ echo -e "\n# K3s environment variables" >> ~/.bashrc && \
echo export KUBECONFIG=/etc/rancher/k3s/k3s.yaml >> ~/.bashrc
echo -e - enable interpretation of backslash escapes
- Login again or run following command to enable
kubectlin current session:
$ source ~/.bashrc
- Install Helm:
$ 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 && \
rm ./get_helm.sh
- Install
cert-manager:
$ helm repo add jetstack https://charts.jetstack.io --force-update
$ helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.16.1 \
--set crds.enabled=true
Enable autocompletion for helm
To enable helm bash autocompletion for user (when using Tab key) run command to add entry to your .bashrc:
$ echo -e "\n# helm completion bash" >> ~/.bashrc && \
echo 'source <(helm completion bash)' >>~/.bashrc
echo -e - enable interpretation of backslash escapes
Login again or run following command to enable bash autocompletion in current session:
$ source ~/.bashrc
Creating Issuer for Let's Encrypt certificate
- Create Kubernetes secret with Cloudflare API Token:
$ vim ~/test-stack/cloudflare-api-token-secret.yml
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret-mydomain.com
namespace: cert-manager
type: Opaque
stringData:
api-token: <API_Token>
...-mydomain.com - your domain name, for easy identification of the domain this secret concerns. Instruction how to set domain using Cloudflare is here Obtaining Let's Encrypt certificate using Cloudflare - Cloudflare DNS.
<API_Token> - enter you Cloudflare API Token. Instruction how to generate API Token is here Obtaining Let's Encrypt certificate using Cloudflare - Cloudflare API Token.
- Secure file with secret with stricter access permissions:
$ chmod 600 ~/test-stack/cloudflare-api-token-secret.yml
- Create config for Let's Encrypt certificate Issuer:
$ vim ~/test-stack/02_lets-encrypt-cert.yml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: lets-encrypt-mydomain.com
namespace: cert-manager
spec:
acme:
email: [email protected] # Email address used for ACME registration
server: https://acme-staging-v02.api.letsencrypt.org/directory # Let's Encrypt staging - use for testing
#server: https://acme-v02.api.letsencrypt.org/directory # Let's Encrypt production - use to get final certificate
privateKeySecretRef:
# Secret resource that will be used to store the account's private key.
# The account private key identifies you as a user of the ACME service
# and is used to sign all requests/communication with the ACME server
# to validate your identity.
name: lets-encrypt-account-key-mydomain.com
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
# Remember to first create and apply a secret containing following fields
name: cloudflare-api-token-secret-mydomain.com
key: api-token
Tip
On you first run use line with comment Let's Encrypt staging. This issues a non valid certificate that is good for testing because it's not rate limited, and if you make a mistake you won't need to wait for some time before next try.
After you deployed nginx and all is working then:
- uncomment line Let's Encrypt production
- comment line Let's Encrypt staging
- apply file
02_lets-encrypt-cert.yml
Note
cert-manager issuer can be type Issuer or ClusterIssuer. Here is description from official documentation:
An
Issueris a namespaced resource, and it is not possible to issue certificates from an Issuer in a different namespace. This means you will need to create an Issuer in each namespace you wish to obtain Certificates in.If you want to create a single Issuer that can be consumed in multiple namespaces, you should consider creating a
ClusterIssuerresource. This is almost identical to the Issuer resource, however is non-namespaced so it can be used to issue Certificates across all namespaces.
Creating Kubernetes Volume
Create folder for data available to container:
html- folder with web page
$ sudo mkdir -p /mnt/nginx-test/html
Create volume configuration file:
$ vim ~/test-stack/03_nginx-test-volume.yml
apiVersion: storage.k8s.io/v1
kind: storageClass
metadata:
name: nginx-test-storage
namespace: test-space
provisioner: kubernetes.io/no-provisioner # storageClass setting for local storage
volumeBindingMode: WaitForFirstConsumer # storageClass setting for local storage
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: nginx-test-html-pv
namespace: test-space
spec:
capacity:
storage: 1Gi # for local storage it's just a label and don't cap space available for container
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: nginx-test-storage
local:
path: /mnt/nginx-test/html # path on your server
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- myhostname # host name of your server
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-test-html-pvc
namespace: test-space
spec:
accessModes:
- ReadWriteOnce
storageClassName: nginx-test-storage
resources:
requests:
storage: 1Gi # for local storage it's just a label and don't cap space available for container
Creating Kubernetes Deployment
$ vim ~/test-stack/04_nginx-test-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test-deployment
namespace: test-space
labels:
app: nginx-test-deployment
spec:
replicas: 1
selector:
matchLabels:
app: nginx-test-deployment
template:
metadata:
labels:
app: nginx-test-deployment
spec:
containers:
- name: nginx
image: nginx:1.27.2 # image you want to use
ports:
- containerPort: 80
volumeMounts: # VolumeMounts are the paths that your application uses inside container
- name: nginx-test-html-volume
mountPath: /usr/share/nginx/html # the path that your application uses inside container
volumes:
- name: nginx-test-html-volume
persistentVolumeClaim:
claimName: nginx-test-html-pvc # should match name of the PersistentVolumeClaim
---
apiVersion: v1
kind: Service
metadata:
name: nginx-test-service
namespace: test-space
spec:
selector:
app: nginx-test-deployment
ports:
- name: http
port: 80
targetPort: 80 # should match containerPort in Deployment
protocol: TCP # protocol that will be used
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: redirect
namespace: test-space
spec:
# Redirect traffic from port 80 to 443 - use only HTTPS
redirectScheme:
scheme: https
permanent: true
port: "443" # must be a string, not a numeric value
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-test-ingress
namespace: test-space
annotations:
cert-manager.io/cluster-issuer: "lets-encrypt-mydomain.com" # should match the name of Issuer or ClusterIssuer
# Traefik Middleware reference - syntax <namespace>-<name>@kubernetescrd
traefik.ingress.kubernetes.io/router.middlewares: test-space-redirect@kubernetescrd
spec:
tls:
- hosts:
- myservername.mydomain.com # replace with your domain
secretName: nginx-test-tls # this is the name of certificate that will be created
rules:
- host: myservername.mydomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-test-service # reference to created Service
port:
number: 80 # Service port number
Info
- If using an
Issuerupdate the Ingress annotation tocert-manager.io/issuer. - If using a
ClusterIssuerupdate the Ingress annotation tocert-manager.io/cluster-issuer.
Running Kubernetes Deployment
It's important to create Kubernetes elements in a specific order, that is why files are numbered and should be run in sequence:
$ kubectl apply -f ~/test-stack/01_nginx-test-namespace.yml && \
kubectl apply -f ~/test-stack/cloudflare-api-token-secret.yml && \
kubectl apply -f ~/test-stack/02_lets-encrypt-cert.yml && \
kubectl apply -f ~/test-stack/03_nginx-test-volume.yml && \
kubectl apply -f ~/test-stack/04_nginx-test-deployment.yml
Checking if certificate was issued
Use these commands to check if certificate was issued:
$ kubectl -n cert-manager get issuers.cert-manager.io
$ kubectl -n cert-manager get clusterissuer.cert-manager.io
$ kubectl -n test-space describe issuers
$ kubectl -n test-space describe clusterissuer
$ kubectl -n test-space describe certificaterequest
$ kubectl -n test-space describe order
$ kubectl -n test-space get challenges
$ kubectl -n test-space describe challenges
$ kubectl -n test-space get secrets
Serving your own webpage on nginx
Create your simple webpage:
$ sudo vim /mnt/nginx-test/html/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Page Title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>My Simple Website</h1>
<p>Website on K3s.</p>
</body>
</html>
Enter the address of your service into web browser: https://myservername.mydomain.com. Check certificate. It should be Let's Encrypt certificate. If it is working change certificate from Let's Encrypt staging to production. Apply file after changes:
$ vim ~/test-stack/02_lets-encrypt-cert.yml
apiVersion: cert-manager.io/v1
(...)
spec:
acme:
(...)
#server: https://acme-staging-v02.api.letsencrypt.org/directory # Let's Encrypt staging - use for testing
server: https://acme-v02.api.letsencrypt.org/directory # Let's Encrypt production - use to get final certificate
(...)
$ kubectl apply -f ~/test-stack/02_lets-encrypt-cert.yml
Appendix
Upgrade helm
To upgrade helm to new version just perform install operation. Helm is a single binary file and this will replace it to new version:
$ 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 && \
rm ./get_helm.sh
Upgrade cert-manager
Use following command to upgrade cert-manager:
$ helm repo update && \
helm upgrade --reset-then-reuse-values --namespace cert-manager --version <version> cert-manager jetstack/cert-manager
<version> - enter new version of cert-manager