In my recent IaCConf talk, I showed some demos from this IacConf2026demo repo.
One launched to Azure as an Azure function using Tofu, and the other used AWX with a playbook to launch another container to GCP.
However, leaving these running all the time will really mount costs.
In both GCP
And in Azure
Cleanup with Tofu
In IaCConf talk I demonstrated using tofu plan and tofu apply to create the infrastructure.
Since our tfstate file lives in Azure, we can use destroy to fix it.
So let’s start by making sure our local clone is up to date and do a tofu init
isaac@isaac-G707:~/Workspaces$ cd iacconfDemo2026/
isaac@isaac-G707:~/Workspaces/iacconfDemo2026$ git pull
Already up to date.
isaac@isaac-G707:~/Workspaces/iacconfDemo2026$ cd iac
isaac@isaac-G707:~/Workspaces/iacconfDemo2026/iac$ tofu init
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/random from the dependency lock file
- Reusing previous version of hashicorp/azurerm from the dependency lock file
- Using previously-installed hashicorp/random v3.9.1
- Using previously-installed hashicorp/azurerm v4.81.0
OpenTofu has been successfully initialized!
You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure. All OpenTofu commands
should now work.
If you ever set or change modules or backend configuration for OpenTofu,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Let’s now do a tofu destroy
I needed to remove a Smart Rule so the destroy could finish cleaning up the RG
but then it worked
psed]
azurerm_log_analytics_workspace.example: Destruction complete after 11s
azurerm_resource_group.example: Destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/rg-healthy-jaguar]
random_string.name: Destroying... [id=rojdttsg]
random_string.name: Destruction complete after 0s
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 10s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 20s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 30s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 40s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 50s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 1m0s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 1m10s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 1m20s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 1m30s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 1m40s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 1m50s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 2m0s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 2m10s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 2m20s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 2m30s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 2m40s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 2m50s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 3m0s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 3m10s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 3m20s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 3m30s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 3m40s elapsed]
azurerm_resource_group.example: Still destroying... [id=/subscriptions/d955c0ba-13dc-44cf-a29a-...bb22d/resourceGroups/rg-healthy-jaguar, 3m50s elapsed]
azurerm_resource_group.example: Destruction complete after 3m53s
random_pet.rg_name: Destroying... [id=rg-healthy-jaguar]
random_pet.rg_name: Destruction complete after 0s
Destroy complete! Resources: 8 destroyed.
One of the things I pointed out in the presentation was that by using standards, like Ansible, we can easily have LLMs help create the IaC/provisioning scripts for us
it claimed to get done
I added the remove playbook it created
isaac@isaac-G707:~/Workspaces/iacconfDemo2026$ git add awx/
isaac@isaac-G707:~/Workspaces/iacconfDemo2026$ git commit -m "remove playbook"
[main c2b8004] remove playbook
1 file changed, 89 insertions(+)
create mode 100644 awx/update_kuma_remove.yaml
isaac@isaac-G707:~/Workspaces/iacconfDemo2026$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 16 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 1.19 KiB | 1.19 MiB/s, done.
Total 4 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
remote: . Processing 1 references
remote: Processed 1 references in total
To https://forgejo.freshbrewed.science/builderadmin/iacconfDemo2026.git
e2db3b7..c2b8004 main -> main
To add this in AWX I’ll first sync the playbook to get the latest commit
Then I usually will copy an existing playbook just to save time setting project and labels
I then just rename the playbook and switch template files to use
I can now launch it
Once it completed
I went and checked Cloud Run and saw the instance was gone
A day later we see the cloud costs abated
We see the $0.41/d for App Svc plan was dropped after we removed the Azure Function
Another costs that has been there for some time is for ACR.
However, all that is there is a single backup of a container image.
Container Registry - Basic is costing me $0.17/day or $5.27 a month
What if I saved that as a file and moved it to LRS storage?
That would cost me less than 2c a month instead of over $5!!!
Let’s download the image
(base) builder@LuiGi:~/Workspaces/fbsnew$ docker pull fbharborbkup.azurecr.io/backups/myghrunner:1.1.13
1.1.13: Pulling from backups/myghrunner
4d32b49e2995: Pull complete
4f4fb700ef54: Pull complete
5b3bb35e4e39: Pull complete
9db00d74da43: Pull complete
d9f474f208fd: Pull complete
528b31ad2b78: Pull complete
9fcf9862f2f9: Pull complete
dbc81e793853: Pull complete
9e8e6b5b14ae: Pull complete
178f42893500: Pull complete
927b86b396f4: Pull complete
db4ad63c9155: Pull complete
3021bea054e6: Pull complete
8ca489d538e5: Pull complete
b8f6482c48ee: Pull complete
d68f5c87c38f: Pull complete
61a3e00bb306: Pull complete
55711c8eed25: Pull complete
5056f5389e02: Pull complete
Digest: sha256:fb271e07295790976446f72eec7d050befa884f92944f8cc93ff1de75ca9d16b
Status: Downloaded newer image for fbharborbkup.azurecr.io/backups/myghrunner:1.1.13
fbharborbkup.azurecr.io/backups/myghrunner:1.1.13
(base) builder@LuiGi:~/Workspaces/fbsnew$ docker save fbharborbkup.azurecr.io/backups/myghrunner:1.1.13 | gzip > myghrunner-1.1.13.tar.gz
(base) builder@LuiGi:~/Workspaces/fbsnew$
(base) builder@LuiGi:~/Workspaces/fbsnew$ ls
INSTALL.md README.md bashToFix hugo.toml myghrunner-1.1.13.tar.gz ss3b-video.sh static
LATEST.md archetypes content layouts public ss3b.sh themes
(base) builder@LuiGi:~/Workspaces/fbsnew$ ls -ltrah
total 615M
drwxrwxr-x 10 builder builder 4.0K Oct 22 20:06 .
-rw-rw-r-- 1 builder builder 615M Oct 22 20:09 myghrunner-1.1.13.tar.gz
Then push it up to storage
(base) builder@LuiGi:~/Workspaces/fbsnew$ az storage blob upload --account-name fbsbackups --container-name fbsbackupcontainer --name docker-myghrunner-1.1.13.tar.gz --file myghrunner-1.1.13.tar.gz --auth-mode key
There are no credentials provided in your command and environment, we will query for account key for your storage account.
It is recommended to provide --connection-string, --account-key or --sas-token in your command as credentials.
You also can add `--auth-mode login` in your command to use Azure Active Directory (Azure AD) for authorization if your login account is assigned required RBAC roles.
For more information about RBAC roles in storage, visit https://learn.microsoft.com/azure/storage/common/storage-auth-aad-rbac-cli.
In addition, setting the corresponding environment variables can avoid inputting credentials in your command. Please use --help to get more information about environment variable usage.
Finished[#############################################################] 100.0000%
{
"client_request_id": "86140160-ce6f-11f1-8000-a4f9339941d9",
"content_crc64": "b/DfgMWX8cs=",
"content_md5": null,
"date": "2026-10-23T01:23:00+00:00",
"encryption_key_sha256": null,
"encryption_scope": null,
"etag": "\"0x8DF30A42DD56400\"",
"lastModified": "2026-10-23T01:23:01+00:00",
"request_id": "a4118c32-c01e-0075-4c8d-62b661000000",
"request_server_encrypted": true,
"version": "2022-11-02",
"version_id": null
}
I then set the storage to Cool to save even more over time
There is a 30d minimum but at Cool, I would likely spend a couple of pennies a YEAR now instead of over $5 a month:
Azure calculates blob storage by the gigabyte (GB/GiB) per month. For the Cool tier in a standard US region, the storage capacity rate is typically $0.01 per GB/month for Locally Redundant Storage (LRS) and $0.013 per GB/month for Zone-Redundant Storage (ZRS). Assuming standard LRS redundancy: • Data Size: 650 MB = 0.65 GB • Monthly Storage Cost: 0.65 GB × $0.01/GB = $0.0065 per month
Having stored the file, I can now just delete the container registry
Summary
What is the point? There are two key things to think about : how you delete resources in old demos/projects to save money and when to reconsider the type of resource you are using.
With the first, we realize sitting on demos just wastes money. One of those apps just showed a dad joke I could just as well run in my local k3s. In the second instance, the idea of having a base ACR was smart in 2022 when I thought I would need an active hot backup. But today I realized I usually have local copies of the key containers in build hosts and its easy enough to tag and push back. Things that are not secret, I just backup to Dockerhub. Lastly, for other things, if you really just need a one-off backup saving to a file is way way cheaper.