Skip to main content

Deploy the Kubernetes Guestbook App

15 min

The Guestbook is a simple, multi-tier web application that uses Redis and Nginx, powered by Docker containers and Kubernetes. In this tutorial, you will build and deploy the standard Kubernetes Guestbook example with Pulumi.

Unlike the original example, which is authored in YAML and deployed with kubectl, you will author this one in a general-purpose language and deploy it with pulumi. This gives you the full power of real programming languages, combined with immutable infrastructure, for a robust and repeatable update experience.

You will stand up a Redis leader, Redis replicas, and the Guestbook frontend, expose and view the frontend service, perform an incremental update that scales the frontend, and then clean everything up.

What you'll learn
  • How to model a multi-tier app — Redis leader, Redis replicas, and an Nginx frontend — as Kubernetes Deployments and Services in code
  • How to parameterize the frontend Service between ClusterIP and LoadBalancer with configuration
  • How to view the running application with kubectl and pulumi stack output
  • How Pulumi computes the minimal set of changes when you update your program
  • How to tear the application down cleanly
Prerequisites

In this tutorial, you will build and deploy the standard Kubernetes Guestbook example using Pulumi.

The Guestbook is a simple, multi-tier web application that uses Redis and Nginx, powered by Docker containers and Kubernetes. The primary difference between this example and the standard Kubernetes one is that you will author it in a general-purpose language instead of YAML, and deploy it with pulumi rather than kubectl. This gives you the full power of general-purpose languages, combined with immutable infrastructure, delivering a robust and repeatable update experience.

The code for this tutorial is available on GitHub.

Objectives#

  • Start up a Redis leader
  • Start up Redis replicas
  • Start up the Guestbook frontend
  • Expose and view the frontend service
  • Clean up

Before you begin#

You need to have the Pulumi CLI and a working Kubernetes cluster.

  1. Install Pulumi
  2. Connect Pulumi to a Kubernetes Cluster

Running the Guestbook#

The Guestbook application uses Redis to store its data. It writes its data to a Redis leader instance and reads data from multiple Redis replica instances.

Normally you would write YAML files to configure them, and then run kubectl commands to create and manage the services. Instead of doing that, you will author your program in code and deploy it with pulumi.

To start, you’ll need to create a project and stack (a deployment target) for your new project.

Create and configure a project#

To create a new Pulumi project, use a template:

Terminal window
mkdir k8s-guestbook && cd k8s-guestbook
pulumi new kubernetes-typescript

This command will initialize a fresh project in the newly-created k8s-guestbook directory.

Next, replace the minimal contents of the template’s main file with the full Guestbook code:

index.ts
import * as k8s from "@pulumi/kubernetes";
import * as pulumi from "@pulumi/pulumi";
// Create only services of type `ClusterIP`
// for clusters that don't support `LoadBalancer` services
const config = new pulumi.Config();
const useLoadBalancer = config.getBoolean("useLoadBalancer");
//
// REDIS LEADER.
//
const redisLeaderLabels = { app: "redis-leader" };
const redisLeaderDeployment = new k8s.apps.v1.Deployment("redis-leader", {
spec: {
selector: { matchLabels: redisLeaderLabels },
template: {
metadata: { labels: redisLeaderLabels },
spec: {
containers: [
{
name: "redis-leader",
image: "redis",
resources: { requests: { cpu: "100m", memory: "100Mi" } },
ports: [{ containerPort: 6379 }],
},
],
},
},
},
});
const redisLeaderService = new k8s.core.v1.Service("redis-leader", {
metadata: {
name: "redis-leader",
labels: redisLeaderDeployment.metadata.labels,
},
spec: {
ports: [{ port: 6379, targetPort: 6379 }],
selector: redisLeaderDeployment.spec.template.metadata.labels,
},
});
//
// REDIS REPLICA.
//
const redisReplicaLabels = { app: "redis-replica" };
const redisReplicaDeployment = new k8s.apps.v1.Deployment("redis-replica", {
spec: {
selector: { matchLabels: redisReplicaLabels },
template: {
metadata: { labels: redisReplicaLabels },
spec: {
containers: [
{
name: "replica",
image: "pulumi/guestbook-redis-replica",
resources: { requests: { cpu: "100m", memory: "100Mi" } },
// If your cluster config does not include a dns service, then to instead access an environment
// variable to find the leader's host, change `value: "dns"` to read `value: "env"`.
env: [{ name: "GET_HOSTS_FROM", value: "dns" }],
ports: [{ containerPort: 6379 }],
},
],
},
},
},
});
const redisReplicaService = new k8s.core.v1.Service("redis-replica", {
metadata: {
name: "redis-replica",
labels: redisReplicaDeployment.metadata.labels
},
spec: {
ports: [{ port: 6379, targetPort: 6379 }],
selector: redisReplicaDeployment.spec.template.metadata.labels,
},
});
//
// FRONTEND
//
const frontendLabels = { app: "frontend" };
const frontendDeployment = new k8s.apps.v1.Deployment("frontend", {
spec: {
selector: { matchLabels: frontendLabels },
replicas: 3,
template: {
metadata: { labels: frontendLabels },
spec: {
containers: [
{
name: "frontend",
image: "pulumi/guestbook-php-redis",
resources: { requests: { cpu: "100m", memory: "100Mi" } },
// If your cluster config does not include a dns service, then to instead access an environment
// variable to find the leader's host, change `value: "dns"` to read `value: "env"`.
env: [{ name: "GET_HOSTS_FROM", value: "dns" /* value: "env"*/ }],
ports: [{ containerPort: 80 }],
},
],
},
},
},
});
const frontendService = new k8s.core.v1.Service("frontend", {
metadata: {
labels: frontendDeployment.metadata.labels,
name: "frontend",
},
spec: {
type: useLoadBalancer ? "LoadBalancer" : "ClusterIP",
ports: [{ port: 80 }],
selector: frontendDeployment.spec.template.metadata.labels,
},
});
// Export the frontend IP.
export let frontendIp: pulumi.Output<string>;
if (useLoadBalancer) {
frontendIp = frontendService.status.loadBalancer.ingress[0].ip;
} else {
frontendIp = frontendService.spec.clusterIP;
}

This code creates three Kubernetes Services, each with an associated Deployment. The full Kubernetes object model is available to you, giving you the full power of Kubernetes right away.

(Optional) By default, your frontend Service will be of type ClusterIP. This will work on Minikube and similar dev/local clusters; however, for most production Kubernetes clusters, you’ll want a LoadBalancer Service to ensure a load balancer gets allocated in your target cloud environment.

The above code uses configuration to make this parameterizable. If you’d like your program to use a load balancer, simply run:

Terminal window
pulumi config set useLoadBalancer true

If you’re not sure, it’s safe to skip this step.

Deploying#

Now you’re ready to deploy your code. To do so, simply run pulumi up:

$ pulumi up

The command will first show you a complete preview of what will take place, with a confirmation prompt. No changes will have been made yet. It should look something like this:

Previewing update of stack 'k8s-guestbook-dev'
Previewing changes:
Type Name Plan Info
+ pulumi:pulumi:Stack k8s-guestbook-k8s-guestbook-dev create
+ ├─ kubernetes:core:Service redis-leader create
+ ├─ kubernetes:core:Service redis-replica create
+ ├─ kubernetes:core:Service frontend create
+ ├─ kubernetes:apps:Deployment redis-leader create
+ ├─ kubernetes:apps:Deployment redis-replica create
+ └─ kubernetes:apps:Deployment frontend create
info: 7 changes previewed:
+ 7 resources to create
Do you want to perform this update?
> yes
no
details

Select “yes” and hit enter. The deployment will proceed, and the output will look like this:

Updating stack 'k8s-guestbook-dev'
Performing changes:
Type Name Status Info
+ pulumi:pulumi:Stack k8s-guestbook-k8s-guestbook-dev created
+ ├─ kubernetes:core:Service redis-replica created 1 info message
+ ├─ kubernetes:core:Service frontend created 1 info message
+ ├─ kubernetes:core:Service redis-leader created 1 info message
+ ├─ kubernetes:apps:Deployment redis-leader created
+ ├─ kubernetes:apps:Deployment redis-replica created
+ └─ kubernetes:apps:Deployment frontend created
Diagnostics:
kubernetes:core:Service: redis-replica
info: ✅ Service 'redis-replica' successfully created endpoint objects
kubernetes:core:Service: frontend
info: ✅ Service 'frontend' successfully created endpoint objects
kubernetes:core:Service: redis-leader
info: ✅ Service 'redis-leader' successfully created endpoint objects
---outputs:---
frontendIP: "10.102.193.86"
info: 7 changes performed:
+ 7 resources created
Update duration: 16.226520447s
Permalink: https://app.pulumi.com/joeduffy/k8s-guestbook-dev/updates/1

Viewing the Guestbook#

The application is now running in your cluster. Let’s inspect the cluster state to validate the deployment.

Use kubectl to see the deployed services:

Terminal window
kubectl get services

You should see entries for each of the four Services you’ve created in your program:

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend ClusterIP 10.102.193.86 <none> 80/TCP 2m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 1d
redis-leader ClusterIP 10.98.205.37 <none> 6379/TCP 2m
redis-replica ClusterIP 10.96.9.70 <none> 6379/TCP 2m

The pulumi stack output command prints exported program variables:

Terminal window
pulumi stack output frontendIP

The value of the frontendIP variable matches either frontend’s CLUSTER-IP (if you’re deploying with useLoadBalancer set to false) or frontend’s EXTERNAL-IP (if you’re deploying with useLoadBalancer set to true). For example:

10.102.193.86

Now let’s see the Guestbook application in action.

Guestbook in browser

Without a LoadBalancer

As the example above uses ClusterIP, in order to access it in a browser over HTTP, you must first forward a local port on localhost to it. To do so, run:

Terminal window
kubectl port-forward svc/frontend 8765:80

At this point, you can view your running Guestbook application:

Terminal window
curl localhost:8765

The HTML from the Guestbook will be fetched and printed:

<html ng-app="redis">
<head>
<title>Guestbook</title>
...
</html>

With a LoadBalancer

If you are instead running this program in a full-featured production cluster, and set useLoadBalancer to true earlier, then you can simply access your Guestbook application with:

Terminal window
curl $(pulumi stack output frontendIP)

The HTML from the Guestbook will be fetched and printed:

<html ng-app="redis">
<head>
<title>Guestbook</title>
...
</html>

Making an incremental update#

You’re almost done. To demonstrate incremental updates, however, let’s make an update to your program to scale the frontend from 3 replicas to 5. Find the line:

replicas: 3,

and change it to:

replicas: 5,

Or simply run sed -i "s/replicas: 3/replicas: 5/g" index.ts.

Now all you need to do is run pulumi up, and Pulumi will figure out the minimal set of changes to make:

Terminal window
pulumi up -y --skip-preview

The output from running this command should look something like this:

Updating stack 'k8s-guestbook-dev'
Performing changes:
Type Name Plan Info
* pulumi:pulumi:Stack k8s-guestbook-k8s-guestbook-dev no change
~ └─ kubernetes:apps:Deployment frontend updated changes: ~ spec
info: 1 change performed:
~ 1 resource updated
6 resources unchanged

Cleaning up#

Feel free to experiment. As soon as you’re done, let’s clean up and destroy the resources and remove the stack:

Terminal window
pulumi destroy --yes
pulumi stack rm --yes

Afterwards, query the list of pods to verify that none are remaining:

Terminal window
kubectl get pods

If your cluster is empty, you will see output along the following lines:

No resources found.

Of course, if you have other applications deployed, you should still see them, but not the Guestbook.

Related

The infrastructure as code platform for any cloud.