0 of 15 done
Prove It Works

Run the smoke test

Where you are

The cluster is built. The control plane is up, both worker nodes are ready, and pods can reach each other across nodes. Now you confirm that each core capability works from end to end: secret data is encrypted at rest, you can create a deployment, forward a port, read logs, run a command inside a container, and expose an application through a service. If these pass, you have a real working cluster.

Encrypt secret data at rest

First you verify that Kubernetes encrypts secret data before it is written to etcd, the database that holds cluster state.

Create a generic secret:

kubectl create secret generic kubernetes-the-hard-way \
  --from-literal="mykey=mydata"

Print a hexdump of the kubernetes-the-hard-way secret stored in etcd:

ssh root@server \
    'etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C'
00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|
00000010  73 2f 64 65 66 61 75 6c  74 2f 6b 75 62 65 72 6e  |s/default/kubern|
00000020  65 74 65 73 2d 74 68 65  2d 68 61 72 64 2d 77 61  |etes-the-hard-wa|
00000030  79 0a 6b 38 73 3a 65 6e  63 3a 61 65 73 63 62 63  |y.k8s:enc:aescbc|
00000040  3a 76 31 3a 6b 65 79 31  3a 4f 1b 80 d8 89 72 f4  |:v1:key1:O....r.|
00000050  60 8a 2c a0 76 1a e1 dc  98 d6 00 7a a4 2f f3 92  |`.,.v......z./..|
00000060  87 63 c9 22 f4 58 c8 27  b9 ff 2c 2e 1a b6 55 be  |.c.".X.'..,...U.|
00000070  d5 5c 4d 69 82 2f b7 e4  b3 b0 12 e1 58 c4 9c 77  |.\Mi./......X..w|
00000080  78 0c 1a 90 c9 c1 23 6c  73 8e 6e fd 8e 9c 3d 84  |x.....#ls.n...=.|
00000090  7d bf 69 81 ce c9 aa 38  be 3b dd 66 aa a3 33 27  |}.i....8.;.f..3'|
000000a0  df be 6d ac 1c 6d 8a 82  df b3 19 da 0f 93 94 1e  |..m..m..........|
000000b0  e0 7d 46 8d b5 14 d0 c5  97 e2 94 76 26 a8 cb 33  |.}F........v&..3|
000000c0  57 2a d0 27 a6 5a e1 76  a7 3f f0 b7 0a 7b ff 53  |W*.'.Z.v.?...{.S|
000000d0  cf c9 1a 18 5b 45 f8 b1  06 3b a9 45 02 76 23 61  |....[E...;.E.v#a|
000000e0  5e dc 86 cf 8e a4 d3 c9  5c 6a 6f e6 33 7b 5b 8f  |^.......\jo.3{[.|
000000f0  fb 8a 14 74 58 f9 49 2f  97 98 cc 5c d4 4a 10 1a  |...tX.I/...\.J..|
00000100  64 0a 79 21 68 a0 9e 7a  03 b7 19 e6 20 e4 1b ce  |d.y!h..z.... ...|
00000110  91 64 ce 90 d9 4f 86 ca  fb 45 2f d6 56 93 68 e1  |.d...O...E/.V.h.|
00000120  0b aa 8c a0 20 a6 97 fa  a1 de 07 6d 5b 4c 02 96  |.... ......m[L..|
00000130  31 70 20 83 16 f9 0a 22  5c 63 ad f1 ea 41 a7 1e  |1p ...."\c...A..|
00000140  29 1a d4 a4 e9 d7 0c 04  74 66 04 6d 73 d8 2e 3f  |).......tf.ms..?|
00000150  f0 b9 2f 77 bd 07 d7 7c  42 0a                    |../w...|B.|
0000015a

The etcd key should be prefixed with k8s:enc:aescbc:v1:key1, which indicates the aescbc provider was used to encrypt the data with the key1 encryption key. The secret value never appears in plain text in the dump.

Create a deployment

Next you create and manage a Deployment, which is the object Kubernetes uses to run and keep an application running.

Create a deployment for the nginx web server:

kubectl create deployment nginx \
  --image=nginx:latest

List the pod created by the nginx deployment:

kubectl get pods -l app=nginx
NAME                     READY   STATUS    RESTARTS   AGE
nginx-56fcf95486-c8dnx   1/1     Running   0          8s

Forward a port

Now you reach the running application from your own machine using port forwarding, which sends traffic from a local port straight to a port on a pod.

Retrieve the full name of the nginx pod:

POD_NAME=$(kubectl get pods -l app=nginx \
  -o jsonpath="{.items[0].metadata.name}")

Forward port 8080 on your local machine to port 80 of the nginx pod:

kubectl port-forward $POD_NAME 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

In a new terminal make an HTTP request using the forwarding address:

curl --head http://127.0.0.1:8080
HTTP/1.1 200 OK
Server: nginx/1.27.4
Date: Sun, 06 Apr 2025 17:17:12 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Wed, 05 Feb 2025 11:06:32 GMT
Connection: keep-alive
ETag: "67a34638-267"
Accept-Ranges: bytes

Switch back to the previous terminal and stop the port forwarding to the nginx pod:

Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80
Handling connection for 8080
^C

Read logs

Now you read the container logs, which is how you see what an application has printed while running.

Print the nginx pod logs:

kubectl logs $POD_NAME
...
127.0.0.1 - - [06/Apr/2025:17:17:12 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.88.1" "-"

The log shows the request you just made through the forwarded port.

Run a command inside a container

Now you run a command inside a running container, which is how you inspect or debug an application in place.

Print the nginx version by executing the nginx -v command in the nginx container:

kubectl exec -ti $POD_NAME -- nginx -v
nginx version: nginx/1.27.4

Expose the application with a service

Finally you expose the application using a Service, which gives a set of pods a stable way to receive traffic.

Expose the nginx deployment using a NodePort service:

kubectl expose deployment nginx \
  --port 80 --type NodePort

The LoadBalancer service type can not be used because your cluster is not configured with cloud provider integration. Setting up cloud provider integration is out of scope for this tutorial.

PHI lens. A NodePort opens the same port on every node and lets traffic from outside the cluster reach the application. In a healthcare cluster that is exactly how protected health information gets exposed by accident, so confirm here that you understand what you just opened. Before any real workload you would lock this down, for example with a firewall rule or an ingress controller, instead of leaving a port open on every node.

Retrieve the node port assigned to the nginx service:

NODE_PORT=$(kubectl get svc nginx \
  --output=jsonpath='{range .spec.ports[0]}{.nodePort}')

Retrieve the hostname of the node running the nginx pod:

NODE_NAME=$(kubectl get pods \
  -l app=nginx \
  -o jsonpath="{.items[0].spec.nodeName}")

Make an HTTP request using the IP address and the nginx node port:

curl -I http://${NODE_NAME}:${NODE_PORT}
Server: nginx/1.27.4
Date: Sun, 06 Apr 2025 17:18:36 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Wed, 05 Feb 2025 11:06:32 GMT
Connection: keep-alive
ETag: "67a34638-267"
Accept-Ranges: bytes

The request reaches nginx through the node port. Every core capability now works.

Tear down

When you are done with the cluster, you remove the machines you created.

Earlier versions of this guide created cloud resources for compute and networking. This version does not. All configuration lives on the jumpbox, the server, and the nodes, so there is nothing in a cloud account to unwind.

Cleanup is just deleting the virtual machines you created for this exercise. Stop them if you want to come back later, or delete them to free the resources for good. Once they are gone, the cluster is gone.

Key terms

Deployment: A Kubernetes object that runs an application and keeps it running. You tell it which container image to use and how many copies you want, and it creates the pods and replaces any that fail.

Port forward: A way to connect a port on your local machine directly to a port on a pod. Traffic you send to the local port arrives at the pod, which lets you reach an application without exposing it to anyone else.

NodePort service: A Service that opens the same port on every node in the cluster. Any request that reaches a node on that port is routed to the application, including requests from outside the cluster.

Reflect

You just confirmed the cluster runs a stock nginx image with default settings. If you needed this cluster to run a specific healthcare application instead, which of these tests would you change first, and what would you check that you did not check here?

Next: Optional extension: run a PHI scrubber on your cluster