Deploy Your First Application¶
This guide deploys podinfo, a small Go web application, on Safespring Kubernetes Engine. The deployment covers the building blocks most applications need:
- a Secret injected as an environment variable
- a PersistentVolumeClaim mounted for data that survives pod restarts
- HTTPS exposure with a Let's Encrypt certificate, via Ingress or Gateway API
Prerequisites¶
- A running SKE cluster and a kubeconfig obtained from the portal, active in your shell via the
KUBECONFIGenvironment variable. kubectlinstalled locally.- A DNS A record for your application hostname pointing at the cluster's public IPv4 address, shown in the portal. The examples use
podinfo.example.com— replace it with your hostname throughout.
Apply each manifest below with kubectl apply -f <file>.
Namespace¶
apiVersion: v1 kind: Namespace metadata: name: podinfo-demo
Secret¶
The secret holds the message podinfo displays on its start page, so you can verify the value reached the pod.
apiVersion: v1 kind: Secret metadata: name: podinfo-secret namespace: podinfo-demo type: Opaque stringData: ui-message: "Hello from a Kubernetes Secret"
Note
Applying a plain Secret manifest is fine for getting started, but do not commit one to Git.
See Secret Management for Git-safe workflows.
Persistent Volume Claim¶
The claim provisions a Cinder block volume through the pre-installed CSI driver. See Persistent Volumes for the available storage classes and how to expand a volume later.
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: podinfo-data namespace: podinfo-demo spec: accessModes: - ReadWriteOnce storageClassName: fast resources: requests: storage: 1Gi
Deployment¶
The deployment consumes the secret as the PODINFO_UI_MESSAGE environment variable and mounts the volume at /data, where podinfo's /store endpoint writes.
apiVersion: apps/v1 kind: Deployment metadata: name: podinfo namespace: podinfo-demo spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: podinfo template: metadata: labels: app: podinfo spec: securityContext: runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 runAsNonRoot: true containers: - name: podinfo image: ghcr.io/stefanprodan/podinfo:6.7.1 command: - ./podinfo - --port=9898 - --data-path=/data env: - name: PODINFO_UI_MESSAGE valueFrom: secretKeyRef: name: podinfo-secret key: ui-message ports: - name: http containerPort: 9898 protocol: TCP livenessProbe: httpGet: path: /healthz port: http readinessProbe: httpGet: path: /readyz port: http resources: requests: cpu: 100m memory: 64Mi limits: memory: 128Mi volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: podinfo-data
Note
strategy: Recreate matters here: a ReadWriteOnce volume can only attach to one node.
A rolling update would start the new pod before the old one releases the volume and deadlock.
Service¶
apiVersion: v1 kind: Service metadata: name: podinfo namespace: podinfo-demo spec: selector: app: podinfo ports: - name: http port: 80 targetPort: http protocol: TCP type: ClusterIP
Expose the Application over HTTPS¶
Cert Manager is pre-installed on every cluster, but a ClusterIssuer is not — you create one per cluster. Both options below use Let's Encrypt with an HTTP-01 solver, so the DNS record from the prerequisites must be in place before the certificate can be issued.
Pick one of the two options. See Traffic Management for a comparison.
Note
Let's Encrypt rate-limits failed and repeated issuance per domain. While experimenting, use the
staging server https://acme-staging-v02.api.letsencrypt.org/directory in the ClusterIssuer,
then switch to production.
Option A: Ingress¶
Cilium provides the IngressClass named cilium; verify with kubectl get ingressclass.
Create the issuer with an Ingress HTTP-01 solver:
apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-ingress spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: you@example.com privateKeySecretRef: name: letsencrypt-ingress-private-key solvers: - http01: ingress: ingressClassName: cilium
The cert-manager.io/cluster-issuer annotation makes cert-manager request the certificate and store it in the secret named under tls.secretName:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: podinfo namespace: podinfo-demo annotations: cert-manager.io/cluster-issuer: letsencrypt-ingress spec: ingressClassName: cilium tls: - hosts: - podinfo.example.com secretName: podinfo-tls rules: - host: podinfo.example.com http: paths: - path: / pathType: Prefix backend: service: name: podinfo port: number: 80
Option B: Gateway API¶
The clusters ship with Gateway API enabled and the GatewayClass named cilium.
Create the Gateway first — the certificate solver below attaches a challenge route to its HTTP listener:
apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: podinfo-gateway namespace: podinfo-demo spec: gatewayClassName: cilium listeners: - name: https hostname: "podinfo.example.com" port: 443 protocol: HTTPS tls: mode: Terminate certificateRefs: - kind: Secret name: podinfo-tls allowedRoutes: namespaces: from: Same - name: http hostname: "podinfo.example.com" port: 80 protocol: HTTP allowedRoutes: namespaces: from: Same
Create the issuer and request the certificate. The HTTPS listener stays unready until the secret podinfo-tls exists; issuance succeeds anyway because the HTTP-01 challenge uses the HTTP listener.
apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-gateway spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: you@example.com privateKeySecretRef: name: letsencrypt-gateway-private-key solvers: - http01: gatewayHTTPRoute: parentRefs: - name: podinfo-gateway namespace: podinfo-demo kind: Gateway --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: podinfo-tls namespace: podinfo-demo spec: secretName: podinfo-tls issuerRef: name: letsencrypt-gateway kind: ClusterIssuer dnsNames: - podinfo.example.com
Route HTTPS traffic to the service and redirect plain HTTP to HTTPS:
apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: podinfo-https namespace: podinfo-demo spec: parentRefs: - name: podinfo-gateway sectionName: https hostnames: - "podinfo.example.com" rules: - matches: - path: type: PathPrefix value: "/" backendRefs: - name: podinfo port: 80 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: podinfo-http-redirect namespace: podinfo-demo spec: parentRefs: - name: podinfo-gateway sectionName: http hostnames: - "podinfo.example.com" rules: - filters: - type: RequestRedirect requestRedirect: scheme: https statusCode: 301
Verify¶
Wait for the pod and the certificate to become ready:
kubectl -n podinfo-demo get pods kubectl -n podinfo-demo get certificate
The certificate is issued when READY is True; the first issuance typically takes a minute or two.
Open https://podinfo.example.com in a browser. The start page shows the message from the secret. Or check the environment directly:
➜ curl -s https://podinfo.example.com/env | grep PODINFO_UI_MESSAGE "PODINFO_UI_MESSAGE=Hello from a Kubernetes Secret",
Verify data survives a pod restart. POST /store writes the payload to the volume and returns its hash:
➜ curl -s -X POST https://podinfo.example.com/store -d 'hello world' {"hash":"<hash>"} ➜ kubectl -n podinfo-demo delete pod -l app=podinfo ➜ kubectl -n podinfo-demo wait --for=condition=Ready pod -l app=podinfo ➜ curl -s https://podinfo.example.com/store/<hash> hello world
The replacement pod reads the data from the persistent volume — the container filesystem itself was discarded.
Clean Up¶
kubectl delete namespace podinfo-demo kubectl delete clusterissuer letsencrypt-ingress letsencrypt-gateway
Deleting the namespace removes the PVC, and the storage class's Delete reclaim policy removes the underlying Cinder volume and its data.
Next Steps¶
- Traffic Management — advanced routing with Gateway API
- Secret Management — encrypted secrets in Git
- Persistent Volumes — storage classes and volume expansion
- Observability — metrics and monitoring