Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python-pulumi/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies = [
"azure-containerregistry>=1.2.0,<2.0.0",
"azure-identity>=1.19.0,<2.0.0",
"azure-keyvault-secrets>=4.9.0,<5.0.0",
"azure-mgmt-compute>=33.0.0,<34.0.0",
# NOTE: The pulumi dependencies are pinned to exact versions so that the plugins used in
# each pulumi stack state remain stable.
"pulumi==3.187.0",
Expand Down
51 changes: 51 additions & 0 deletions python-pulumi/src/ptd/azure_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import azure.identity
import azure.keyvault.secrets
from azure.mgmt.compute import ComputeManagementClient


def get_secret(secret_name: str, vault_name: str) -> str:
Expand All @@ -27,3 +28,53 @@ def set_secret(secret_name: str, vault_name: str, secret_value: dict[str, str])

secret_value_json = json.dumps(secret_value)
client.set_secret(secret_name, secret_value_json)


def get_latest_vm_image_version(
subscription_id: str,
location: str,
publisher: str,
offer: str,
sku: str,
) -> str:
"""
Get the latest VM image version for a given publisher, offer, and SKU.

Equivalent to:
az vm image list --publisher {publisher} --offer {offer} --sku {sku} --all --location {location}

Args:
subscription_id: Azure subscription ID
location: Azure region (e.g., "eastus")
publisher: Image publisher (e.g., "Canonical")
offer: Image offer (e.g., "0001-com-ubuntu-server-jammy")
sku: Image SKU (e.g., "22_04-lts-gen2")

Returns:
Latest version string (e.g., "22.04.202412100")
"""
credential = azure.identity.DefaultAzureCredential()
compute_client = ComputeManagementClient(credential, subscription_id)

# List all versions for the given publisher, offer, and SKU
images = compute_client.virtual_machine_images.list(
location=location,
publisher_name=publisher,
offer=offer,
skus=sku,
)

# Extract version strings and sort them
# Azure returns VirtualMachineImageResource objects with a 'name' field containing the version
versions = [image.name for image in images]

if not versions:
msg = f"No images found for {publisher}/{offer}/{sku} in {location}"
raise ValueError(msg)

# Sort versions lexicographically (Azure version format sorts correctly this way)
# Example: "22.04.202401010" < "22.04.202412100"
versions.sort()

# Return the latest version
return versions[-1]
17 changes: 14 additions & 3 deletions python-pulumi/src/ptd/pulumi_resources/azure_bastion.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(
location: str | pulumi.Output[str],
tags: dict[str, str],
vm_size: str | pulumi.Output[str],
image_version: str,
*args,
**kwargs,
):
Expand Down Expand Up @@ -116,12 +117,13 @@ def __init__(
publisher="Canonical",
offer="0001-com-ubuntu-server-jammy",
sku="22_04-lts-gen2",
version="latest",
version=image_version,
),
os_disk=compute.OSDiskArgs(
name=f"{name}-jumpbox-osdisk",
caching=compute.CachingTypes("ReadWrite"),
create_option="FromImage",
delete_option="Delete",
),
),
network_profile=compute.NetworkProfileArgs(
Expand All @@ -137,6 +139,10 @@ def __init__(
computer_name=f"{name}-jumpbox",
linux_configuration=compute.LinuxConfigurationArgs(
disable_password_authentication=True,
patch_settings=compute.LinuxPatchSettingsArgs(
patch_mode="AutomaticByPlatform",
assessment_mode="AutomaticByPlatform",
),
ssh=compute.SshConfigurationArgs(
public_keys=[
compute.SshPublicKeyArgs(
Expand All @@ -147,6 +153,11 @@ def __init__(
),
),
),
tags=tags | {"Name": f"{name}-jumpbox"},
opts=pulumi.ResourceOptions(parent=self),
tags=tags | {"Name": f"{name}-jumpbox", "ImageVersion": image_version},
opts=pulumi.ResourceOptions(
parent=self,
protect=False, # allow the VM to be recreated on image version updates
replace_on_changes=["storageProfile.imageReference.version"],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace_on_changes forces VM recreation when the imageReference version changes, otherwise it would be an update which doesn't actually update the image running on the VM.

delete_before_replace=True,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We ensure we delete the old VM first so it cleans up its disks and has no conflicts with the pending new VM.

),
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pulumi_azure_native import containerregistry, dns, netapp, network, privatedns, storage

import ptd
import ptd.azure_sdk
import ptd.azure_workload
from ptd.pulumi_resources.azure_bastion import AzureBastion

Expand Down Expand Up @@ -787,6 +788,21 @@ def _define_dns_zones(self):
)

def _define_bastion(self):
# Get the latest Ubuntu 22.04 LTS image version
# This will cause Pulumi to recreate the VM when a new version is available
latest_image_version = "latest"
try:
latest_image_version = ptd.azure_sdk.get_latest_vm_image_version(
subscription_id=self.workload.cfg.subscription_id,
location=self.workload.cfg.region,
publisher="Canonical",
offer="0001-com-ubuntu-server-jammy",
sku="22_04-lts-gen2",
)
except Exception as e:
msg = f"Failed to fetch latest VM image version: {e}"
raise RuntimeError(msg) from e

self.bastion = AzureBastion(
name=f"bas-ptd-{self.workload.compound_name}-bastion",
bastion_subnet=self.bastion_subnet,
Expand All @@ -795,6 +811,7 @@ def _define_bastion(self):
location=self.workload.cfg.region,
tags=self.required_tags,
vm_size=self.workload.cfg.bastion_instance_type,
image_version=latest_image_version,
opts=pulumi.ResourceOptions(
protect=self.workload.cfg.protect_persistent_resources,
),
Expand Down
Loading
Loading