Preparing Kubernetes
Once the bootstrap process completes, the Talos Kubernetes cluster is not yet ready to run applications. It lacks services such as ingress, a load balancer and monitoring, which need to be installed next.
Components:
- MetalLB
- Metrics-server
They are located in the clusters/talos-dev directory. Logically, they are treated as system services, separate from regular applications. Once the base services are configured, you can deploy, in order: the appchart repository and the individual applications.
FluxCD
Start the process by bootstrapping FluxCD. This allows some of the resources to be created automatically.
To start using Flux, a bootstrap operation must be performed. During this process Flux generates and deploys its manifests to the Kubernetes cluster and publishes them to the specified repository. The cluster configuration for my environment is stored on GitHub. All available options are listed in the bootstrap process documentation.
Kubernetes
Before running the flux command, make sure you are logged in to the Kubernetes cluster where Flux should be installed.
export GITHUB_TOKEN=gh-xxxxxxxxxxxxx
flux bootstrap github \
--components-extra=image-reflector-controller,image-automation-controller \
--token-auth \
--owner=HomeDevopsLab \
--repository=test-fluxrepo \
--branch=main \
--path=clusters/k3s-test \
--personalThe above command publishes Flux manifests to the repository while simultaneously installing them in the Kubernetes cluster. As a result, the directory clusters/k3s-test is created in the repository, containing the Flux objects.
Kustomization
To deploy applications (and other configurations) with FluxCD, start by preparing a Kustomization manifest that points to the apps directory.
apps
To avoid a Kustomization error, create the apps directory and publish it to the repository together with the Kustomization manifest.
cd test-fluxrepo
mkdir apps
touch apps/.gitkeep---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 1m0s
sourceRef:
kind: GitRepository
name: flux-system
path: ./apps
prune: true
wait: true
timeout: 1m0sFrom this point, every manifest that appears in the apps directory will be automatically deployed by Flux.
More details about FluxCD: Kubernetes/FluxCD
SOPS
Installing the age key
age-keygen -o age.agekey
cat age.agekey |
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=/dev/stdinRepository configuration
The age key contains a string with the Age public key. It can be read from the previously created secret:
{
"age.agekey": "# created: 2026-06-25T22:40:53+02:00\n# public key: agexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nAGE-SECRET-KEY-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxZ\n"
}In the root directory of the repository, create a .sops.yaml file using the public key obtained in the previous step:
creation_rules:
- encrypted_regex: "^(data|stringData)$"
age: agexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxExtend the clusters/raspberry/apps.yaml configuration with the decryption settings:
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: apps-cl2
namespace: flux-system
spec:
interval: 1m0s
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-age
path: ./apps-raspberry
prune: true
wait: true
timeout: 1m0sPrivate key
Once the configuration is complete, the age.agekey file can be deleted from disk. The entire secret encryption setup now lives in the Kubernetes cluster: the sops-age secret holds the private key, and the .sops.yaml file holds the public key.
More details about SOPS: Kubernetes/SOPS
Load Balancer
Installation
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.16.1/config/manifests/metallb-native.yamlConfiguration
You need to configure IP address advertisement and the address pool MetalLB can use. The configuration is stored in clusters/talos-dev/metallb, and FluxCD deploys it to the cluster.
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-systemapiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: metallb-ip-pool
namespace: metallb-system
spec:
addresses:
- 192.0.2.170-192.0.2.179Addresses in the IPAddressPool must not be used by any other device or service.
Ingress
The cluster uses Traefik as its ingress, installed with Helm.
First, install the required CRDs:
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.5/docs/content/reference/dynamic-configuration/kubernetes-gateway-rbac.ymlNext, prepare a values.yaml file that enables Gateway API support:
providers:
kubernetesGateway:
enabled: trueand install the chart:
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm upgrade --install traefik traefik/traefik \
-n traefik --create-namespace \
-f values.yamlKubernetes metrics
The metrics-server manifest is located in the clusters/talos-dev/metrics-server directory.
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
k8s-app: metrics-server
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-view: "true"
name: system:aggregated-metrics-reader
rules:
- apiGroups:
- metrics.k8s.io
resources:
- pods
- nodes
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
k8s-app: metrics-server
name: system:metrics-server
rules:
- apiGroups:
- ""
resources:
- nodes/metrics
verbs:
- get
- apiGroups:
- ""
resources:
- pods
- nodes
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
k8s-app: metrics-server
name: metrics-server-auth-reader
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: extension-apiserver-authentication-reader
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
k8s-app: metrics-server
name: metrics-server:system:auth-delegator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
k8s-app: metrics-server
name: system:metrics-server
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:metrics-server
subjects:
- kind: ServiceAccount
name: metrics-server
namespace: kube-system
---
apiVersion: v1
kind: Service
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
spec:
ports:
- appProtocol: https
name: https
port: 443
protocol: TCP
targetPort: https
selector:
k8s-app: metrics-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
k8s-app: metrics-server
name: metrics-server
namespace: kube-system
spec:
selector:
matchLabels:
k8s-app: metrics-server
strategy:
rollingUpdate:
maxUnavailable: 0
template:
metadata:
labels:
k8s-app: metrics-server
spec:
containers:
- args:
- --cert-dir=/tmp
- --secure-port=10250
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=15s
- --kubelet-insecure-tls
image: registry.k8s.io/metrics-server/metrics-server:v0.9.0
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 3
httpGet:
path: /livez
port: https
scheme: HTTPS
periodSeconds: 10
name: metrics-server
ports:
- containerPort: 10250
name: https
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /readyz
port: https
scheme: HTTPS
initialDelaySeconds: 20
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 200Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
volumeMounts:
- mountPath: /tmp
name: tmp-dir
nodeSelector:
kubernetes.io/os: linux
priorityClassName: system-cluster-critical
serviceAccountName: metrics-server
volumes:
- emptyDir: {}
name: tmp-dir
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
labels:
k8s-app: metrics-server
name: v1beta1.metrics.k8s.io
spec:
group: metrics.k8s.io
groupPriorityMinimum: 100
insecureSkipTLSVerify: true
service:
name: metrics-server
namespace: kube-system
version: v1beta1
versionPriority: 100