diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/clusters_in_vsphere.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/clusters_in_vsphere.png new file mode 100644 index 0000000..dc90bb5 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/clusters_in_vsphere.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/coffee_break.gif b/content/posts/2022/tanzu-community-edition-k8s-homelab/coffee_break.gif new file mode 100644 index 0000000..de5596e Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/coffee_break.gif differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/container_volume_in_vsphere.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/container_volume_in_vsphere.png new file mode 100644 index 0000000..d80e711 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/container_volume_in_vsphere.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/create_new_agent.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/create_new_agent.png new file mode 100644 index 0000000..f43c0fe Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/create_new_agent.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/creating_new_subnet.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/creating_new_subnet.png new file mode 100644 index 0000000..f53772a Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/creating_new_subnet.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/dhcp_reservations.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/dhcp_reservations.png new file mode 100644 index 0000000..26fb086 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/dhcp_reservations.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/five_minutes.gif b/content/posts/2022/tanzu-community-edition-k8s-homelab/five_minutes.gif new file mode 100644 index 0000000..e195211 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/five_minutes.gif differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/index.md b/content/posts/2022/tanzu-community-edition-k8s-homelab/index.md new file mode 100644 index 0000000..06500c6 --- /dev/null +++ b/content/posts/2022/tanzu-community-edition-k8s-homelab/index.md @@ -0,0 +1,988 @@ +--- +title: "VMware Tanzu Community Edition Kubenertes Platform in a Homelab" # Title of the blog post. +date: 2022-01-12 # Date of post creation. +# lastmod: 2022-01-06T09:42:51-06:00 # Date when last modified +description: "Gaining familiarity with VMware Tanzu Community Edition by deploying phpIPAM on Kubernetes in my homelab" # Description used for search engine. +featured: false # Sets if post is a featured post, making appear on the home page side bar. +draft: false # Sets whether to render this page. Draft of true will not be rendered. +toc: true # Controls if a table of contents should be generated for first-level links automatically. +usePageBundles: true +# menu: main +# featureImage: "file.png" # Sets featured image on blog post. +# featureImageAlt: 'Description of image' # Alternative text for featured image. +# featureImageCap: 'This is the featured image.' # Caption (optional). +thumbnail: "/tanzu_community_edition.png" # Sets thumbnail image appearing inside card on homepage. +# shareImage: "share.png" # Designate a separate image for social media sharing. +codeLineNumbers: false # Override global value for showing of line numbers within code block. +series: Projects +tags: + - vmware + - linux + - kubernetes + - docker + - containers + - tanzu + - homelab +comment: true # Disable comment if false. +--- + +Back in October, VMware [announced](https://tanzu.vmware.com/content/blog/vmware-tanzu-community-edition-announcement) [Tanzu Community Edition](https://tanzucommunityedition.io/) as way to provide "a full-featured, easy-to-manage Kubernetes platform that’s perfect for users and learners alike." TCE bundles a bunch of open-source components together in a modular, "batteries included but swappable" way: +![Tanzu Community Edition components](/tanzu_community_edition.png) + +I've been meaning to brush up on my Kubernetes skills so I thought deploying and using TCE in my self-contained [homelab](/vmware-home-lab-on-intel-nuc-9/) would be a fun and rewarding learning exercise - and it was! + +Here's how I did it. + +### Planning +TCE supports several different deployment scenarios and targets. It can be configured as separate Management and Workload Clusters or as a single integrated Standalone Cluster, and deployed to cloud providers like AWS and Azure, on-premise vSphere, or even a local Docker environment[^yo_dawg]. I'll be using the standard Management + Workload Cluster setup in my on-prem vSphere, so I start by reviewing the [Prepare to Deploy a Cluster to vSphere](https://tanzucommunityedition.io/docs/latest/vsphere/) documentation to get an idea of what I'll need. + +Looking ahead, part of the installation process creates a local [KIND](https://kind.sigs.k8s.io/) cluster for bootstrapping the Management and Workload clusters. I do most of my home computing (and homelab work) by using the [Linux environment available on my Chromebook](/setting-up-linux-on-a-new-lenovo-chromebook-duet-bonus-arm64-complications/). Unfortunately I know from past experience that KIND will not work within this environment so I'll be using a Debian 10 VM to do the deployment. + +[^yo_dawg]: Yo dawg, I heard you like containers... + +#### Networking +The Kubernetes node VMs will need to be attached to a network with a DHCP server to assign their addresses, and that network will need to be able to talk to vSphere. My router handles DHCP for the range `192.168.1.101-250` so I'll plan on using that. + +I'll also need to set aside a few static IPs for this project. These will need to be routable and within the same subnet as the DHCP range, but excluded from that DHCP range. + +| IP Address | Purpose | +| --- | --- | +| `192.168.1.60` | Control plane for Management cluster | +| `192.168.1.61` | Control plane for Workload cluster | +| `192.168.1.64 - 192.168.1.80` | IP range for Workload load balancer | + + +### Prerequisites +Moving on to the [Getting Started](https://tanzucommunityedition.io/docs/latest/getting-started/), I'll need to grab some software before I can actually Get Started. + +#### Kubernetes control plane image +I need to download a VMware OVA which can be used for deploying my Kubernetes nodes from the VMWare Customer Connect portal [here](https://customerconnect.vmware.com/downloads/get-download?downloadGroup=TCE-090)[^register]. There are a few different options available. I'll get the Photon release with the highest Kubernetes version currently available, `photon-3-kube-v1.21.2+vmware.1-tkg.2-12816990095845873721.ova`. + +Once the file is downloaded, I'll log into my vCenter and use the **Deploy OVF Template** action to deploy a new VM using the OVA. I won't bother booting the machine once deployed but will rename it to `k8s-node` to make it easier to identify later on and then convert it to a template. +![New k8s-node template](/k8s-node_template.png) + +[^register]: Register [here](https://customerconnect.vmware.com/account-registration) if you don't yet have an account. + +#### Docker +I've already got Docker installed on this machine, but if I didn't I would follow the instructions [here](https://docs.docker.com/engine/install/debian/) to get it installed and then follow [these instructions](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user) to enable management of Docker without root. + +I also verify that my install is using `cgroup` version 1 as version 2 is not currently supported: + +```bash +❯ docker info | grep -i cgroup + Cgroup Driver: cgroupfs + Cgroup Version: 1 +``` + +#### `kubectl` binary +Next up, I'll install `kubectl` [as described here](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/) - though the latest version is currently `1.23` and that won't work with the `1.21` control plane node image I downloaded from VMware (`kubectl` needs to be within one minor version of the control plane). Instead I need to find the latest `1.22` release. + +I can look at the [releases page on GithHub](https://github.com/kubernetes/kubernetes/releases) to see that the latest release for me is `1.22.5`. With this newfound knowledge I can follow the [Install kubectl binary with curl on Linux](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/#install-kubectl-binary-with-curl-on-linux) instructions to grab that specific version: + +```bash +❯ curl -LO https://dl.k8s.io/release/v1.22.5/bin/linux/amd64/kubectl + + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 154 100 154 0 0 2298 0 --:--:-- --:--:-- --:--:-- 2298 +100 44.7M 100 44.7M 0 0 56.9M 0 --:--:-- --:--:-- --:--:-- 56.9M + +❯ sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + +[sudo] password for john: + +❯ kubectl version --client +Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.5", GitCommit:"5c99e2ac2ff9a3c549d9ca665e7bc05a3e18f07e", GitTreeState:"clean", BuildDate:"2021-12-16T08:38:33Z", GoVersion:"go1.16.12", Compiler:"gc", Platform:"linux/amd64"} +``` + +#### `kind` binary +It's not strictly a requirement, but having the `kind` executable available will be handy for troubleshooting during the bootstrap process in case anything goes sideways. It can be installed in basically the same was as `kubectl`: + +```bash +❯ curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64 + + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 98 100 98 0 0 513 0 --:--:-- --:--:-- --:--:-- 513 +100 655 100 655 0 0 2212 0 --:--:-- --:--:-- --:--:-- 10076 +100 6660k 100 6660k 0 0 11.8M 0 --:--:-- --:--:-- --:--:-- 11.8M + +❯ sudo install -o root -g root -m 0755 kind /usr/local/bin/kind + +❯ kind version +kind v0.11.1 go1.16.5 linux/amd64 +``` + +#### Tanzu CLI +The final bit of required software is the Tanzu CLI, which can be downloaded from the [project on GitHub](https://github.com/vmware-tanzu/community-edition/releases). + +```bash +curl -H "Accept: application/vnd.github.v3.raw" \ + -L https://api.github.com/repos/vmware-tanzu/community-edition/contents/hack/get-tce-release.sh | \ + bash -s v0.9.1 linux +``` + +And then unpack it and run the installer: +```bash +tar xf tce-linux-amd64-v0.9.1.tar.gz +cd tce-linux-amd64-v0.9.1 +./install.sh +``` + +I can then verify the installation is working correctly: +```bash +❯ tanzu version +version: v0.2.1 +buildDate: 2021-09-29 +sha: ceaa474 +``` + +### Cluster creation +Okay, now it's time for the good stuff - creating some shiny new Tanzu clusters! The Tanzu CLI really does make this very easy to accomplish. + +#### Management cluster +I need to create a Management cluster first and I'd like to do that with the UI, so that's as simple as: +```bash +tanzu management-cluster create --ui +``` + +I should then be able to access the UI by pointing a web browser at `http://127.0.0.1:8080`... but I'm running this on a VM without a GUI, so I'll need to back up and tell it to bind on `0.0.0.0:8080` so the web installer will be accessible across the network. I can also include `--browser none` so that the installer doesn't bother with trying to launch a browser locally. + +```bash +❯ tanzu management-cluster create --ui --bind 0.0.0.0:8080 --browser none + +Validating the pre-requisites... +Serving kickstart UI at http://[::]:8080 +``` + +*Now* I can point my local browser to my VM and see the UI: +![The Tanzu Installer UI](/installer_ui.png) + +And then I can click the button at the bottom left to save my eyes[^dark_mode] before selecting the option to deploy on vSphere. +![Configuring the IaaS Provider](/installer_iaas_provider.png) + +I'll plug in the FQDN of my vCenter and provide a username and password to use to connect to it, then hit the **Connect** button. That will prompt me to accept the vCenter's certificate thumbprint, and then I'll be able to select the virtual datacenter that I want to use. Finally, I'll paste in the SSH public key[^gen_key] I'll use for interacting with the cluster. + +I click **Next** and move on to the Management Cluster Settings. +![Configuring the Management Cluster](/installer_management_cluster.png) + +This is for a lab environment that's fairly memory-constrained, so I'll pick the single-node *Development* setup with a *small* instance type. I'll name the cluster `tce-mgmt` and stick with the default `kube-vip` control plane endpoint provider. I plug in the control plane endpoint IP that I'll use for connecting to the cluster and select the *small* instance type for the worker node type. + +I don't have an NSX Advanced Load Balancer or any Metadata to configure so I'll skip past those steps and move on to configuring the Resources. +![Configuring Resources](/installer_resources.png) + +Here I pick to place the Tanzu-related resources in a VM folder named `Tanzu`, to store their data on my single host's single datastore, and to deploy to the one-host `physical-cluster` cluster. + +Now for the Kubernetes Networking Settings: +![Configuring Kubernetes Networking](/installer_k8s_networking.png) + +This bit is actually pretty easy. For Network Name, I select the vSphere network where the `192.168.1.0/24` network I identified earlier lives, `d-Home-Mgmt`. I leave the service and pod CIDR ranges as default. + +I disable the Identity Management option and then pick the `k8s-node` template I had imported to vSphere earlier. +![Configuring the OS Image](/installer_image.png) + +I skip the Tanzu Mission Control piece (since I'm still waiting on access to [TMC Starter](https://tanzu.vmware.com/tmc-starter)) and click the **Review Configuration** button at the bottom of the screen to review my selections. +![Reviewing the configuration](/installer_review.png) + +See the option at the bottom to copy the CLI command? I'll need to use that since clicking the friendly **Deploy** button doesn't seem to work while connected to the web server remotely. + +```bash +tanzu management-cluster create --file /home/john/.config/tanzu/tkg/clusterconfigs/dr94t3m2on.yaml -v 6 +``` + +In fact, I'm going to copy that file into my working directory and give it a more descriptive name so that I can re-use it in the future. + +```bash +cp ~/.config/tanzu/tkg/clusterconfigs/dr94t3m2on.yaml ~/projects/tanzu-homelab/tce-mgmt.yaml +``` + +Now I can run the install command: + +```bash +tanzu management-cluster create --file ./tce-mgmt.yaml -v 6 +``` + +After a moment or two of verifying prerequisites, I'm met with a polite offer to enable Tanzu Kubernetes Grid Service in vSphere: + +``` +vSphere 7.0 Environment Detected. + +You have connected to a vSphere 7.0 environment which does not have vSphere with Tanzu enabled. vSphere with Tanzu includes +an integrated Tanzu Kubernetes Grid Service which turns a vSphere cluster into a platform for running Kubernetes workloads in dedicated +resource pools. Configuring Tanzu Kubernetes Grid Service is done through vSphere HTML5 client. + +Tanzu Kubernetes Grid Service is the preferred way to consume Tanzu Kubernetes Grid in vSphere 7.0 environments. Alternatively you may +deploy a non-integrated Tanzu Kubernetes Grid instance on vSphere 7.0. +Note: To skip the prompts and directly deploy a non-integrated Tanzu Kubernetes Grid instance on vSphere 7.0, you can set the 'DEPLOY_TKG_ON_VSPHERE7' configuration variable to 'true' + +Do you want to configure vSphere with Tanzu? [y/N]: n +Would you like to deploy a non-integrated Tanzu Kubernetes Grid management cluster on vSphere 7.0? [y/N]: y +``` + +That's not what I'm after in this case, though, so I'll answer with a `n` and a `y` to confirm that I want the non-integrated TKG deployment. + +And now I go get coffee as it'll take 10-15 minutes for the deployment to complete. +![Coffee break!](/coffee_break.gif) + +Okay, I'm back - and so is my shell prompt! The deployment completed successfully: +``` +Waiting for additional components to be up and running... +Waiting for packages to be up and running... +Context set for management cluster tce-mgmt as 'tce-mgmt-admin@tce-mgmt'. + +Management cluster created! + + +You can now create your first workload cluster by running the following: + + tanzu cluster create [name] -f [file] + + +Some addons might be getting installed! Check their status by running the following: + + kubectl get apps -A + +``` + +I can run that last command to go ahead and verify that the addon installation has completed: + +```bash +❯ kubectl get apps -A +NAMESPACE NAME DESCRIPTION SINCE-DEPLOY AGE +tkg-system antrea Reconcile succeeded 26s 6m49s +tkg-system metrics-server Reconcile succeeded 36s 6m49s +tkg-system tanzu-addons-manager Reconcile succeeded 22s 8m54s +tkg-system vsphere-cpi Reconcile succeeded 19s 6m50s +tkg-system vsphere-csi Reconcile succeeded 36s 6m50s +``` + +And I can use the Tanzu CLI to get some other details about the new management cluster: +```bash +❯ tanzu management-cluster get tce-mgmt + NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES + tce-mgmt tkg-system running 1/1 1/1 v1.21.2+vmware.1 management + + +Details: + +NAME READY SEVERITY REASON SINCE MESSAGE +/tce-mgmt True 40m +├─ClusterInfrastructure - VSphereCluster/tce-mgmt True 41m +├─ControlPlane - KubeadmControlPlane/tce-mgmt-control-plane True 40m +│ └─Machine/tce-mgmt-control-plane-xtdnx True 40m +└─Workers + └─MachineDeployment/tce-mgmt-md-0 + └─Machine/tce-mgmt-md-0-745b858d44-4c9vv True 40m + + +Providers: + + NAMESPACE NAME TYPE PROVIDERNAME VERSION WATCHNAMESPACE + capi-kubeadm-bootstrap-system bootstrap-kubeadm BootstrapProvider kubeadm v0.3.23 + capi-kubeadm-control-plane-system control-plane-kubeadm ControlPlaneProvider kubeadm v0.3.23 + capi-system cluster-api CoreProvider cluster-api v0.3.23 + capv-system infrastructure-vsphere InfrastructureProvider vsphere v0.7.10 +``` + + +Excellent! Things are looking good so I can move on to create the cluster which will actually run my workloads. + +[^dark_mode]: Enabling dark mode is probably the most important part of this process. +[^gen_key]: If I didn't already have a key pair to use I would generate one with `ssh-keygen -t rsa -b 4096 -C "email@example.com"` and add it to my client with `ssh-add ~/.ssh/id_rsa`. +#### Workload cluster +I won't use the UI for this but will instead take a copy of my `tce-mgmt.yaml` file and adapt it to suit the workload needs (as described [here](https://tanzucommunityedition.io/docs/latest/workload-clusters/)). + +```bash +cp tce-mgmt.yaml tce-work.yaml +vi tce-work.yaml +``` + +I only need to change 2 of the parameters in this file: +- `CLUSTER_NAME`: from `tce-mgmt` to `tce-work` +- `VSPHERE_CONTROL_PLANE_ENDPOINT`: from `192.168.1.60` to `192.168.1.61` + +I *could* change a few others if I wanted to[^i_wont]: +- (Optional) `CLUSTER_PLAN` to change between `dev`/`prod` plans independently +- (Optional) `CONTROL_PLANE_MACHINE_COUNT` to deploy an increased number of control plane nodes (must but an odd integer) +- (Optional) `WORKER_MACHINE_COUNT` to add worker nodes +- (Optional) `NAMESPACE` to deploy the cluster in a specific Kubernetes namespace +- (Optional) `OS_NAME` and `OS_VERSION` to use a different machine image for the workload cluster + +After saving my changes to the `tce-work.yaml` file, I'm ready to deploy the cluster: + +```bash +❯ tanzu cluster create --file tce-work.yaml +Validating configuration... +Warning: Pinniped configuration not found. Skipping pinniped configuration in workload cluster. Please refer to the documentation to check if you can configure pinniped on workload cluster manually +Creating workload cluster 'tce-work'... +Waiting for cluster to be initialized... +Waiting for cluster nodes to be available... +Waiting for addons installation... +Waiting for packages to be up and running... + +Workload cluster 'tce-work' created +``` + +Right on! I'll use `tanzu cluster get` to check out the workload cluster: +```bash +❯ tanzu cluster get tce-work + NAME NAMESPACE STATUS CONTROLPLANE WORKERS KUBERNETES ROLES + tce-work default running 1/1 1/1 v1.21.2+vmware.1 +ℹ + +Details: + +NAME READY SEVERITY REASON SINCE MESSAGE +/tce-work True 9m31s +├─ClusterInfrastructure - VSphereCluster/tce-work True 10m +├─ControlPlane - KubeadmControlPlane/tce-work-control-plane True 9m31s +│ └─Machine/tce-work-control-plane-8km9m True 9m31s +└─Workers + └─MachineDeployment/tce-work-md-0 + └─Machine/tce-work-md-0-687444b744-cck4x True 8m31s +``` + +I can also go into vCenter and take a look at the VMs which constitute the two clusters: +![Cluster VMs](/clusters_in_vsphere.png) + +I've highlighted the two Control Plane nodes. They got their IP addresses assigned by DHCP, but [VMware says](https://tanzucommunityedition.io/docs/latest/verify-deployment/#configure-dhcp-reservations-for-the-control-plane-nodes-vsphere-only) that I need to create reservations for them to make sure they don't change. So I'll do just that. +![DHCP reservations on Google Wifi](/dhcp_reservations.png) + +Excellent, I've got a Tanzu management cluster and a Tanzu workload cluster. What now? + +[^i_wont]: I'm not going to, but I totally could. + +### Working with Tanzu + +If I run `kubectl get nodes` right now, I'll only get information about the management cluster: + +```bash +❯ kubectl get nodes +NAME STATUS ROLES AGE VERSION +tce-mgmt-control-plane-xtdnx Ready control-plane,master 18h v1.21.2+vmware.1 +tce-mgmt-md-0-745b858d44-4c9vv Ready 17h v1.21.2+vmware.1 +``` + +#### Setting the right context +To be able to deploy stuff to the workload cluster, I need to tell `kubectl` how to talk to it. And to do that, I'll first need to use `tanzu` to capture the cluster's kubeconfig: + +```bash +❯ tanzu cluster kubeconfig get tce-work --admin +Credentials of cluster 'tce-work' have been saved +You can now access the cluster by running 'kubectl config use-context tce-work-admin@tce-work' +``` + +I can now run `kubectl config get-contexts` and see that I have access to contexts on both management and workload clusters: + +```bash +❯ kubectl config get-contexts +CURRENT NAME CLUSTER AUTHINFO NAMESPACE +* tce-mgmt-admin@tce-mgmt tce-mgmt tce-mgmt-admin + tce-work-admin@tce-work tce-work tce-work-admin +``` + +And I can switch to the `tce-work` cluster like so: + +```bash +❯ kubectl config use-context tce-work-admin@tce-work +Switched to context "tce-work-admin@tce-work". +❯ kubectl get nodes +NAME STATUS ROLES AGE VERSION +tce-work-control-plane-8km9m Ready control-plane,master 17h v1.21.2+vmware.1 +tce-work-md-0-687444b744-cck4x Ready 17h v1.21.2+vmware.1 +``` + +There they are! + +#### Deploying the `yelb` demo app +Before I move on to deploying actually *useful* workloads, I'll start with deploying a quick demo application as described in William Lam's post on [Interesting Kubernetes application demos](https://williamlam.com/2020/06/interesting-kubernetes-application-demos.html). `yelb` is a web app which consists of a UI front end, application server, database server, and Redis caching service so it's a great little demo to make sure Kubernetes is working correctly. + +I can check out the sample deployment that William put together [here](https://github.com/lamw/vmware-k8s-app-demo/blob/master/yelb.yaml), and then deploy it with: + +```bash +❯ kubectl create ns yelb +namespace/yelb created + +❯ kubectl apply -f https://raw.githubusercontent.com/lamw/vmware-k8s-app-demo/master/yelb.yaml +service/redis-server created +service/yelb-db created +service/yelb-appserver created +service/yelb-ui created +deployment.apps/yelb-ui created +deployment.apps/redis-server created +deployment.apps/yelb-db created +deployment.apps/yelb-appserver created + +❯ kubectl -n yelb get pods +NAME READY STATUS RESTARTS AGE +redis-server-74556bbcb7-r9jqc 1/1 Running 0 10s +yelb-appserver-d584bb889-2jspg 1/1 Running 0 10s +yelb-db-694586cd78-wb8tt 1/1 Running 0 10s +yelb-ui-8f54fd88c-k2dw9 1/1 Running 0 10s +``` + +Once the app is running, I can point my web browser at it to see it in action. But what IP do I use? + +```bash +❯ kubectl -n yelb get svc/yelb-ui +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +yelb-ui NodePort 100.71.228.116 80:30001/TCP 84s +``` + +This demo is using a `NodePort` type service to expose the front end, which means it will be accessible on port `30001` on the node it's running on. I can find that IP by: +```bash +❯ kubectl -n yelb describe pod $(kubectl -n yelb get pods | grep yelb-ui | awk '{print $1}') | grep "Node:" +Node: tce-work-md-0-687444b744-cck4x/192.168.1.145 +``` + +So I can point my browser at `http://192.168.1.145:30001` and see the demo: +![yelb demo page](/yelb_nodeport_demo.png) + +After marveling at my own magnificence[^magnificence] for a few minutes, I'm ready to move on to something more interesting - but first, I'll just delete the `yelb` namespace to clean up the work I just did: +```bash +❯ kubectl delete ns yelb +namespace "yelb" deleted +``` + +Now let's move on and try to deploy `yelb` behind a `LoadBalancer` service so it will get its own IP. William has a [deployment spec](https://github.com/lamw/vmware-k8s-app-demo/blob/master/yelb-lb.yaml) for that too. + +```bash +❯ kubectl create ns yelb +namespace/yelb created + +❯ kubectl apply -f https://raw.githubusercontent.com/lamw/vmware-k8s-app-demo/master/yelb-lb.yaml +service/redis-server created +service/yelb-db created +service/yelb-appserver created +service/yelb-ui created +deployment.apps/yelb-ui created +deployment.apps/redis-server created +deployment.apps/yelb-db created +deployment.apps/yelb-appserver created + +❯ kubectl -n yelb get pods +NAME READY STATUS RESTARTS AGE +redis-server-74556bbcb7-q6l62 1/1 Running 0 7s +yelb-appserver-d584bb889-p5qgd 1/1 Running 0 7s +yelb-db-694586cd78-hjtn4 1/1 Running 0 7s +yelb-ui-8f54fd88c-pm9qw 1/1 Running 0 7s +``` + +And I can take a look at that service... +```bash +❯ kubectl -n yelb get svc/yelb-ui +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +yelb-ui LoadBalancer 100.67.177.185 80:32339/TCP 15s +``` + +Wait a minute. That external IP is *still* ``. What gives? Oh yeah I need to actually deploy and configure a load balancer before I can balance anything. That's up next. + +[^magnificence]: Mr. Anderson. + +#### Deploying `kube-vip` as a load balancer +Fortunately, William Lam [wrote up some tips](https://williamlam.com/2021/10/quick-tip-install-kube-vip-as-service-load-balancer-with-tanzu-community-edition-tce.html) for handling that too. It's [based on work by Scott Rosenberg](https://github.com/vrabbi/tkgm-customizations). The quick-and-dirty steps needed to make this work are: + +```bash +git clone https://github.com/vrabbi/tkgm-customizations.git +cd tkgm-customizations/carvel-packages/kube-vip-package +kubectl apply -n tanzu-package-repo-global -f metadata.yml +kubectl apply -n tanzu-package-repo-global -f package.yaml +cat << EOF > values.yaml +vip_range: 192.168.1.64-192.168.1.80 +EOF +tanzu package install kubevip -p kubevip.terasky.com -v 0.3.9 -f values.yaml +``` + +Now I can check out the `yelb-ui` service again: +```bash +❯ kubectl -n yelb get svc/yelb-ui +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +yelb-ui LoadBalancer 100.67.177.185 192.168.1.65 80:32339/TCP 4h35m +``` + +And it's got an IP! I can point my browser to `http://192.168.1.65` now and see: +![Successful LoadBalancer test!](/yelb_loadbalancer_demo.png) + +I'll keep the `kube-vip` load balancer since it'll come in handy, but I have no further use for `yelb`: +```bash +❯ kubectl delete ns yelb +namespace "yelb" deleted +``` + +#### Persistent Volume Claims, Storage Classes, and Storage Policies +At some point, I'm going to want to make sure that data from my Tanzu workloads stick around persistently - and for that, I'll need to [define some storage stuff](https://tanzucommunityedition.io/docs/latest/vsphere-cns/). + +First up, I'll add a new tag called `tkg-storage-local` to the `nuchost-local` vSphere datastore that I want to use for storing Tanzu volumes: +![Tag (and corresponding category) applied ](/storage_tag.png) + +Then I create a new vSphere Storage Policy called `tkg-storage-policy` which states that data covered by the policy should be placed on the datastore(s) tagged with `tkg-storage-local`: +![My Tanzu storage policy](/storage_policy.png) + +So that's the vSphere side of things sorted; now to map that back to the Kubernetes side. For that, I'll need to define a Storage Class tied to the vSphere Storage profile so I drop these details into a new file called `vsphere-sc.yaml`: +```yaml +kind: StorageClass +apiVersion: storage.k8s.io/v1 +metadata: + name: vsphere +provisioner: csi.vsphere.vmware.com +parameters: + storagePolicyName: tkg-storage-policy +``` + +And then apply it with : +```bash +❯ kubectl apply -f vsphere-sc.yaml +storageclass.storage.k8s.io/vsphere created +``` + +I can test that I can create a Persistent Volume Claim against the new `vsphere` Storage Class by putting this in a new file called `vsphere-pvc.yaml`: +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + name: vsphere-demo-1 + name: vsphere-demo-1 +spec: + accessModes: + - ReadWriteOnce + storageClassName: vsphere + resources: + requests: + storage: 5Gi +``` + +And applying it: +```bash +❯ kubectl apply -f demo-pvc.yaml +persistentvolumeclaim/vsphere-demo-1 created +``` + +I can see the new claim, and confirm that its status is `Bound`: +```bash +❯ kubectl get pvc +NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE +vsphere-demo-1 Bound pvc-36cc7c01-a1b3-4c1c-ba0d-dff3fd47f93b 5Gi RWO vsphere 4m25s +``` + +And for bonus points, I can see that the container volume was created on the vSphere side: +![Container Volume in vSphere](/container_volume_in_vsphere.png) + +So that's storage sorted. I'll clean up my test volume before moving on: +```bash +❯ kubectl delete -f demo-pvc.yaml +persistentvolumeclaim "vsphere-demo-1" deleted +``` + +### A real workload - phpIPAM +Demos are all well and good, but how about a real-world deployment to tie it all together? I've been using a [phpIPAM instance for assigning static IP addresses for my vRealize Automation deployments](/integrating-phpipam-with-vrealize-automation-8/), but have *only* been using it to monitor IP usage within the network ranges to which vRA will provision machines. I recently decided that I'd like to expand phpIPAM's scope so it can keep an eye on *all* the network ranges within the environment. That's not a big ask in [my little self-contained homelab](/vmware-home-lab-on-intel-nuc-9/), but having a single system scanning all the ranges of a large production network probably wouldn't scale too well. + +Fortunately the phpIPAM project provides a [remote scanning agent](https://github.com/phpipam/phpipam-agent) which can be used for keeping an eye on networks and reporting back to the main phpIPAM server. With this, I could deploy an agent to each region (or multiple agents to a region!) and divide up the network into chunks that each agent would be responsible for scanning. But that's a pretty lightweight task for a single server to manage, and who wants to deal with configuring multiple instances of the same thing? Not this guy. + +So I set to work exploring some containerization options, and I found [phpipam-docker](https://github.com/phpipam-docker/phpipam-docker). That would easily replicate my existing setup in a trio of containers (one for the web front-end, one for the database back-end, and one with `cron` jobs to run scans at regular intervals)... but doesn't provide a remote scan capability. I also found a [dockerized phpipam-agent](https://github.com/pierrecdn/phpipam-agent), but this one didn't quite meet my needs. It did provide me a base to work off of though so a few days of [tinkering](https://github.com/jbowdre/phpipam-agent-docker) resulted in me publishing my first [Docker image](https://github.com/jbowdre/phpipam-agent-docker/pkgs/container/phpipam-agent). I've still some work to do before this application stack is fully ready for production but it's at a point where I think it's worth doing a test deploy. + +To start, I'll create a new namespace to keep things tidy: + +```bash +❯ kubectl create ns ipam +namespace/ipam created +``` + +I'm going to wind up with four pods: +- `phpipam-db` for the database back-end +- `phpipam-www` for the web front-end +- `phpipam-cron` for the local cron jobs, which will be largely but not completely[^dns_scans] replaced by: +- `phpipam-agent` for my remote scan agent + +I'll use each container's original `docker-compose` configuration and adapt that into something I can deploy on Kubernetes. + +[^dns_scans]: My `phpipam-agent` image won't (yet?) do the DNS lookups that `phpipam-cron` can. + +#### phpipam-db +The phpIPAM database will live inside a MariaDB container. Here's the relevant bit from `docker-compose`: +```yaml +services: + phpipam-db: + image: mariadb:latest + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=VMware1!VMWare1! + volumes: + - phpipam-db-data:/var/lib/mysql +``` + +So it will need a `Service` exposing the container's port `3306` so that other pods can connect to the database. For my immediate demo, using `type: ClusterIP` will be sufficient since all the connections will be coming from within the cluster. When I do this for real, it will need to be `type: LoadBalancer` so that the agent running on a different cluster can connect. And it will need a `PersistentVolumeClaim` so it can store the database data at `/var/lib/mysql`. It will also get passed an environment variable to set the initial `root` password on the database instance (which will be used later during the phpIPAM install to create the initial `phpipam` database). + +It might look like this on the Kubernetes side: +```yaml +# phpipam-db.yaml +apiVersion: v1 +kind: Service +metadata: + name: phpipam-db + labels: + app: phpipam-db + namespace: ipam +spec: + type: ClusterIP + ports: + - name: mysql + port: 3306 + protocol: TCP + targetPort: 3306 + selector: + app: phpipam-db +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + name: phpipam-db + name: phpipam-db-pvc + namespace: ipam +spec: + accessModes: + - ReadWriteOnce + storageClassName: vsphere + resources: + requests: + storage: 5Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: phpipam-db + namespace: ipam +spec: + selector: + matchLabels: + app: phpipam-db + replicas: 1 + template: + metadata: + labels: + app: phpipam-db + spec: + containers: + - name: phpipam-db + image: mariadb:latest + env: + - name: MYSQL_ROOT_PASSWORD + value: "VMware1!VMware1!" + ports: + - name: mysql + containerPort: 3306 + volumeMounts: + - name: phpipam-db-vol + mountPath: /var/lib/mysql + volumes: + - name: phpipam-db-vol + persistentVolumeClaim: + claimName: phpipam-db-pvc +``` + +Moving on: + +#### phpipam-www +This is the `docker-compose` excerpt for the web component: +```yaml +services: + phpipam-web: + image: phpipam/phpipam-www:1.5x + ports: + - "80:80" + environment: + - TZ=UTC + - IPAM_DATABASE_HOST=phpipam-db + - IPAM_DATABASE_PASS=VMware1! + - IPAM_DATABASE_WEBHOST=% + volumes: + - phpipam-logo:/phpipam/css/images/logo +``` + +Based on that, I can see that my `phpipam-www` pod will need a container running the `phpipam/phpipam-www:1.5x` image, a `Service` of type `LoadBalancer` to expose the web interface on port `80`, a `PersistentVolumeClaim` mounted to `/phpipam/css/images/logo`, and some environment variables passed in to configure the thing. Note that the `IPAM_DATABASE_PASS` variable defines the password used for the `phpipam` user on the database (not the `root` user referenced earlier), and the `IPAM_DATABASE_WEBHOST=%` variable will define which hosts that `phpipam` database user will be able to connect from; setting it to `%` will make sure that my remote agent can connect to the database even if I don't know where the agent will be running. + +Here's how I'd adapt that into a structure that Kubernetes will understand: +```yaml +# phpipam-www.yaml +apiVersion: v1 +kind: Service +metadata: + name: phpipam-www + labels: + app: phpipam-www + namespace: ipam +spec: + type: LoadBalancer + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 80 + selector: + app: phpipam-www +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + name: phpipam-www + name: phpipam-www-pvc + namespace: ipam +spec: + accessModes: + - ReadWriteOnce + storageClassName: vsphere + resources: + requests: + storage: 100Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: phpipam-www + namespace: ipam +spec: + selector: + matchLabels: + app: phpipam-www + replicas: 1 + template: + metadata: + labels: + app: phpipam-www + spec: + containers: + - name: phpipam-www + image: phpipam/phpipam-www:1.5x + env: + - name: TZ + value: "UTC" + - name: IPAM_DATABASE_HOST + value: "phpipam-db" + - name: IPAM_DATABASE_PASS + value: "VMware1!" + - name: IPAM_DATABASE_WEBHOST + value: "%" + ports: + - containerPort: 80 + volumeMounts: + - name: phpipam-www-vol + mountPath: /phpipam/css/images/logo + volumes: + - name: phpipam-www-vol + persistentVolumeClaim: + claimName: phpipam-www-pvc +``` + +#### phpipam-cron +This container has a pretty simple configuration in `docker-compose`: +```yaml +services: + phpipam-cron: + image: phpipam/phpipam-cron:1.5x + environment: + - TZ=UTC + - IPAM_DATABASE_HOST=phpipam-db + - IPAM_DATABASE_PASS=VMware1! + - SCAN_INTERVAL=1h +``` + +No exposed ports, no need for persistence - just a base image and a few variables to tell it how to connect to the database and how often to run the scans: + +```yaml +# phpipam-cron.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: phpipam-cron + namespace: ipam +spec: + selector: + matchLabels: + app: phpipam-cron + replicas: 1 + template: + metadata: + labels: + app: phpipam-cron + spec: + containers: + - name: phpipam-cron + image: phpipam/phpipam-cron:1.5x + env: + - name: IPAM_DATABASE_HOST + value: "phpipam-db" + - name: IPAM_DATABASE_PASS + value: "VMWare1!" + - name: SCAN_INTERVAL + value: "1h" + - name: TZ + value: "UTC" +``` + +#### phpipam-agent +And finally, my remote scan agent. Here's the `docker-compose`: +```yaml +services: + phpipam-agent: + container_name: phpipam-agent + restart: unless-stopped + image: ghcr.io/jbowdre/phpipam-agent:latest + environment: + - IPAM_DATABASE_HOST=phpipam-db + - IPAM_DATABASE_NAME=phpipam + - IPAM_DATABASE_USER=phpipam + - IPAM_DATABASE_PASS=VMware1! + - IPAM_DATABASE_PORT=3306 + - IPAM_AGENT_KEY= + - IPAM_SCAN_INTERVAL=5m + - IPAM_RESET_AUTODISCOVER=true + - IPAM_REMOVE_DHCP=true + - TZ=UTC +``` + +It's got a few additional variables to make it extra-configurable, but still no need for persistence or network exposure. That `IPAM_AGENT_KEY` variable will need to get populated the appropriate key generated within the new phpIPAM deployment, but we can deal with that later. + +For now, here's how I'd tell Kubernetes about it: +```yaml +# phpipam-agent.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: phpipam-agent + namespace: ipam +spec: + selector: + matchLabels: + app: phpipam-agent + replicas: 1 + template: + metadata: + labels: + app: phpipam-agent + spec: + containers: + - name: phpipam-agent + image: ghcr.io/jbowdre/phpipam-agent:latest + env: + - name: IPAM_DATABASE_HOST + value: "phpipam-db" + - name: IPAM_DATABASE_NAME + value: "phpipam" + - name: IPAM_DATABASE_USER + value: "phpipam" + - name: IPAM_DATABASE_PASS + value: "VMware1!" + - name: IPAM_DATABASE_PORT + value: "3306" + - name: IPAM_AGENT_KEY + value: "" + - name: IPAM_SCAN_INTERVAL + value: "5m" + - name: IPAM_RESET_AUTODISCOVER + value: "true" + - name: IPAM_REMOVE_DHCP + value: "true" + - name: TZ + value: "UTC" +``` + +#### Deployment and configuration of phpIPAM +I can now go ahead and start deploying these containers, starting with the database one (upon which all the others rely): +```bash +❯ kubectl apply -f phpipam-db.yaml +service/phpipam-db created +persistentvolumeclaim/phpipam-db-pvc created +deployment.apps/phpipam-db created +``` + +And the web server: +```bash +❯ kubectl apply -f phpipam-www.yaml +service/phpipam-www created +persistentvolumeclaim/phpipam-www-pvc created +deployment.apps/phpipam-www created +``` + +And the cron runner: +```bash +❯ kubectl apply -f phpipam-cron.yaml +deployment.apps/phpipam-cron created +``` + +I'll hold off on the agent container for now since I'll need to adjust the configuration slightly after getting phpIPAM set up, but I will go ahead and check out my work so far: + +```bash +❯ kubectl -n ipam get all +NAME READY STATUS RESTARTS AGE +pod/phpipam-cron-6c994897c4-6rsnp 1/1 Running 0 4m30s +pod/phpipam-db-5f4c47d4b9-sb5bd 1/1 Running 0 16m +pod/phpipam-www-769c95c68d-94klg 1/1 Running 0 5m59s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/phpipam-db ClusterIP 100.66.194.69 3306/TCP 16m +service/phpipam-www LoadBalancer 100.65.232.238 192.168.1.64 80:31400/TCP 5m59s + +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/phpipam-cron 1/1 1 1 4m30s +deployment.apps/phpipam-db 1/1 1 1 16m +deployment.apps/phpipam-www 1/1 1 1 5m59s + +NAME DESIRED CURRENT READY AGE +replicaset.apps/phpipam-cron-6c994897c4 1 1 1 4m30s +replicaset.apps/phpipam-db-5f4c47d4b9 1 1 1 16m +replicaset.apps/phpipam-www-769c95c68d 1 1 1 5m59s +``` + +And I can point my browser to the `EXTERNAL-IP` associated with the `phpipam-www` service to see the initial setup page: +![phpIPAM installation page](/phpipam_install_page.png) + +I'll click the **New phpipam installation** option to proceed to the next step: +![Database initialization options](/phpipam_database_install_options.png) + +I'm all for easy so I'll opt for **Automatic database installation**, which will prompt me for the credentials of an account with rights to create a new database within the MariaDB instance. I'll enter `root` and the password I used for the `MYSQL_ROOT_PASSWORD` variable above: +![Automatic database install](/phpipam_automatic_database_install.png) + +I click the **Install database** button and I'm then met with a happy success message saying that the `phpipam` database was successfully created. + +And that eventually gets me to the post-install screen, where I set an admin password and proceed to log in: +![We made it to the post-install!](/phpipam_post_install.png) + +To create a new scan agent, I go to **Menu > Administration > Server management > Scan agents**. +![Scan agents screen](/scan_agents.png) + +And click the button to create a new one: +![Creating a new agent](/create_new_agent.png) + +I'll copy the agent code and plug it into my `phpipam-agent.yaml` file: +```yaml + - name: IPAM_AGENT_KEY + value: "4DC5GLo-F_35cy7BEPnGn7HivtjP_o-v" +``` + +And then deploy that: +```bash +❯ kubectl apply -f phpipam-agent.yaml +deployment.apps/phpipam-agent created +``` + +The scan agent isn't going to do anything until it's assigned to a subnet though, so now I head to **Administration > IP related management > Sections**. phpIPAM comes with a few default sections and ranges and such defined so I'll delete those and create a new one that I'll call `Lab`. +![Section management](/section_management.png) + +Now I can create a new subnet within the `Lab` section by clicking the **Subnets** menu, selecting the `Lab` section, and clicking **+ Add subnet**. +![Empty subnets menu](/subnets_empty.png) + +I'll define the new subnet as `192.168.1.0/24`. Once I enable the option to *Check hosts status*, I'll then be able to specify my new `remote-agent` as the scanner for this subnet. +![Creating a new subnet](/creating_new_subnet.png) +![A new (but empty) subnet](/new_subnet_pre_scan.png) + +It shows the scanner associated with the subnet, but no data yet. I'll need to wait a few minutes for the first scan to kick off (at the five-minute interval I defined in the configuration). +![](/five_minutes.gif) +![Newly discovered IPs!](/newly-discovered_IPs.png) + +Woah, it actually works! + +### Conclusion +I still need to do more work to the containerized phpIPAM stack ready for production, but I'm feeling pretty good for having deployed a functional demo of it at this point! And working on this was a nice excuse to get a bit more familiar with Tanzu Community Edition specifically, Kubernetes in general, and Docker (I learned a ton while assembling the `phpipam-agent` image!). I find I always learn more about a new-to-me technology when I have an actual project to do rather than just going through the motions of a lab exercise. Maybe my notes will be useful to you, too. \ No newline at end of file diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_iaas_provider.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_iaas_provider.png new file mode 100644 index 0000000..50be6f6 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_iaas_provider.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_image.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_image.png new file mode 100644 index 0000000..92ff57e Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_image.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_k8s_networking.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_k8s_networking.png new file mode 100644 index 0000000..69fe25c Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_k8s_networking.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_management_cluster.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_management_cluster.png new file mode 100644 index 0000000..604d5e0 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_management_cluster.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_resources.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_resources.png new file mode 100644 index 0000000..d10c7ab Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_resources.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_review.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_review.png new file mode 100644 index 0000000..a514c3a Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_review.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_ui.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_ui.png new file mode 100644 index 0000000..c914a4f Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/installer_ui.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/k8s-node_template.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/k8s-node_template.png new file mode 100644 index 0000000..a4ab6a6 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/k8s-node_template.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/new_subnet_pre_scan.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/new_subnet_pre_scan.png new file mode 100644 index 0000000..44465f7 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/new_subnet_pre_scan.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/newly-discovered_IPs.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/newly-discovered_IPs.png new file mode 100644 index 0000000..e6fabb5 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/newly-discovered_IPs.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_automatic_database_install.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_automatic_database_install.png new file mode 100644 index 0000000..bf80a0f Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_automatic_database_install.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_database_install_options.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_database_install_options.png new file mode 100644 index 0000000..ae658fe Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_database_install_options.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_install_page.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_install_page.png new file mode 100644 index 0000000..3d0317c Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_install_page.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_post_install.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_post_install.png new file mode 100644 index 0000000..aedfdf9 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/phpipam_post_install.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/scan_agents.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/scan_agents.png new file mode 100644 index 0000000..98144d0 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/scan_agents.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/section_management.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/section_management.png new file mode 100644 index 0000000..83b2f02 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/section_management.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/storage_policy.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/storage_policy.png new file mode 100644 index 0000000..75578bb Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/storage_policy.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/storage_tag.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/storage_tag.png new file mode 100644 index 0000000..e922902 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/storage_tag.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/subnets_empty.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/subnets_empty.png new file mode 100644 index 0000000..d3895d7 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/subnets_empty.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/tanzu_community_edition.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/tanzu_community_edition.png new file mode 100644 index 0000000..339bbb9 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/tanzu_community_edition.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/yelb_loadbalancer_demo.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/yelb_loadbalancer_demo.png new file mode 100644 index 0000000..45c9000 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/yelb_loadbalancer_demo.png differ diff --git a/content/posts/2022/tanzu-community-edition-k8s-homelab/yelb_nodeport_demo.png b/content/posts/2022/tanzu-community-edition-k8s-homelab/yelb_nodeport_demo.png new file mode 100644 index 0000000..d17a695 Binary files /dev/null and b/content/posts/2022/tanzu-community-edition-k8s-homelab/yelb_nodeport_demo.png differ