Skip to content
vWorld
Menu
  • Main Page
  • About
  • Study Guide
    • VCAP-CMA Deploy 2018
Menu

The Full Power of VCF Automation in Action: How I Connect the Dots and Build a Multi-Tier App with Kubernetes Objects.

Posted on October 16, 2025October 16, 2025 by admin

In the previous articles in my VMware Cloud Foundation 9.0 series, I explored two fundamental paths. First, in “[VCF Automation: From Zero to a Running Virtual Machine],” I demonstrated how to easily deploy a virtual machine using a simple dummyBlueprint. Then, in “[From Zero to Database-as-a-Service],” I explored VMware Data Services Manager (DSM) and deployed a fully managed PostgreSQL database.

Today, it’s time for me to connect the dots. I will show you how to move beyond simple, single resources and start thinking about deploying complete, multi-tier applications. In this article, I’ll demonstrate how a single, cohesive blueprint can define, deploy, and connect:

  • A PostgreSQL database provisioned by Data Services Manager.
  • A virtual machine for my web application.
  • The Cloud-init mechanism for on-the-fly, dynamic configuration of the application server.
  • An NSX Load Balancer (provisioned as a VirtualMachineService) to expose my application to the world.

I will build a simple, two-tier application where a web server, upon startup, connects to a newly created database, writes information to it, and then reads it back to display on a webpage. All of this will be fully automated and available as a single item in the service catalog.

A Shift in Philosophy: From Resources to Native Kubernetes Objects

One of the key changes in newer versions of VCF Automation is the move away from dedicated resource types towards a more universal and robust mechanism. Instead of using specific types like VMware.vSphere.Machine I will increasingly use CCI.Supervisor.Resource.

Why is this so important? This is beneficial because it allows me to directly define and deploy native Kubernetes API objects within the Supervisor Cluster. According to the official VMware documentation, this model gives me unprecedented flexibility. I can create not only virtual machines (VirtualMachine) but also secrets (Secret), services (VirtualMachineService), and, most importantly for today’s example, database clusters (PostgresCluster) provided by DSM.

In practice, this means my blueprint becomes a manifest describing the desired state of an entire group of Kubernetes resources, and Aria Automation acts as the orchestrator that brings this state to life.

The Application Blueprint: A Detailed Code Analysis

Below is the complete YAML code for my blueprint. Let’s analyze it step-by-step so you can understand how the individual components fit together into a cohesive whole.

formatVersion: 2
inputs:
  dbInstanceName:
    type: string
    title: Database Instance Name
    default: vcf-db
  dbAdminPassword:
    type: string
    title: Database Administrator Password
    encrypted: true
  dbVersion:
    type: string
    title: PostgreSQL Version
    enum:
      - '17'
      - '16'
      - '15'
    default: '17'
  storageSize:
    type: integer
    title: Database Disk Capacity (GB)
    default: 60
  dbVmClass:
    type: string
    title: VM Class for Database
    enum:
      - medium
      - large
    default: medium
  appVmName:
    type: string
    title: Application VM Name
    default: web-app
  appVmClass:
    type: string
    title: VM Class for Application
    enum:
      - best-effort-small
      - best-effort-medium
    default: best-effort-small
  vmImage:
    type: string
    title: Virtual Machine Image
    default: vmi-c9e21e0b30ee129c3
outputs:
  appServerIpAddress:
    type: string
    title: Application Server IP Address
    value: ${resource.App_Server_VM.object.status.network.primaryIp4}
  loadBalancerIpAddress:
    type: string
    title: Load Balancer IP Address
    value: ${resource.App_Server_Service.object.status.loadBalancer.ingress[0].ip}
  databaseHost:
    type: string
    title: Database Host
    value: ${resource.Postgres_Database_Cluster.object.status.connection.host}
  databasePort:
    type: string
    title: Database Port
    value: ${resource.Postgres_Database_Cluster.object.status.connection.port}
resources:
  CCI_Supervisor_Namespace_1:
    type: CCI.Supervisor.Namespace
    properties:
      name: vcf-vworld-nsp-v669r
      existing: true
  Postgres_Admin_Secret:
    type: CCI.Supervisor.Resource
    properties:
      context: ${resource.CCI_Supervisor_Namespace_1.id}
      manifest:
        apiVersion: v1
        kind: Secret
        metadata:
          name: ${input.dbInstanceName}-secret-${env.shortDeploymentId}
        stringData:
          username: pgadmin
          password: ${input.dbAdminPassword}
  Postgres_Database_Cluster:
    type: CCI.Supervisor.Resource
    dependsOn:
      - Postgres_Admin_Secret
    properties:
      context: ${resource.CCI_Supervisor_Namespace_1.id}
      manifest:
        apiVersion: databases.dataservices.vmware.com/v1alpha1
        kind: PostgresCluster
        metadata:
          name: ${input.dbInstanceName}
        spec:
          databaseName: ${input.dbInstanceName}
          version: ${input.dbVersion}
          storageSpace: ${input.storageSize + 'Gi'}
          vmClass:
            name: ${input.dbVmClass}
          adminUsername: pgadmin
          adminPasswordRef:
            name: ${resource.Postgres_Admin_Secret.manifest.metadata.name}
          infrastructurePolicy:
            name: vcf-infra-policy
          storagePolicyName: vworld-cl01 - Optimal Datastore Default Policy - RAID1
          replicas: 0
      wait:
        timeoutSeconds: 1800
        conditions:
          - type: Ready
            status: 'True'
          - type: DatabaseEngineReady
            status: 'True'
        jsonPath:
          - path: '{.status.connection.host}'
            regex: .+
  App_Server_Cloud_Init:
    type: CCI.Supervisor.Resource
    dependsOn:
      - Postgres_Database_Cluster
    properties:
      context: ${resource.CCI_Supervisor_Namespace_1.id}
      manifest:
        apiVersion: v1
        kind: Secret
        metadata:
          name: ${input.appVmName}-ci-secret-${env.shortDeploymentId}
        stringData:
          user-data: |
            #cloud-config
            hostname: ${input.appVmName}
            fqdn: ${input.appVmName}.local
            manage_etc_hosts: true

            write_files:
              - path: /etc/db-config.env
                permissions: '0600'
                owner: root:root
                content: |
                  DB_HOST=${resource.Postgres_Database_Cluster.object.status.connection.host}
                  DB_PORT=${resource.Postgres_Database_Cluster.object.status.connection.port}
                  DB_NAME=${input.dbInstanceName}
                  DB_USER=pgadmin
                  DB_PASSWORD=${input.dbAdminPassword}
                  DEPLOYMENT_ID=${env.shortDeploymentId}
              
              - path: /usr/local/bin/db-test.sh
                permissions: '0755'
                owner: root:root
                content: |
                  #!/bin/bash
                  set -e
                  
                  LOG="/tmp/cloud-init.log"
                  echo "=== Cloud-init START - $(date) ===" > $LOG
                  
                  if [ -f /etc/db-config.env ]; then
                    source /etc/db-config.env
                    echo "Configuration loaded from /etc/db-config.env" >> $LOG
                  else
                    echo "ERROR: /etc/db-config.env file not found!" >> $LOG
                    exit 1
                  fi
                  
                  echo "Installing packages..." >> $LOG
                  dnf install -y httpd postgresql 2>&1 | tee -a $LOG
                  
                  echo "Configuring firewall..." >> $LOG
                  firewall-cmd --permanent --add-service=http 2>&1 | tee -a $LOG
                  firewall-cmd --reload 2>&1 | tee -a $LOG
                  
                  echo "Starting httpd..." >> $LOG
                  systemctl enable --now httpd 2>&1 | tee -a $LOG
                  
                  if [ -z "$DB_HOST" ] || [ "$DB_HOST" = "null" ]; then
                    echo "ERROR: DB_HOST is empty or null!" >> $LOG

                    cat > /var/www/html/index.html << 'HTML'
                  <!DOCTYPE html>
                  <html>
                  <head><title>Configuration Error</title></head>
                  <body>
                    <h1>ERROR: Missing database configuration</h1>
                    <p>The DB_HOST or DB_PORT variables were not set correctly in the cloud-init script.</p>
                    <p>Check the logs on the virtual machine in the following files:</p>
                    <ul>
                      <li>/tmp/cloud-init.log</li>
                      <li>/etc/db-config.env</li>
                    </ul>
                  </body>
                  </html>
                  HTML
                    exit 1
                  fi
                  
                  export PGPASSWORD="$DB_PASSWORD"
                  
                  echo "Waiting for database $DB_HOST:$DB_PORT..." >> $LOG
                  ATTEMPTS=0
                  MAX_ATTEMPTS=60
                  
                  while ! psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c '\q' 2>/dev/null; do
                    ATTEMPTS=$((ATTEMPTS + 1))
                    if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then
                      echo "ERROR: Database unavailable after $MAX_ATTEMPTS attempts" >> $LOG

                      cat > /var/www/html/index.html << HTML
                  <!DOCTYPE html>
                  <html>
                  <head><title>Database Connection Error</title></head>
                  <body>
                    <h1>ERROR: Could not connect to the database</h1>
                    <p>The application could not establish a connection to the database server after $MAX_ATTEMPTS attempts.</p>
                    <hr>
                    <h2>Connection attempt details:</h2>
                    <p><b>Host:</b> $DB_HOST</p>
                    <p><b>Port:</b> $DB_PORT</p>
                    <p><b>Database:</b> $DB_NAME</p>
                    <p><b>User:</b> $DB_USER</p>
                    <br>
                    <p>Check the logs on the virtual machine in the file: <b>/tmp/cloud-init.log</b></p>
                  </body>
                  </html>
                  HTML
                      exit 1
                    fi
                    echo "Attempt $ATTEMPTS/$MAX_ATTEMPTS..." >> $LOG
                    sleep 5
                  done
                  
                  echo "Database is available!" >> $LOG
                  
                  echo "Creating test_table..." >> $LOG
                  psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" << 'SQL' 2>&1 | tee -a $LOG
                  CREATE TABLE IF NOT EXISTS test_table (id serial primary key, message text, created_at timestamp default now());
                  INSERT INTO test_table (message) VALUES ('Aria Automation - Full Test Completed Successfully!');
                  SQL
                  
                  echo "Reading data from the database..." >> $LOG
                  DB_MESSAGE=$(psql -tA -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT message FROM test_table ORDER BY id DESC LIMIT 1;")
                  
                  if [ $? -ne 0 ]; then
                    echo "ERROR reading from database: $DB_MESSAGE" >> $LOG
                    DB_MESSAGE="ERROR READING FROM DATABASE"
                  else
                    echo "Read from database: $DB_MESSAGE" >> $LOG
                  fi
                  
                  echo "Generating HTML page..." >> $LOG

                  cat > /var/www/html/index.html << HTML
                  <!DOCTYPE html>
                  <html>
                  <head>
                    <title>Aria Automation - Test Completed</title>
                  </head>
                  <body>
                    <h1>Application is Running Correctly</h1>
                    <hr>
                    <h2>Message read from the PostgreSQL database:</h2>
                    <p><b>$DB_MESSAGE</b></p>
                    <hr>
                    <h2>Deployment Details:</h2>
                    <p><b>Database Host:</b> $DB_HOST</p>
                    <p><b>Database Port:</b> $DB_PORT</p>
                    <p><b>Database Name:</b> $DB_NAME</p>
                    <p><b>Database User:</b> $DB_USER</p>
                    <p><b>Deployment ID:</b> $DEPLOYMENT_ID</p>
                    <p><b>VM Name:</b> $(hostname)</p>
                    <p><b>Generated:</b> $(date)</p>
                    <hr>
                    <p><small>Powered by VMware Aria Automation & vSphere with Tanzu</small></p>
                  </body>
                  </html>
                  HTML
                  
                  echo "=== Cloud-init DONE - $(date) ===" >> $LOG
                  echo "HTML page generated successfully" >> $LOG

            runcmd:
              - /usr/local/bin/db-test.sh
  App_Server_VM:
    type: CCI.Supervisor.Resource
    dependsOn:
      - App_Server_Cloud_Init
      - Postgres_Database_Cluster
    properties:
      context: ${resource.CCI_Supervisor_Namespace_1.id}
      manifest:
        apiVersion: vmoperator.vmware.com/v1alpha3
        kind: VirtualMachine
        metadata:
          name: ${input.appVmName}
          labels:
            vmoperator.vmware.com/vm-name: ${input.appVmName}
        spec:
          className: ${input.appVmClass}
          imageName: ${input.vmImage}
          powerState: PoweredOn
          storageClass: vworld-cl01-optimal-datastore-default-policy-raid1
          bootstrap:
            cloudInit:
              rawCloudConfig:
                name: ${resource.App_Server_Cloud_Init.manifest.metadata.name}
                key: user-data
      wait:
        timeoutSeconds: 1800
        conditions:
          - type: VirtualMachineCreated
            status: 'True'
          - type: VirtualMachineConditionPlacementReady
            status: 'True'
          - type: VirtualMachineTools
            status: 'True'
        jsonPath:
          - path: '{.status.network.primaryIp4}'
            regex: \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
  App_Server_Service:
    type: CCI.Supervisor.Resource
    dependsOn:
      - App_Server_VM
    properties:
      context: ${resource.CCI_Supervisor_Namespace_1.id}
      manifest:
        apiVersion: vmoperator.vmware.com/v1alpha3
        kind: VirtualMachineService
        metadata:
          name: ${input.appVmName}-svc
        spec:
          type: LoadBalancer
          selector:
            vmoperator.vmware.com/vm-name: ${input.appVmName}
          ports:
            - name: http
              port: 80
              protocol: TCP
              targetPort: 80
      wait:
        timeoutSeconds: 600
        jsonPath:
          - path: '{.status.loadBalancer.ingress[0].ip}'
1. Inputs and Outputs
  • inputs: This section defines the form a user will see in the service catalog. I’ve defined fields for the database instance name, admin password, PostgreSQL version, and also the VM classes and name for the application server. This provides flexibility while maintaining control.
  • outputs: After the deployment is complete, the blueprint returns key information necessary to use the application. In this case, these are the IP addresses of the server and the load balancer, and most importantly, the host and port for connecting to the database.
2. Resources – The Heart of the Orchestration

This is where all the magic happens. The resources are processed in an order dictated by their dependencies (dependsOn).

  • CCI_Supervisor_Namespace_1: The starting point. I specify an existing namespace in the Supervisor Cluster where all the following objects will be created.
  • Postgres_Admin_Secret: A crucial step. Instead of passing the database password directly, I first create a native Kubernetes secret. This is a security best practice. The password is taken from the inputs.
  • Postgres_Database_Cluster: This is my direct integration with DSM. I create an PostgresCluster object. Note the apiVersion: databases.dataservices.vmware.com/v1alpha1. I specify the version, size, VM class, and the infrastructure and storage policies I defined in the previous DSM article. The key part is adminPasswordRef, which refers to the secret created during the last step. The wait section ensures that the blueprint will wait until the database is fully ready and has a host assigned before proceeding.
  • App_Server_Cloud_Init: This is the glue connecting my application to the database. I create another Kubernetes secret, but this time it contains a cloud-init script.
    • write_files: The script creates two files on the virtual machine.
    • /etc/db-config.env: This is a dynamic configuration. Notice values like DB_HOST=${resource.Postgres_Database_Cluster.object.status.connection.host}. Here, Aria Automation injects the host address that was dynamically created and returned by the database resource! The same happens with the port, password, and database name.
    • /usr/local/bin/db-test.sh: This is a bash script that performs all the application configuration logic:
      1. Installs an HTTP server (httpd) and a PostgreSQL client.
      2. Configures the firewall to allow HTTP traffic.
      3. Starts the web server.
      4. The system waits in a loop until the database becomes available, with robust error handling that generates a helpful HTML error page on failure.
      5. Once the database is ready, it connects and creates a test_table, and inserts a test record.
      6. It reads that record back from the database.
      7. Finally, it generates a clean, functional index.html page that displays the retrieved message and all relevant deployment details.
  • App_Server_VM: Now I create the virtual machine. Notice I am leveraging the latest apiVersion: vmoperator.vmware.com/v1alpha3 for enhanced capabilities and future compatibility. I use the image and VM class from the inputs. The most important part is the bootstrap section, which instructs the machine to use the cloud-init script from the App_Server_Cloud_Init secret I created earlier. The wait section ensures I wait for the VM to be fully up and have an IP address.
  • App_Server_Service: The final piece of the puzzle. I create a VirtualMachineService object of type LoadBalancer, also using the v1alpha3 API version. It operates based on a selector That points to my application machine by its label. NSX ALB automatically detects this object, creates a server pool with the VM, and assigns it an external IP address from the VIP pool, exposing port 80.

Deployment and Validation

Once the blueprint is published in the catalog, the deployment process is just a few clicks away.

  1. A user selects the item from the catalog and clicks “Request.”
  2. A form generated from the inputs section of my blueprint appears. The user provides the database password and selects the versions and VM classes.
  3. After submission, Aria Automation begins the orchestration. In the deployment view, you can track the progress of each resource being created according to the defined dependencies.

After about a dozen minutes, the entire environment is ready. In the outputs section of the deployment, I will find the IP address of my load balancer.

  1. Validation: I open a web browser and enter the load balancer’s IP address.

Success! The result is a functional webpage that confirms the application is running, displays the message retrieved from the database, and lists the deployment details. My simple web application was automatically configured, connected to a dynamically created database, and is now displaying data retrieved from it.

By checking the resources in the vSphere Client, you will see two new virtual machines (one for the database, one for the application) and, in the NSX infrastructure, a new load balancer and its server pool.

Summary

Today, I’ve taken you on a journey from simple, isolated resources to a fully integrated, multi-tier application deployed with a single click. This example perfectly demonstrates the power of the VMware Cloud Foundation and VCF Automation platform.

By using native Kubernetes objects as the language for describing infrastructure, I can cohesively integrate services from different domains—compute (VM), storage (vSAN), networking (NSX ALB), and database (DSM). For an administrator, this means complete control and standardization. For a developer, it means unparalleled agility and speed, comparable to public clouds, but within a secure, private environment.

This is the true power of Infrastructure as Code, VMware-style. I hope this example inspires you to create your own, even more advanced blueprints.

Share with:


Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recent Posts

  • The Full Power of VCF Automation in Action: How I Connect the Dots and Build a Multi-Tier App with Kubernetes Objects.
  • From Code to Kubernetes Cluster with Chiselled Ubuntu Images on VMware
  • From Zero to Database-as-a-Service: A Deep Dive into VMware Data Services Manager 9.0 and VCF Automation
  • Complete Guide: Configuring SSO in VMware Cloud Foundation with Active Directory and VCF Automation Integration
  • From Zero to a Scalable Application in VCF 9.0: The Complete, Hyper-Detailed Configuration Guide

Archives

Follow Me!

Follow Me on TwitterFollow Me on LinkedIn

GIT

  • GITHub – vWorld GITHub – vWorld 0
© 2025 vWorld | Powered by Superbs Personal Blog theme