Deploys a Photon OS 5.0 virtual machine on VMware vSphere and fully configures it on first boot using cloud-init — no console interaction, no post-deploy Ansible, no golden images per configuration variant.
┌─────────────────────────────────────────────────────────────────┐
│ Your workstation │
│ │
│ deploy-photon.ps1 │
│ │ │
│ ├─ 1. Connect to vCenter │
│ ├─ 2. Deploy OVF from Content Library │
│ ├─ 3. Base64-encode YAML files │
│ ├─ 4. Set guestinfo.* keys on the VM ──────────────────┐ │
│ └─ 5. Power on │ │
│ │ │
└────────────────────────────────────────────────────────────┼───┘
│
VMware GuestInfo RPC channel
│
┌────────────────────────────────────────────────────────────▼───┐
│ Photon OS VM (first boot) │
│ │
│ cloud-init │
│ ├─ reads guestinfo.metadata → applies static IP / hostname │
│ └─ reads guestinfo.userdata → installs packages, drops keys │
│ runs runcmd, clones repo │
└─────────────────────────────────────────────────────────────────┘
The two YAML files (meta-data.yaml and user-data.yaml) describe the
VM's entire desired state. They are the only files you need to edit for
a new deployment.
infra/
├── README.md ← you are here
├── deploy-photon.ps1 ← main deployment script (PowerCLI)
├── monitor-cloud-init.ps1 ← optional SSH monitoring script
└── cloud-init/
├── meta-data.yaml ← VM identity + static network config
└── user-data.yaml ← credentials, packages, files, commands
Install-Module VCF.PowerCLI -Scope CurrentUserInstall-Module Posh-SSH -Scope CurrentUser| Requirement | Notes |
|---|---|
| vCenter 7.0 or later | |
| Content Library with the Photon OS OVF | Item name: photon-hw15-5.0-dde71ec57.x86_64 — download from github.com/vmware/photon |
| A distributed virtual portgroup (vDS) | Standard vSwitch: replace Get-VDPortgroup with Get-VirtualPortGroup in deploy-photon.ps1 |
| A static IP reservation for the VM | Must match the address in meta-data.yaml |
# Generate a dedicated key pair — do not reuse a personal key
ssh-keygen -t ed25519 -C "vm-deploy-key" -f ~/.ssh/vm_deploy_key
# Add vm_deploy_key.pub as a read-only deploy key on your repository
# (GitHub: Settings > Deploy keys > Add deploy key)Edit these files before running the deployment script.
| Placeholder | Replace with |
|---|---|
<YOUR-VM-NAME> |
VM hostname, e.g. my-dev-vm |
<YOUR-STATIC-IP>/<PREFIX> |
Static IP in CIDR notation, e.g. 192.168.1.100/24 |
<YOUR-GATEWAY> |
Default gateway IP |
<YOUR-DNS-1> |
Primary DNS server |
<YOUR-DNS-2> |
Secondary DNS server |
eth0 |
Interface name for your distro (Photon OS = eth0, Ubuntu = ens160) |
| Placeholder | Replace with |
|---|---|
<YOUR-PASSWORD> |
Root password |
<YOUR-NTP-SERVER> |
NTP server, e.g. pool.ntp.org |
<PASTE YOUR ED25519 PRIVATE KEY HERE> |
Contents of ~/.ssh/vm_deploy_key (private key) |
<YOUR-GIT-HOST> |
Git server hostname, e.g. github.com |
<YOUR-REGISTRY-URL> |
npm registry URL (remove the .npmrc block for public registry) |
<YOUR-AUTH-TOKEN> |
npm / Artifactory auth token |
<YOUR-ORG>/<YOUR-REPO>.git |
Repository path |
Override on the command line or edit the param() block defaults:
| Parameter | Default | Description |
|---|---|---|
-VCenterServer |
vcenter.your-lab.local |
vCenter hostname or IP |
-VCenterUser |
administrator@vsphere.local |
vCenter username |
-DatacenterName |
your-datacenter |
Datacenter name |
-FolderName |
your-folder |
VM folder name |
-DatastoreName |
your-datastore |
Datastore name |
-PortGroupName |
VM Network |
Distributed portgroup name |
-ContentLibraryName |
Your Content Library |
Content Library name |
-TemplateItemName |
photon-hw15-5.0-dde71ec57.x86_64 |
OVF item name |
-VMName |
photon-vm |
New VM display name |
-VMIP |
192.168.1.100 |
IP assigned in meta-data.yaml |
pwsh infra/deploy-photon.ps1You will be prompted for the vCenter password interactively. The password is never stored in any file.
pwsh infra/deploy-photon.ps1 `
-VCenterServer vcenter.mylab.local `
-DatacenterName mydc `
-FolderName dev-vms `
-VMName dev-photon-01 `
-VMIP 10.0.1.50pwsh infra/deploy-photon.ps1 -MonitorThis streams /var/log/cloud-init-output.log from the VM over SSH as
provisioning runs. A version check is printed when cloud-init finishes:
[1/3] Waiting for SSH port on 192.168.1.100 (timeout: 300s)...
Port 22 open after 45s.
[2/3] Waiting for stable SSH (5 consecutive successes)...
SSH ok (5/5)
[3/3] Streaming cloud-init output...
Installing: openjdk21-21.0.10-3.ph5.x86_64
...
cloud-init: done
--- Version Check ---
openjdk version "21.0.10-internal"
Apache Maven 3.9.x
v24.x.x
PowerShell 7.x.x
git version 2.x.x
pwsh infra/monitor-cloud-init.ps1 -VMIP 192.168.1.100 -SSHPassword YourPasswordCheck that instance-id in meta-data.yaml is unique. cloud-init
skips all modules if it detects the same instance-id from a previous run
(stored in /var/lib/cloud/instance/).
# On the VM — force a full re-run on next reboot
rm -rf /var/lib/cloud/instance
cloud-init clean
reboot# Show which module failed and why
cloud-init status --long
# Full output of all runcmd commands
cat /var/log/cloud-init-output.log
# Detailed module execution log
cat /var/log/cloud-init.log- Verify the interface name in
meta-data.yamlmatches the VM's actual interface (ip addr showon the VM console). - Photon OS 5.0 uses
eth0. Ubuntu 20+/22+ on VMware typically usesens160. - Use
to: 0.0.0.0/0for the default route, notto: default.
Ensure Get-OvfConfiguration is called before New-VM and that
$ovfConfig.EULAs.Accept.Value = $true is set. See deploy-photon.ps1
step 4 for the correct pattern.
The base OVF ships with chkconfig which conflicts with alternatives
(required by openjdk21). The --allowerasing flag in user-data.yaml
replaces chkconfig automatically:
- tdnf install -y --nogpgcheck --allowerasing alternatives
- tdnf install -y --nogpgcheck openjdk21The PowerCLI deployment script and the GuestInfo mechanism are the same
for any cloud-init-enabled Linux distribution. Only the runcmd section
of user-data.yaml needs to change:
| Distro | Package manager | Java package name |
|---|---|---|
| Photon OS 5.0 | tdnf |
openjdk21 |
| Ubuntu 22.04 | apt-get |
openjdk-21-jdk |
| Rocky Linux 9 | dnf |
java-21-openjdk |
The network interface name in meta-data.yaml also varies by distro —
check with ip addr show on a running instance.
- Never commit real secrets to source control. All placeholders in the YAML files must be replaced locally before use.
- The SSH deploy key should be read-only and scoped to a single repository. Do not reuse personal or organisation-wide SSH keys.
- For production VMs, disable
ssh_pwauthafter provisioning is complete and rely on SSH key-based authentication only. - The
.npmrcauth token should be a scoped, short-lived token with the minimum permissions required to download packages.