Hey all, I'm looking for best practices regarding ...
# azure
b
Hey all, I'm looking for best practices regarding locks (pulumi.com/registry/…/lock) Questions that wander in my head: • Does Pulumi have a way to add locks after all resources are deployed, without using depends on. • How do I make sure the locks are deleted when updating, creating, or deleting resources
@steep-plastic-74107
f
I suspect a combo of protect and hooks could work here, and then transforms to apply to an entire stack. TS, totally untested:
Copy code
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure-native";
import { ManagementLocksClient } from "@azure/arm-locks";
import { DefaultAzureCredential } from "@azure/identity";

const config = new pulumi.Config("azure-native");
const subscriptionId = config.require("subscriptionId");
const locks = new ManagementLocksClient(new DefaultAzureCredential(), subscriptionId);

const LOCK_NAME = "auto-lock";
const LOCK_LEVEL = "CanNotDelete"; // switch to "ReadOnly" only if you also wire the beforeUpdate/afterUpdate hooks below

// --- Hooks: apply the lock after create/replace, remove it before delete/replace ---

const applyLock = new pulumi.ResourceHook("apply-lock", async (args) => {
  const scope = args.newOutputs.id as string;
  await locks.managementLocks.createOrUpdateByScope(scope, LOCK_NAME, {
    level: LOCK_LEVEL,
    notes: "Applied automatically via Pulumi resource hook",
  });
});

const removeLock = new pulumi.ResourceHook(
  "remove-lock",
  async (args) => {
    const scope = args.oldOutputs.id as string;
    await locks.managementLocks.deleteByScope(scope, LOCK_NAME);
  },
  { ignoreErrors: false } // if removal fails, the delete/update must NOT proceed
);

// --- Stack transform: attach the hooks + protect to every matching resource, stack-wide ---

const LOCK_ELIGIBLE_TYPES = new Set([
  "azure-native:storage:StorageAccount",
  "azure-native:network:PublicIPAddress",
]);

pulumi.runtime.registerResourceTransform(args => {
  if (!LOCK_ELIGIBLE_TYPES.has(args.type)) {
    return undefined;
  }
  return {
    props: args.props,
    opts: pulumi.mergeOptions(args.opts, {
      protect: true,
      hooks: {
        afterCreate: [applyLock],
        afterUpdate: [applyLock],
        beforeDelete: [removeLock],
        // beforeUpdate: [removeLock], // only needed if LOCK_LEVEL is "ReadOnly"
      },
    }),
  };
});

// --- Resources: no per-resource lock wiring needed, the transform handles it ---

const resourceGroup = new azure.resources.ResourceGroup("rg", {
  resourceGroupName: "locked-example-rg",
  location: "East US",
});

const storageAccount = new azure.storage.StorageAccount("account", {
  resourceGroupName: resourceGroup.name,
  location: resourceGroup.location,
  sku: { name: azure.storage.SkuName.Standard_LRS },
  kind: azure.storage.Kind.StorageV2,
});

const publicIp = new azure.network.PublicIPAddress("pip", {
  resourceGroupName: resourceGroup.name,
  location: resourceGroup.location,
  publicIPAllocationMethod: azure.network.IPAllocationMethod.Static,
});

export const storageAccountId = storageAccount.id;
export const publicIpId = publicIp.id;
b
Thanks @future-hairdresser-70637, I like the general idea but I foresee some issues. I still have to keep track of all resources coupled (peps, diagnostic settings, etc) to the resource I lock, and it could trigger the deletion and creation of the lock multiple times in one deploy. I assume you suggesting this solution means that there are no global hooks on the stack then. I guess the best way would be to keep locks out of Pulumi, but ideally it would have been nice if Pulumi would resolve locks with the execution order in mind. If someone knows another solution I would like to hear it :)
👍 1
f
There are stack transforms that can auto-attach to every matching resource but every hook would fire at the resource level. Are you making use of resource groups? Locks at that scope cascade to everything inside, so you wouldn't need to track PEPs/diagnostic settings separately (it does mean any unlock window covers the whole RG.) We do have a few customers using locks via Pulumi at scale but I wouldn't say it's common and I don't have knowledge whether they're doing it in this fashion/automatic. You could write Azure Policy to do similar. If you're looking for defense-in-depth, that's one way to go. Then you could layer on Pulumi protected resources and Pulumi's policy-as-code.