Skip to content

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 KUBECONFIG environment variable.
  • kubectl installed 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