Post 2: Setting up a workload cluster, and deploying a blog

Tanzu creates a Management Cluster, which is used to deploy one or more Workload Clusters which will actually run our applications.
To create a workload cluster follow the TCE docs. Make sure your kubectl config is set to the workload cluster (you can check this using kubectl config current-context
).
Installing kube-vip
Kubernetes runs workloads inside of pods. In order for those to be reachable outside of Kubernetes, they need to be exposed to the outside world using a service. Services come in 3 flavors: ClusterIP (the default, which only exposes a pod on the k8s network), NodePort (which exposes pods on a single IP using a range of ports), and LoadBalancer (which chooses from a range of IPs to assign to a pod). TCE does not come with a LoadBalancer implementation, allowing the user to choose one to use. We will be installing kube-vip (MetalLB is another choice).
To install kube-vip, we will be using a Carvel package created by fellow VMware employee Scott Rosenberg. You can follow Steps 1-5 in the excellent blog post by another VMware employee, William Lam.
If you want to be sure that your installation was successful, you could continue following William's tutorial and install the yelb demo application. Once you are done, you can uninstall yelb using kubectl delete all -n yelb
(deletes everything in the yelb namepace) and kubectl delete ns yelb
(deletes the namespace itself).
Hosting a Blog
As a part of the jumpbox installation helm was installed. Like Carvel, helm is a package manager for Kubernetes, which allows packages to be defined using charts, which can be installed with the helm install
command. Charts also define a set of values, variables which can be set by the user to customize the install.
In order to install Ghost we first need to add Bitnami's chart repository to helm.
helm repo add bitnami https://charts.bitnami.com/bitnami
We can then install ghost with the following command:
helm install ghost bitnami/ghost
helm install name-of-release location/of/chart
. Releases are helm's name for an instance of a chart. In our case we named the release "ghost".Running kubectl get all
you can get some of the kubernetes resources created by Ghost (note that unlike the name suggests "get all" does not show every Kubernetes resource, and excludes many e.g. ingresses and persistent volumes).
You can see that service/ghost is of type LoadBalancer has an external IP. This IP is assigned to it by kube-vip out of the pool of IPs specified in the kube-vip values file.

kubectl get all
NAME READY STATUS RESTARTS AGE
pod/ghost-mysql-0 1/1 Running 0 4m50s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ghost LoadBalancer 100.71.156.29 192.168.30.240 80:30201/TCP 4m51s
service/ghost-mysql ClusterIP 100.70.89.188 <none> 3306/TCP 4m51s
service/ghost-mysql-headless ClusterIP None <none> 3306/TCP 4m51s
service/kubernetes ClusterIP 100.64.0.1 <none> 443/TCP 5h32m
NAME READY AGE
statefulset.apps/ghost-mysql 1/1 4m51s
The eagle-eyed among you may notice the only pod is one for the database. That can't be right—where's the actual website?
As a part of the installation we needed to specify the hostname of the website. The problem is we don't know the IP address the Load Balancer will get until after the deployment.
No problem. We can simply get the address and other info, and then redeploy.
export APP_HOST=$(kubectl get svc --namespace default ghost --template "{{ range (index .status.loadBalancer.ingress 0) }}{{ . }}{{ end }}")
export GHOST_PASSWORD=$(kubectl get secret --namespace "default" ghost -o jsonpath="{.data.ghost-password}" | base64 -d)
export MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace "default" ghost-mysql -o jsonpath="{.data.mysql-root-password}" | base64 -d)
export MYSQL_PASSWORD=$(kubectl get secret --namespace "default" ghost-mysql -o jsonpath="{.data.mysql-password}" | base64 -d)
helm upgrade --namespace default ghost bitnami/ghost --set service.type=LoadBalancer,ghostHost=$APP_HOST,ghostPassword=$GHOST_PASSWORD,mysql.auth.rootPassword=$MYSQL_ROOT_PASSWORD,mysql.auth.password=$MYSQL_PASSWORD
Now when we run kubectl get all
we get the following output:
kubectl get all
NAME READY STATUS RESTARTS AGE
pod/ghost-75dd96d9f9-gc7dv 0/1 ContainerCreating 0 22s
pod/ghost-mysql-0 1/1 Running 0 5m35s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ghost LoadBalancer 100.71.156.29 192.168.30.240 80:30201/TCP 5m36s
service/ghost-mysql ClusterIP 100.70.89.188 <none> 3306/TCP 5m36s
service/ghost-mysql-headless ClusterIP None <none> 3306/TCP 5m36s
service/kubernetes ClusterIP 100.64.0.1 <none> 443/TCP 5h33m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/ghost 0/1 1 0 22s
NAME DESIRED CURRENT READY AGE
replicaset.apps/ghost-75dd96d9f9 1 1 0 22s
NAME READY AGE
statefulset.apps/ghost-mysql 1/1 5m35s
Once the ghost pod is created and ready you can go to the LoadBalancer external IP in your web browser, and you should see the ghost homepage.
In our next post we'll discuss exposing the blog to the internet, including support for HTTPS.