5 - Create complex pods

Create more complex pods with entra login, requiring entra variables and volumes
Published

June 5, 2026

In previous chapters we deployed an app with the nginx default page. Before deploying more complex images we will start by adding volumes to change that default into somthing ours.

Static websites

For static websites (or even static html served through a flask app that handles login and session through cookies) 1 replica should be enough as it avoids some issues with volumes being read twice at the same time.

We will for these work with volumes but the volumes can’t be just a routing since we don’t know which node will be running the pod (so we don’t know which server to keep the files on). For this we will use a server with nfs to store our files, in this case we’ll use the control pane for this. Just run the following in the control pane server:

apt install nfs-kernel-server
echo "/home/gitlabci/websites *(ro,no_subtree_check)" >> /etc/exports
exportfs -a

and on the nodes:

apt install -y nfs-common
which mount.nfs

For the my-app deployement we will add some information:

# /home/gitlabci/conf/websites/my-app.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-app-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadOnlyMany 
  nfs:
    server: stivm0052   # or its IP
    path: /home/gitlabci/websites/my-app
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-app-pvc
  namespace: default
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: default
spec:
  replicas: 1 #changed to 1 for avoiding reading concurrency problems
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts: # add volume mounts
        - name: website
          mountPath: /usr/share/nginx/html
      volumes: # add volume declaration
      - name: website
        persistentVolumeClaim:
          claimName: my-app-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
  namespace: default
spec:
  selector:
    app: my-app
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP 
kubectl apply -f /home/gitlabci/conf/websites/my-app.yaml
What changed?
  1. Added the volumes and volumemounts in the app spec.
  2. Added PersistentVolume and PersistentVolumeClaim
  • The first is the cluster wide decalration of the storage the claim sits in between the storage and the pod
  • These layers exist because k8s was designed so the volumes can be declared once and the development teams can call them without knowing the exact path

Now lets add another pod so we can work with routing in the next step:

Show YAML
# /home/gitlabci/conf/websites/pinchsmall.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pinchsmall-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadOnlyMany
  nfs:
    server: stivm0052   # or its IP
    path: /home/gitlabci/websites/pinchsmall
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pinchsmall-pvc
  namespace: default
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pinchsmall
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pinchsmall
  template:
    metadata:
      labels:
        app: pinchsmall
    spec:
      containers:
      - name: pinchsmall
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: website
          mountPath: /usr/share/nginx/html
      volumes:
      - name: website
        persistentVolumeClaim:
          claimName: pinchsmall-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: pinchsmall-svc
  namespace: default
spec:
  selector:
    app: pinchsmall
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP
kubectl apply -f /home/gitlabci/conf/websites/pinchsmall.yaml

And upgrade the ingress for our domain with the first website in /test/ and the pinchsmall website at root path:

# /home/gitlabci/conf/ingress/ipese-test.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: strip-test
  namespace: default
spec:
  stripPrefix:
    prefixes:
      - /test
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ipese-test
  namespace: default
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.middlewares: default-strip-test@kubernetescrd
spec:
  ingressClassName: traefik
  tls:
  - hosts:
    - ipese-test.epfl.ch
    secretName: myapp-tls
  rules:
  - host: ipese-test.epfl.ch
    http:
      paths:
      - path: /test
        pathType: Prefix
        backend:
          service:
            name: my-app-svc
            port:
              number: 80
      - path: /
        pathType: Prefix
        backend:
          service:
            name: pinchsmall-svc
            port:
              number: 80
kubectl apply -f /home/gitlabci/conf/ingress/ipese-test.yaml

Check status of your kube

$ root@stivm0052:/home/gitlabci/conf/ingress# kubectl get all
NAME                              READY   STATUS      RESTARTS   AGE
pod/my-app-5fb5c4bb54-z9kfh       1/1     Running     0          2m55s
pod/pinchsmall-6bf6c55b9c-pl4bp   1/1     Running     0          16m
pod/test-ping                     0/1     Completed   0          49d

NAME                     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
service/kubernetes       ClusterIP   10.96.0.1        <none>        443/TCP   53d
service/my-app-svc       ClusterIP   10.111.163.238   <none>        80/TCP    2m55s
service/pinchsmall-svc   ClusterIP   10.103.131.201   <none>        80/TCP    16m

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/my-app       1/1     1            1           2m55s
deployment.apps/pinchsmall   1/1     1            1           16m

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/my-app-5fb5c4bb54       1         1         1       2m55s
replicaset.apps/pinchsmall-6bf6c55b9c   1         1         1       16m
$ root@stivm0052:/home/gitlabci/conf/ingress# kubectl get ingress
NAME         CLASS     HOSTS                ADDRESS      PORTS     AGE
ipese-test   traefik   ipese-test.epfl.ch   10.95.35.4   80, 443   5m11s

Here we can see:

  • 2 pods both 1/1 Running — my-app and pinchsmall are up
  • 2 services routing internal traffic to each pod
  • 2 deployments each managing their replica as expected
  • 1 ingress ipese-test handling both /test and / on ipese-test.epfl.ch
  • test-ping is just an old completed job, harmless but can be erased with kubectl delete pod test-ping

Current configuration:

/conf
├── certs
   ├── ipese-test.epfl.ch.crt
   └── ipese-test.epfl.ch.key
├── ingress
   └── ipese-test.yaml # path descriptions (traefic routes/locations)
├── traefik
   └── values.yaml # ~traefik config - open ports and deployed in which servers as deamonset
└── websites # deployment and service definition for each app
    ├── my-app.yaml 
    └── pinchsmall.yaml

and /website availavle through nfs to all the servers

Deploy image other than nginx

To start we’ll deploy a static website deployed over flask to support authentication through an auth-server. (docker image built for work purposes withihn EPFL).

For this we’ll need environment variables, pulling a new container from a private docker registry and make sure the connection to another tool works properly.

  1. Create the folder with the static files
    • mkdir /home/gitlabci/websites/secure && nano /home/gitlabci/websites/secure/index.html
  2. create your deployment and service
    • nano /home/gitlabci/conf/websites/secure.yaml
asdfsf
  1. add your new route to your ingress