[{"content":" ","date":"19 May 2026","externalUrl":null,"permalink":"/","section":"","summary":"","title":"","type":"page"},{"content":"","date":"19 May 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"19 May 2026","externalUrl":null,"permalink":"/categories/food/","section":"Categories","summary":"","title":"Food","type":"categories"},{"content":"","date":"19 May 2026","externalUrl":null,"permalink":"/tags/groceries/","section":"Tags","summary":"","title":"Groceries","type":"tags"},{"content":" Disclaimers # Gluten things # I went gluten-free in my early 20s due to some health issues though I\u0026rsquo;ve never followed up with the test for coeliac disease. Taking the test(s) requires eating gluten for a few weeks beforehand - a process which isn\u0026rsquo;t worth the discomfort (to me).\nWith that in mind I might have products on here which are not certified for someone with coeliac disease, such as oat milk, as I don\u0026rsquo;t seem to have any noticeable reaction. I\u0026rsquo;m not handing out any official dietary advice here so you do what\u0026rsquo;s right for you.\nPlant-based substitutes - rolling the dice # I want to highlight that there is an awful lot of terrible vege* food and substitutes in the world, so if you\u0026rsquo;ve tried one thing before and it\u0026rsquo;s made you write off the whole concept, I implore you to try a few of the things in this list and see if you still feel the same. There\u0026rsquo;s a running joke within vege* circles that companies threatened by the rise of plant-based substitutes deliberately release products which are awful enough to taint the ecosystem; I\u0026rsquo;m sure it\u0026rsquo;s not the case but sometimes (as with lots of food/restaurants) one really does question whether someone in the production line or kitchen actually ate what they are making/serving.\nAnyway, the range and quality available these days is astounding so I\u0026rsquo;m sure there\u0026rsquo;s something out there for everyone.\nYour recommendations are unhealthy # Yes, probably. I\u0026rsquo;m not really one of those health-focused plant-based people. I\u0026rsquo;m not anti-health and I eat plenty of things are aren\u0026rsquo;t processed (such as, you know, vegetables), but if I\u0026rsquo;m going to make some boss potato mash or a \u0026lsquo;cheese\u0026rsquo; toasty that will make your eyes roll into the back of your head with pleasure, I\u0026rsquo;m going to use some fat and salt. I used to be really pedantic about my food and these days I\u0026rsquo;m much happier to eat whatever I feel like in relative moderation.\nLocation # I\u0026rsquo;m currently based in southern England so some of my grocery recommendations may be localised/unavailable where you are.\nButters \u0026amp; creams # I\u0026rsquo;ve used many butter substitutes over the years but I\u0026rsquo;ve finally settled on this Flora one - it\u0026rsquo;s so on-par with normal butter my partner\u0026rsquo;s 80-year-old father stole half a block thinking it was \u0026lsquo;real butter\u0026rsquo; we\u0026rsquo;d bought just for his visit.\nFlora Salted Block For creams, the oat ones are banging:\nCreamy Oat Creamy Oat Fraiche Breads # A great breakfast deserves some thick crusty bread slathered with butter and if you\u0026rsquo;re prone to avoiding the gluten do yourself a favour and check out The Gluten Free Bakery.\nAdditionally, if you can get down there, the Infinity Foods bakery in Brighton does a tapioca and rice flour loaf which is eye-rolling good, though the loaves are in the smaller dimensions you might be familiar with if you\u0026rsquo;ve been eating GF for a while.\nCheeses # Don\u0026rsquo;t give up on vegan cheese, there are plenty of bad/average ones but there\u0026rsquo;s also some absolute bangers. Top of the list is pretty much anything by La Fauxmagerie and I DARE YOU to not find their Brixton Blue an absolute game changer.\nThe soft cheeses (Brixton Blue/Boursin) pair well with general cheeseboard fare such as crackers, grapes, pears, olives, nuts, chutneys or jams, and pickles. The Applewood block is perfect for grating onto pizzas and unlike some plant-based cheeses, does a pretty good job of melting down. Slices of course can be used in sandwiches or toasties as you would normal cheese. The Violife feta is a pretty decent feta substitute; I use it in salads or avocado + feta on toast.\nLa Fauxmagerie - Brixton Blue Boursin - Plant-Based Garlic \u0026amp; Herbs Applewood - Plant based block Violife - Just like Feta Cathedral City - Plant based slices Milk # Alternative milks have been a thing for so long now I\u0026rsquo;m sure you\u0026rsquo;ll find one to your liking. I generally use Oatly Barista Edition as it tastes good and froths well - although I don\u0026rsquo;t think the UK/EU version is suitable for coeliacs.\nTofu \u0026amp; bean curd # Tofu is pretty ubiquitous these days and there\u0026rsquo;s plenty of pre-flavoured options, some of my favs the Clearspring silken tofu (available in many supermarkets) which makes a boss tofu scramble and the Taifun ones which are full of flavour and great in all manner of things such as rice bowls, sushi rolls etc.\nClearspring - Organic Japanese Silken Tofu Taifun - Smoked tofu almond sesame Taifun - Tofu fillet wild garlic If you\u0026rsquo;re lucky enough to have one, check out your local asian shop for some tofu-knots or various bean-curd sheets or rolls.\nAjvar - the G.O.A.T. # Ajvar (pronounced aye-vahr) is an eastern-european spread which was one of my most delicious discoveries. It has become increasingly popular over the years and you can find at least one version of it in every-day supermarkets. That said, not all ajvar is created equal. I have not conducted an exhaustive review of global ajvar production but of the ones I have tried my preferred brands are (in order):\nGrandma\u0026rsquo;s recipe - Mild Grandma\u0026rsquo;s recipe - Hot Mama\u0026rsquo;s - Mild Mama\u0026rsquo;s - Hot Pelagonia / The Macedonian Kitchen Grandma\u0026rsquo;s (from Ask Foods in Kosovo) is much harder to find. I emailed them a few years ago asking if I could buy a case of it, but alas they never returned my email.\nMama\u0026rsquo;s is easier to find and comes in a close second.\nLast but not least, Pelagonia has done great work bringing ajvar to the common people, stocking at a selection of supermarkets which should make acquisition a breeze.\nPersonally, I would avoid the Podravka brand if you see it. It was not to my liking and I cannot endorse it for this recipe. My preference is for creamy consistency and closer to orange in colour - the ones I\u0026rsquo;ve tried that are redder/wetter or mixed with tomato (??) have not sparked joy, but each to their own I guess.\nIf you want to know more about ajvar, Balkan Lunch Box has a really good write-up and recipe to make it from scratch (it\u0026rsquo;s pretty intensive!).\nMeat substitutes # I know many prefer not to partake in the meat substitutes, but as someone who did eat meat (mostly) until around my early thirties, I appreciate there being some meat-like textural options available. There are so many options now you\u0026rsquo;re likely to find something for everyone. Here are some of my favourites, though:\nLa Vie - Smoked bacon\nLa Vie - Honey roast vegan ham\nTHIS Isn\u0026rsquo;t - Caramelised onion pork sausages\nTHIS Isn\u0026rsquo;t - Roast chicken \u0026amp; stuffing\nBeyond meat - Beyond burger\nStock # My mother used these - Massel - Ultracubes Stock Cubes Vegetable - and I\u0026rsquo;ve never found a better stock cube.\nThey taste great, dissolve well, and are an accessible cooking cheat code that\u0026rsquo;s right up there with onions/garlic/olive oil.\nThey\u0026rsquo;re Australian so slightly harder to find, but you can buy a 12-pack from Broadway Candy which makes no sense to me given it\u0026rsquo;s an online American supermarket, go figure.\nKala namak # Also known as Indian black salt, this is kiln-fired rock salt that has a distinct sulphurous/eggy smell. This is the star seasoning of my scramble tofu dish but can be used anywhere you\u0026rsquo;re going for that deep flavour.\nSalthouse \u0026amp; Peppermongers have it in chunks suited to use with a grinder or pestle and mortar, as well as the pre-ground version.\nDon\u0026rsquo;t forget: if you see black salt in the shops, it doesn\u0026rsquo;t mean it\u0026rsquo;s going to be the kiln-fired version. I\u0026rsquo;ve made that mistake a few times!\nKraut # Do yourself a favour and make your own kraut! You will need something like:\nFermentation jar such as this 3L Kilner ~2kg of white and/or red cabbages Around 20g of salt per kilo of cabbage Slice it up Massage it in a big bowl with the salt until it\u0026rsquo;s super juicy I like to add whole peppercorns, dill, and fennel seeds Stuff it in the jar and leave it on the bench for 2–3 weeks There are so many fermentation guides out there, so read a few blogs to learn about temperature and hygiene best practices.\nBut for the times you don\u0026rsquo;t want to DIY, there are plenty of kraut options in most supermarkets. If I\u0026rsquo;m not getting a heat-treated/shelf-stable one, I\u0026rsquo;ll usually pick up something like Vadasz - Garlic and dill sauerkraut.\n","date":"19 May 2026","externalUrl":null,"permalink":"/posts/plant-based-gf-groceries/","section":"Posts","summary":"","title":"Plant-based and gluten-free foodstuffs","type":"posts"},{"content":" Ingredients # Serves two. Refer to my foodstuffs post for the plant-based brands used in this recipe. I\u0026rsquo;ll link each ingredient to the relevant sections just in case.\nFor the scramble:\n1 pack of Clearspring silken tofu (300g) 1 medium-sized brown or red onion 2 - 5 garlic cloves (depending on your enthusiasm) Olive oil Kala namak salt Turmeric powder Nutritional yeast flakes (aka nooch) Spring onions Accompaniments:\nPlant-based bacon Sauerkraut Ajvar Avocado Toast Butter A word on substitutions # I\u0026rsquo;ve tried a few different silken tofus with this recipe and I think the texture of the Clearspring one is superior, but if you can\u0026rsquo;t get a hold of it swap in whichever silken tofu you have available.\nThe kala namak, however, is the star and can\u0026rsquo;t be substituted. This ingredient is a must. Although kala namak translates to black salt, it\u0026rsquo;s not just any black salt. It\u0026rsquo;s Indian black salt and should have a distinct sulfuric, \u0026ldquo;eggy\u0026rdquo; aroma. Check out my foodstuffs post for a link to where you can purchase it.\nRegarding brown or red onion, they both come out nice in my opinion. Using a red will make it a little sweeter but it should be fine with whatever you have in the drawer.\nProcess # Before you start, drain the tofu and sit it in a colander - this lets some of the water drain out of it.\nDice up the onions and garlic and throw them in a fry pan on medium heat with a generous amount of olive oil. We want them turning soft and glassy - not flash frying or burning - so give them time and stir as needed.\nGet the \u0026lsquo;bacon\u0026rsquo; going in another pan, it won\u0026rsquo;t take as long to cook so can be on a lower heat initially - manage this on the side throughout the main tofu process, turning as needed, and pop to the side when done.\nOnce the onions are looking good, move them to the sides of the pan and lightly break up the tofu block into the centre. Don\u0026rsquo;t pulverise it; we want some variation of chunks to remain, anything around 1 - 3cm should be good.\nSprinkle the kala namak evenly over the chunks - it should look like a lightly speckled egg. If you\u0026rsquo;re a fan of the salt like I am, speckle it harder.\nFollow with a half teaspoon/a few pinches of turmeric dusted lightly over the full surface area - don\u0026rsquo;t just drop it in one spot. This is more for colour than flavour so don\u0026rsquo;t go overboard otherwise it gives it a pasty consistency.\nLastly, add 2 - 3 heaped tablespoons of nooch, spread evenly over the tofu. Don\u0026rsquo;t be skimpy with the nooch.\nNow fold the onions over the top of the tofu so they don\u0026rsquo;t burn and let it sit for a bit. From here it should only need 3 - 5 gentle folds to ensure everything is mixed through. Let it sit for ~3 minutes after each stir and don\u0026rsquo;t whisk it around like you\u0026rsquo;re making an omelette, silken tofu is delicate and you\u0026rsquo;ll just end up with a slurry - no one wants that. Use a spatula and use a folding motion like in baking, gently combining the ingredients.\nThe scramble should be taking less of your attention now so while that\u0026rsquo;s cooking get the plates ready with a big dollop of ajvar, an amount of sliced avocado you\u0026rsquo;re happy with, and a generous forkful of kraut. A little lemon/salt/ground pepper on the avocado wouldn\u0026rsquo;t go astray.\nCut that toast to suit your preferred thickness and get it toasting, dice the spring onions while you wait.\nOnce everything is looking good, butter the toast, serve the tofu, garnish with spring onions and dig in.\n","date":"19 May 2026","externalUrl":null,"permalink":"/posts/tofu-scramble/","section":"Posts","summary":"","title":"Plant-based tofu scramble","type":"posts"},{"content":"","date":"19 May 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"19 May 2026","externalUrl":null,"permalink":"/tags/recipe/","section":"Tags","summary":"","title":"Recipe","type":"tags"},{"content":"","date":"19 May 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"14 May 2026","externalUrl":null,"permalink":"/tags/azure/","section":"Tags","summary":"","title":"Azure","type":"tags"},{"content":"This post will hopefully give an overview of the various entities which might need to authenticate to Azure, where this authentication can happen, and what resources/setup might be required to make it happen.\nThese are the objects in scope for this post:\ngraph BT Azure[\"☁️ Azure\"] User[\"👤 Users Entra ID\"] App[\"🧩 Applications .Net code\"] Pipe[\"🚀 Pipelines Azure DevOps\nGitHub Actions\"] Res[\"⚙️ Resources Azure\"] User --\u003e Azure App --\u003e Azure Pipe --\u003e Azure Res --\u003e Azure Before we dive straight into these objects though I\u0026rsquo;ll start by introducing some of the fundamental pieces which allow all of this to work.\nFundamentals # Who you are | What you can do # Before we crack on I want to clarify about authentication and authorisation. If you\u0026rsquo;ve ever logged into a service (successfully or not) such as social media or your company\u0026rsquo;s internal business applications, you have partaken in authentication - also known as AuthN. This is where some kind of identity provider validates you are who you say you are.\nAuthN is only one part of the picture though - once logged in you\u0026rsquo;ll likely want to perform some actions such as upload images or update some client details. The ability to perform these actions is part of authorisation, also known as AuthZ.\nJust because you logged into a service successfully, it doesn\u0026rsquo;t mean you can do things like delete other people\u0026rsquo;s posts, or access protected areas of your business applications which might be reserved for certain departments.\nAuthentication - who are you, and authorisation - what can you do, are two essential (and separate) pieces to keeping most modern tech systems safe. This is relevant across all the identities we\u0026rsquo;ll review because successfully authenticating with an identity doesn\u0026rsquo;t mean it\u0026rsquo;s been authorised to take action; we\u0026rsquo;ll need to pair this with some kind of authorisation, such as Azure Role Based Access Control (RBAC) assignments.\nService principals # Service principals are objects within Entra ID which can support human and non-human authentication, kind of like a service account. I don\u0026rsquo;t want to launch into a whole in-depth side quest, so for more details and a far better explanation than I could do check out Azure AD App Registrations, Enterprise Apps and Service Principals by John Savill which, although a few years old, still holds up really well. Service principals can be seen within the Azure portal under the Enterprise applications blade - here\u0026rsquo;s an example:\nand the \u0026lsquo;application object\u0026rsquo; is under the App registrations blade:\nDon\u0026rsquo;t get me started on the terminology around these, I have to review the docs every time I try to explain it.\nService principals require some kind of credential setup, they can use secrets (aka password), certificates, or to be rid of passwords forever, a federated credential which leverages workload identity federation. This type of authentication is preferred over secrets as it will dynamically issue short-lived access tokens as required.\nThese can all be configured in this certificates \u0026amp; secrets section:\nManaged identities # Managed identities are a type of service principal but they\u0026rsquo;re unique enough to call out as their own thing. They\u0026rsquo;re super useful because essentially they can bestow an identity upon an Azure resource; we\u0026rsquo;ll see how that looks when we review resources in more detail later.\nMany Azure services and resource types support managed identity - I just want to highlight how excellent this is because if you didn\u0026rsquo;t catch it:\nManaged identities perform secretless authentication!\nWho doesn\u0026rsquo;t want less passwords in their life?! Anyway, managed identities come in two flavours - user-assigned managed identities (UMI) and system-assigned managed identities (SMI). Both have a similar function, but depending on what you need to achieve one might be better suited than the other. The summary is SMIs are tied to the lifecycle of the resource (such as a virtual machine) and deleting the virtual machine means the identity will also be deleted. In contrast, UMIs are an actual Azure resource which can be attached to any resource which supports it, separating the lifecycle of the resource and the identity.\nThere\u0026rsquo;s plenty of guidance from Microsoft about when you might choose one over the other but best practice doesn\u0026rsquo;t mean blind capitulation - do your research on the scenario and then pick a way forward.\nHere\u0026rsquo;s some examples within the Azure portal showing where the identity settings usually are. Most supported resources have an Identity setting in the left-hand menu, the exact location and wording can vary resource by resource so explore until you find it.\nSystem identity Before enabled: After enabled:\nUser identity Before any user identities have been attached to the resource:\nAfter attaching one:\nNote Enabling or creating identities will register them into the Entra directory as a Service Principal. You can see these if you go searching within the Enterprise applications section of the Azure portal. Info Some additional points about UMIs:\nAttaching an identity to a resource requires certain permissions; write, over the resource, and Microsoft.ManagedIdentity/userAssignedIdentities/*/assign/action over the user-assigned identity. User-assigned managed identities cannot be moved across a resource group or subscription boundary. Enabling Isolation Scope can restrict the use of a user-assigned identity to a specific region. An identity story # Some time ago I had to migrate some Web Apps configured with SMI to Azure Container Apps. With system identities, this meant all the role assignments had to be re-provisioned (duplicated for the duration of the migration). If user-assigned identities had been used I could have attached the same identity to the new container apps and presto, they gain all the same permissions.\nThere\u0026rsquo;s a similar argument to be made for Azure Container Apps -\u0026gt; Azure Kubernetes Service migrations - with an existing user-assigned identity that is already authorised with all the permissions your application needs it may make migrations a little easier.\nThe way I see it, the lifecycle of a business application isn\u0026rsquo;t necessarily tied to its resource type as the underlying compute might change for various reasons. If your compute changes are always tied to moving to new resource groups or subscriptions then the UMI becomes a little less attractive.\nUsers # Whether you\u0026rsquo;re testing some Terraform or developing an application, working locally within a Microsoft/Azure ecosystem will likely require access to some Azure resources at some point. What\u0026rsquo;s the identity you\u0026rsquo;re using when you connect? The simplest answer is probably your Entra login; the one you use to access your laptop, sign into business applications etc.\nIt\u0026rsquo;s pretty easy to authenticate as a user from your local machine and there are a number of tools you can use to achieve this; at the end of the day all the requests are going to the Azure Resource Manager so which tool you use is up to you (or your internal policies). Whether you\u0026rsquo;re browsing the Azure portal, connecting via Azure CLI with az login, PowerShell with Connect-AzAccount, or signed in through VS Code/Visual Studio\u0026hellip;\nall roads lead to ARM!\n(credit Microsoft) As long as someone or something has provisioned it, users can also authenticate using a service principal. The secret can either be a password or certificate. Don\u0026rsquo;t forget this is only the authentication piece, if the service principal hasn\u0026rsquo;t been granted any permissions they won\u0026rsquo;t be able to do much.\nHere are the az cli examples - any of the tools mentioned above should have their own implementation for this:\naz login --service-principal --username APP_ID --password CLIENT_SECRET --tenant TENANT_ID az login --service-principal --username APP_ID --certificate /path/to/cert.pem --tenant TENANT_ID Warning User accounts shouldn\u0026rsquo;t be tied to any production resources or processes - if they\u0026rsquo;re deactivated/locked out then anything relying on it will likely cease to function.\nResources # If you\u0026rsquo;re deploying resources to Azure it\u0026rsquo;s highly likely some of them are going to need to interact with each other. Although some resources can be accessed using Shared Access Keys (essentially a password), where possible it\u0026rsquo;s recommended to leverage managed identities instead.\nThe process for assigning managed identities is similar across the board, here\u0026rsquo;s a ClickOps example of enabling an SMI on a web app and granting the identity access to a storage account:\nStep 1 - Enable managed identity Step 2 - Assign access to storage Remember our AuthN and AuthZ - the first demo shows the identity being enabled so when the web app attempts to talk to the managed identity infrastructure it can authenticate with a successful result. The second demo shows creating the Azure RBAC assignment with authorises the web app to access the storage account.\nSome examples of resource-to-resource access might be:\nApplication gateway -\u0026gt; key vault to access certificates required for connecting securely to, as an example, https://your-app.company.com Web app/container app -\u0026gt; app config or database Virtual machine (VM) -\u0026gt; storage account Note Some organisations might have policies that prevent logging in with your Entra account on any device which isn\u0026rsquo;t your primary computer/laptop - if this is the case using az login or Connect-AzAccount wouldn\u0026rsquo;t work from within the virtual machine.\nRegarding VMs, deploying a new virtual machine won\u0026rsquo;t do anything by default, identities only come into play when you want to run an application or script within the VM. Let\u0026rsquo;s imagine you\u0026rsquo;ve created a new VM and enabled its system-assigned managed identity; after you log into it what kind of identities/auth methods can you use?\nAssuming Azure CLI or PowerShell were installed, similar to your local machine you could login with:\nyour Entra user account (using az login or Connect-AzAccount) a service principal (using az login --service-principal --username APP_ID --password CLIENT_SECRET --tenant TENANT_ID) or an additional option - managed identity! This can be done using\naz login --identity or Connect-AzAccount -Identity Successfully authenticating like this means you have obtained an access token for the virtual machine identity, not your user account. It should now be possible to upload a file from the local file system of the virtual machine into a storage account using az storage blob upload as long as the VM identity has been granted Storage Blob Data Contributor over the storage account.\nIt\u0026rsquo;s important to note that when a VM tries to get an access token it\u0026rsquo;s calling a special endpoint called the Azure Instance Metadata Service (IMDS). Different resources might call different endpoints, but the common theme is these endpoints are only viable for resources which support managed identity running within the Azure ecosystem - managed identities cannot be used locally.\nAdditionally, if you\u0026rsquo;ve attached a user-assigned managed identity to the virtual machine you can authenticate with it instead, using the following similar commands:\naz login --identity --client-id CLIENT_ID or Connect-AzAccount -Identity -AccountId CLIENT_ID with CLIENT_ID being the Client ID of the UMI.\nApplications # A long time ago in a galaxy far far away we used to run applications on physical hardware which were stored in the office, or we\u0026rsquo;d send our servers to a shared data centre down the road to stop worrying about the power and cooling requirements that comes with hosting your own hardware. The point is, applications always have and always will need to run on some kind of compute (that is CPU, RAM, disk, network etc), and that hasn\u0026rsquo;t changed simply because things are \u0026lsquo;in the cloud\u0026rsquo; now; the cloud is just someone else\u0026rsquo;s computer.\nWhen an application is built and published (usually as a container these days) to Azure we can choose what kind of compute will power them as per the Microsoft flowchart below - lots of options and as far as I know, all support using managed identities.\nAzure compute choices (credit Microsoft) Let\u0026rsquo;s assume we\u0026rsquo;ve decided to use Azure Container Apps to run our applications. Most applications will have some kind of dependency such as writing data to a storage account, connecting to a database, or loading in secrets from a key vault. As we\u0026rsquo;ve learned in the resources section you can enable managed identity pretty easily, but how does your application know how to use it? And how does it know when to use a service principal, managed identity, or other form of authentication?\nThe answer is we have to tell it! For .Net, the Azure.Identity library has a bunch of good stuff to help your application get authenticated and access the dependencies it needs. This library/package can be imported into your application and if you\u0026rsquo;re not using .Net, most major languages have a similar library.\nThe library has many different TokenCredential types which inform the authentication method with the two most interesting ones to me being DefaultAzureCredential and ChainedTokenCredential.\nThe first is a predefined list of credentials to attempt, whilst the second is a custom list you can create to specify just the ones appropriate for your use case. These chained credentials will try one TokenCredential type after the other, and here\u0026rsquo;s the provider list for DefaultAzureCredential:\nDefaultAzureCredential precedence Order Credential Description 1 Environment Reads environment variables to determine if an application service principal is configured for the app. If so, uses these values to authenticate the app to Azure. Most often used in server environments but can also be used when developing locally. 2 Workload Identity If the app is deployed to an Azure host with Workload Identity enabled, authenticate that account. 3 Managed Identity If the app is deployed to an Azure host with Managed Identity enabled, authenticate the app to Azure using that Managed Identity. 4 Visual Studio If the developer authenticated to Azure by logging into Visual Studio, authenticate the app to Azure using that same account. 5 Visual Studio Code If the developer authenticated via Visual Studio Code\u0026rsquo;s Azure Resources extension and the Azure.Identity.Broker package is installed, authenticate that account. 6 Azure CLI If the developer authenticated to Azure using Azure CLI\u0026rsquo;s az login command, authenticate the app to Azure using that same account. 7 Azure PowerShell If the developer authenticated to Azure using Azure PowerShell\u0026rsquo;s Connect-AzAccount cmdlet, authenticate the app to Azure using that same account. 8 Azure Developer CLI If the developer authenticated to Azure using Azure Developer CLI\u0026rsquo;s azd auth login command, authenticate with that account. 9 Interactive browser If enabled, interactively authenticate the developer via the current system\u0026rsquo;s default browser. 10 Broker Authenticates using the default account logged into the OS via a broker. Requires that the Azure.Identity.Broker package is installed. This link also breaks down which ones to use in different situations.\nAn application that has been configured to use AzureDefaultCredential will go through this list in order and try to obtain a token. This includes a number of credential types, so sometimes people make a ChainedTokenCredential which specifically includes just the ones they want.\nI want to specifically call out the behaviour of how chained credentials work:\nAt runtime, a credential chain attempts to authenticate using the sequence\u0026rsquo;s first credential. If that credential fails to acquire an access token, the next credential in the sequence is attempted, and so on, until an access token is successfully obtained.\nLet\u0026rsquo;s tuck this little fact away for now and switch back to the container app. So far you\u0026rsquo;ve:\nwritten some code, used the Azure.Identity library, and chosen the AzureDefaultCredential token credential class built and published your application as a container image to a container registry deployed an Azure Container App specifying your application image Assuming everything goes well the application should start up and attempt to request a token using the first credential in the list: environment variables. The application will check if it can see AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET environment variables and if not, move on. Given we didn\u0026rsquo;t configure these, it will progress on to workload identity which will also be a negative as this is for use within Azure Kubernetes Service (AKS). Next up, it will try managed identity and as long as we enabled SMI or UMI on the resource this should be successful!\nThe application now has an access token it can use to connect to dependencies within Azure such as storage, key vault, database etc. Keep in mind successfully obtaining a token doesn\u0026rsquo;t mean it can do much with it - the identity must be granted permissions (aka authorisation!) such as Storage Blob Data Contributor on the storage resources, or db_datawriter on the database so the application can perform its work.\nNote If you\u0026rsquo;ve decided to use a UMI for your resource (container app, web app etc) the client id - one of the properties of a user-assigned identity - has to be made accessible to the application code as an environment variable or configuration value as this is required in the credential setup. For example:\nvar clientID = Environment.GetEnvironmentVariable(\u0026#34;Managed_Identity_Client_ID\u0026#34;); var credentialOptions = new DefaultAzureCredentialOptions { ManagedIdentityClientId = clientID }; var credential = new DefaultAzureCredential(credentialOptions); Obviously Microsoft have huge amount of documentation on how all this is meant to work - review the guidance on using managed identities in your code.\nFor SMI, as long as you\u0026rsquo;re using the Azure.Identity packages correctly you don\u0026rsquo;t need to provide any additional configuration values.\nPipelines # Ok pipelines! A quick primer in case you\u0026rsquo;re not familiar - pipelines help deliver code (aka features our customers can use) into our production environments. They do this by executing all sorts of tasks as defined in some YAML file, and if we follow that thread, executing something means some form of compute is required, such as a virtual machine.\nThese virtual machines are usually referred to as agents or runners - and they can be hosted for you, or you can create and host them yourself within your Azure tenant. Examples are GitHub-hosted and GitHub Self-hosted runners, along with Azure DevOps Microsoft-hosted agents and Azure DevOps Self-hosted agents.\nLet\u0026rsquo;s get one thing out of the way first:\nHosted agents cannot use managed identities.\nOnly self-hosted agents whose virtual machine/container resources are running within the Azure eco-system can leverage SMI/UMI. So what does this leave us with?\nAs we\u0026rsquo;ve learned a service principal is an object within Entra ID which can be granted permissions, such as Contributor access over a subscription, so when the pipeline executes tasks they can perform actions. The way authentication happens varies depending on your CI/CD tooling, I\u0026rsquo;ll cover two I\u0026rsquo;m familiar with: GitHub Actions and Azure Pipelines.\nNotable CI/CD differences # There\u0026rsquo;s a few implementation differences when it comes to CI/CD authentication which will be useful for the examples that follow, read on!\nService connections # Azure Pipelines use service connections which are:\n\u0026ldquo;authenticated connections between Azure Pipelines and external or remote services that you use to execute tasks in a job.\u0026rdquo; - learn.microsoft.com\nflowchart LR subgraph Azure DevOps P[\"Pipeline task\"] end subgraph Azure DevOps SC[\"Service connection\"] end subgraph Azure SP[\"Service Principal\"] end P --\u003e|uses| SC SC --\u003e|which is\nsome kind of| SP Service connections sit within an Azure DevOps Project and are used by tasks (such as AzureCLI) to perform authentication - they\u0026rsquo;re essentially an Azure Devops specific abstraction around the service principal which is doing all the real work. Any task which supports the use of a service connection will have a property called azureSubscription, connectedServiceName, or connectedServiceNameARM (there might be other aliases too) where you specify the name of the service connection to use.\nNote The service connection name within Azure DevOps is what must be used on the task property; Service connections can be viewed from within an Azure DevOps project via Project settings -\u0026gt; Service connections.\nService connections can be powered by several variations of service principal - when creating the service connection you can specify whether it will use workload identity federation, user-assigned managed identity, or a secret to perform its authentication.\nIn contrast, GitHub Actions doesn\u0026rsquo;t have anything like the Azure DevOps service connection object - login tasks will directly reference service principal/identity details depending on the method you want to use.\nAuthentication - one vs many # GitHub Actions can use the azure/login action to acquire an access token which is then stored locally on the agent; this task only needs to run once and then most subsequent steps in the workflow which need to interact with Azure can then use this token - though an example of an exception might be docker/login-action.\nHowever, within Azure Pipelines, each task (which requires authentication) must be provided a service connection via a task property (such as azureSubscription) and each task performs a login and logout - the access token is not reused between tasks like in GitHub Actions.\nAzureCLI behaviour # The Azure Pipelines AzureCLI task executes using whichever version of Azure CLI software is installed on the agent.\nThe equivalent in GitHub Actions however is the azure/cli action which runs in a container with minimal software - which means you can specify the version if you don\u0026rsquo;t want to run latest (neat!). How is authentication working if it\u0026rsquo;s running inside a container I hear you ask? See for yourself - it\u0026rsquo;s mounting the .azure directory from the host which contains the access token.\nAlso within GitHub Actions, as long as the azure/login action has completed successfully you can execute arbitrary az commands, such as az account show, just using a run task. Attempting the same in Azure Pipelines will result in the message ERROR: Please run 'az login' to setup account because as we learned above, authentication occurs automatically when using a service connection enabled task and the script, bash, pwsh, and powershell etc steps do not use a service connection. To execute az commands in an authenticated session it\u0026rsquo;s better to use the AzureCLI or AzurePowerShell tasks.\nNow that we\u0026rsquo;ve reviewed some of the differences, lets see a few examples of authentication with different identities.\nPipeline auth options summary # Before we look at a few authentication examples let\u0026rsquo;s recap the options available to us when using executing tasks via CI/CD pipelines.\nflowchart LR HA[\"Hosted agent\"] SHA[\"Self-hosted agent\"] SP[\"Service principal (OIDC or secret)\"] SPUMI[\"Service principal (UMI)\"] SMI[\"SMI (attached)\"] UMI[\"UMI (attached)\"] RG[\"Resource group / Subscription etc\"] HA --\u003e SP HA --\u003e SPUMI SHA --\u003e SP SHA --\u003e SPUMI SHA --\u003e SMI SHA --\u003e UMI SP --\u003e|RBAC| RG SPUMI --\u003e|RBAC| RG SMI --\u003e|RBAC| RG UMI --\u003e|RBAC| RG We\u0026rsquo;ve reviewed that pipelines execute tasks on hosted or self-hosted agents, and the authentication to Azure will use service principals or system and user-assigned identities. We know identities and service principals need to be granted RBAC permissions (authorisation) within Azure before they can create/modify/access resources and data.\nLet\u0026rsquo;s check out some examples - expand each section for specific notes and an example task. For additional information you can always check the docs for connecting to Azure from GitHub Actions and creating Azure DevOps service connections.\nGitHub Actions examples # All of these examples use the azure/login action with varying properties.\nAuth using SMI Only works with self-hosted agents System-assigned managed identity must be enabled on the agent (virtual machine/container) resource Secretless - issuing managed identity access tokens is handled automatically by the Azure platform Note the auth-type here is IDENTITY - name: Azure Login using SMI uses: azure/login@v3 with: auth-type: IDENTITY tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} Auth using UMI Only works with self-hosted agents User-assigned managed identity must be attached to the agent (virtual machine/container) resource Secretless - issuing managed identity access tokens is handled automatically by the Azure platform AZURE_CLIENT_ID would be the Client ID of the Microsoft.ManagedIdentity/userAssignedIdentities resource Note the auth-type here is IDENTITY - name: Azure login using UMI uses: azure/login@v3 with: auth-type: IDENTITY client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} Auth using service principal (UMI) Works with hosted and self-hosted agents Secretless - uses OIDC flow between Azure and GitHub AZURE_CLIENT_ID would be the Client ID of the Microsoft.ManagedIdentity/userAssignedIdentities resource Note the auth-type here is SERVICE_PRINCIPAL Requires federated credentials to be configured on the UMI Does not require the UMI to be attached to the agent - name: Azure login using Service Principal (UMI) uses: azure/login@v3 with: auth-type: SERVICE_PRINCIPAL client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} Auth using service principal (OIDC) Works with hosted and self-hosted agents Secretless - uses OIDC flow between Azure and GitHub Requires federated credentials to be configured on the service principal - name: Azure login using Service Principal (OIDC) uses: azure/login@v3 with: auth-type: SERVICE_PRINCIPAL client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} Auth using service principal (secret) Works with hosted and self-hosted agents Not secretless Requires combining clientSecret, subscriptionId, tenantId, and clientId values into a GitHub secret Generally not recommended - name: Azure login using Service Principal (secret) uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} Azure Pipelines examples # Auth using service connection Works with hosted and self-hosted agents Recommended way to \u0026lsquo;do authentication\u0026rsquo; within Azure Pipelines unless you\u0026rsquo;re doing something bespoke Service connections can use various types of service principals to authenticate, this configuration is done when setting up the service connection not in the pipeline tasks Note the azureSubscription property is where the name of the service connection is specified # Example 1 - task: AzureCLI@3 displayName: \u0026#39;Azure login\u0026#39; inputs: azureSubscription: \u0026#39;SERVICE_CONNECTION_NAME\u0026#39; scriptType: \u0026#39;pscore\u0026#39; scriptLocation: \u0026#39;inlineScript\u0026#39; inlineScript: | # Executing az commands here will use the service connection\u0026#39;s service principal # Example 2 - task: AzurePowerShell@5 inputs: azureSubscription: \u0026#39;SERVICE_CONNECTION_NAME\u0026#39; ScriptType: \u0026#39;InlineScript\u0026#39; azurePowerShellVersion: \u0026#39;LatestVersion\u0026#39; pwsh: true Inline: | # Executing Az PowerShell commands here will use the service connection\u0026#39;s service principal Auth using SMI Only works with self-hosted agents The agent must have Azure CLI installed System-assigned managed identity must be enabled on the agent (virtual machine/container) resource Better suited for use with non-service connection tasks such as script, bash, pwsh, and powershell etc Will replace the access token if performed within an AzureCLI task # Example 1 - task: PowerShell@2 displayName: \u0026#39;Azure login using SMI\u0026#39; inputs: targetType: \u0026#39;inline\u0026#39; script: | az login --identity # Executing additional az commands here will use the SMI # Example 2 - pwsh: | az login --identity # Executing additional az commands here will use the SMI # Example 3 - bash: | az login --identity # Executing additional az commands here will use the SMI Auth using UMI Only works with self-hosted agents The agent must have Azure CLI installed User-assigned managed identity must be attached to the agent (virtual machine/container) resource Better suited for use with non-service connection tasks such as script, bash, pwsh, and powershell etc Will replace the access token if performed within an AzureCLI task # Example 1 - task: PowerShell@2 displayName: \u0026#39;Azure login using UMI\u0026#39; inputs: targetType: \u0026#39;inline\u0026#39; script: | az login --client-id $(CLIENT_ID) # Executing additional az commands here will use the UMI # Example 2 - pwsh: | az login --client-id $(CLIENT_ID) # Executing additional az commands here will use the UMI # Example 3 - bash: | az login --client-id $(CLIENT_ID) # Executing additional az commands here will use the UMI Auth using service principal (secret) Works with hosted and self-hosted agents APP_ID refers to the Client ID of the service principal (if viewing from App registrations portal blade) or Application ID of the enterprise app (from within Enterprise applications blade) # Example 1 - task: PowerShell@2 displayName: \u0026#39;Azure login using service principal (secret)\u0026#39; inputs: targetType: \u0026#39;inline\u0026#39; script: | az login --service-principal --username APP_ID --password CLIENT_SECRET --tenant TENANT_ID # Executing additional az commands here will use the service principal # Example 2 - pwsh: | az login --service-principal --username APP_ID --password CLIENT_SECRET --tenant TENANT_ID # Executing additional az commands here will use the service principal # Example 3 - bash: | az login --service-principal --username APP_ID --password CLIENT_SECRET --tenant TENANT_ID # Executing additional az commands here will use the service principal Service principal UMI # You may have noticed something in the examples I haven\u0026rsquo;t explained - what\u0026rsquo;s the difference between these three:\nAzure login using UMI Azure login using Service Principal (OIDC) Azure login using Service Principal (UMI) The first is creating a UMI, attaching it to the agent resource, operating as that identity in the pipeline. The second is creating a standard service principal and logging in with those details. Finally the third one is creating a UMI resource (which don\u0026rsquo;t forget, is still a service principal) and logging in with those details.\nIgnoring the first one for now, the second option only gives you an access token that\u0026rsquo;s valid for 60 minutes; perfectly fine for most pipeline jobs but what happens if you go beyond an hour? Tasks will fail with AADSTS700024: Client assertion is not within its valid time range.\nUsing a UMI as the service principal will issue a token that is valid for 24 hours which gives long-running jobs the extra time to complete - a real example I\u0026rsquo;ve seen is automated database restores failing because they need a few hours to complete.\nThis is called out directly in the azure/login readme:\nBy default, Azure access tokens issued during OIDC based login could have limited validity. Azure access token issued by Service Principal is expected to have an expiration of 1 hour by default. And with Managed Identities, it would be 24 hours. This expiration time is further configurable in Azure. Refer to access-token lifetime for more details. source\nNote Option 1 will also grant an access token which is valid for 24 hours, but as I\u0026rsquo;ll mention later in Can I vs should I? it\u0026rsquo;s generally not something you\u0026rsquo;d want to use.\nWarnings about device code flow # Although we said pipelines use service principals, if you really wanted to you can drop in a bash/powershell task that runs az login and attempt authentication via what\u0026rsquo;s called the device code flow. When you need to authenticate a device which isn\u0026rsquo;t interactive/doesn\u0026rsquo;t have a console, you can use this method and it should prompt you with a message like this:\nTo sign in, use a web browser to open the page https://login.microsoft.com/device and enter the code CXXC89YY1 to authenticate.\nOpening this link and performing the actions will authenticate the device as your user. Within the context of running pipelines this is an act so heretical you may be cast back to the support desk if discovered, so between that and the fact Microsoft recommends blocking device code flow wherever possible you generally don\u0026rsquo;t want to do this - it\u0026rsquo;s just interesting that one could.\nCan I vs should I? # We\u0026rsquo;ve reviewed a lot of options on what\u0026rsquo;s possible, but that doesn\u0026rsquo;t mean every method is equally appropriate. The industry has been moving away from static secrets for a long time, using federated/OIDC authentication has never been easier so honestly:\nFor Azure Pipelines, use service connections which are Workload Identity Federation service principals For GitHub actions, use service principals which have been configured with OIDC [NOTE]\nIf your pipeline jobs are running for over an hour you may need to switch to the user-assigned managed identity service principals grant access tokens with a ~24hr validity as mentioned in Service principal UMI.\nIf you have some specific requirements where it makes sense to leverage SMI/UMI of your agents then by all means, but in a busy organisation delivering lots of things via CI/CD, creating per workload service principals that have the specific access they need will generally serve you much better rather than having one deployment principal to rule them all.\nA final weird thing # Now that we know all the things I want to pose a hypothetical little scenario, although something similar to this did come up for me a few years ago. Scenario:\nPipeline running on a self-hosted agent with SMI enabled Pipeline has authenticated to Azure using a service principal Task executes an application to apply some database schema changes which is configured with a ChainedCredentialToken that includes the VisualStudioCredential, ManagedIdentityCredential, and AzureCliCredential credentials Only the service principal has been given access to the database What happens? Have a think before reading on.\nUp in the applications section it was highlighted that chained tokens would attempt each credential in order until a token was successfully obtained. The ordering in this scenario puts ManagedIdentityCredential before the AzureCliCredential so when it attempts to retrieve a token, because SMI is enabled for the agent it is successful - it doesn\u0026rsquo;t matter whether that token has the right access to the database, only that it was obtained.\nThe application attempts to update the database schema with an access token which isn\u0026rsquo;t granted the correct authorisation on the database and the step fails. To correct this we can turn of the SMI for the agent, and/or alter the chained token to place AzureCliCredential before ManagedIdentityCredential, or even remove it altogether if it\u0026rsquo;s not appropriate.\nThe issue I encountered years ago was likely because we were moving away from using agent identities towards service principals and we had a mini perfect storm which was confusing until we reviewed all the pieces.\nWrapping up # Congrats on making it this far, we\u0026rsquo;ve certainly covered a lot of ground.\nHopefully we\u0026rsquo;ve reached an understanding that standard passwords kind of suck and we should avoid them wherever possible; they\u0026rsquo;re long-lived, unwieldy to manage, and a prime target for malicious actors. Using managed identities within the Azure eco-system is an improvement in almost every way and is supported by many Azure services and resource types.\nWe covered authentication (AuthN) and authorisation (AuthZ) and how they are different pieces, authentication proving who you are and authorisation setting what actions can be taken.\nWe\u0026rsquo;ve learned that Azure managed identities broadly come in two flavours, the system-assigned and user-assigned; where the system identity is tied to the life-cycle of a specific resource (delete the resource, delete the identity), and the user identity exists independently, something which can be attached to one or more resources as required.\nWe established your Entra user account can be granted permissions in Azure and when you\u0026rsquo;re browsing via the Azure portal or connecting with az login locally, that\u0026rsquo;s your identity. We also saw that Azure resources get an \u0026lsquo;account\u0026rsquo; of their own too if they are enabled/assigned a managed identity, which allows them to access other Azure services.\nWe\u0026rsquo;ve reviewed how applications can authenticate to Azure services using various token credentials and how the behaviour can differ whether it\u0026rsquo;s running local or within Azure on a resources which supports managed identity.\nAnd lastly we learned SO MUCH about how pipelines can authenticate using identities or service principals and some of the implementation differences between Azure DevOps and GitHub.\nI hope this has been interesting and/or helpful, and until next time, stay curious.\n","date":"14 May 2026","externalUrl":null,"permalink":"/posts/azure-identities/","section":"Posts","summary":"","title":"Azure Identities - Managed or Not Here I Come","type":"posts"},{"content":"","date":"14 May 2026","externalUrl":null,"permalink":"/categories/cloud--tech/","section":"Categories","summary":"","title":"Cloud \u0026 Tech","type":"categories"},{"content":"","date":"8 March 2026","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"Originally planned as a single post, it was getting a little large so its been split into two parts. They\u0026rsquo;re both focused on theory but don\u0026rsquo;t let that put you off; I\u0026rsquo;ve really enjoyed writing them so hopefully you\u0026rsquo;ll enjoy reading them! It can really help to know how things are operating under the hood before we add additional complexity to our deployments.\nThere\u0026rsquo;s a lot in here so don\u0026rsquo;t worry if some things don\u0026rsquo;t make complete sense - right now, knowing of these concepts is the main goal.\nWhat are data types? # When we create resources using Terraform usually some information has to be provided, such as resource names and configuration details for your deployment. Most programming languages have a type system which allows you to declare that these pieces of information should be a certain type, such as number (e.g. number_of_virtual_machines = 3), and trying to assign it a value that isn\u0026rsquo;t a number should cause an error.\nData types (or just types in most conversations) are a useful feature to validate the information being received adheres to certain rules before executing code or deploying the Terraform.\nThe table below shows some of the more common types available within Terraform. Note the data type column are not types you can assign to a variable, they are the grouping term for the attribute types in the next column.\nData type Attribute types Declare with Example Primitive - string - bool - number - \u0026quot;\u0026quot; - n/a - n/a \u0026quot;some text\u0026quot; false 23 Complex - Collection - list - map - set - [] - {} - [] See Complex collection Complex - Structural - object - tuple - {} - [] See Complex structural As long as you have the Terraform extensions installed, triggering the auto-complete/intellisense on the type portion of a variable block should show a list of type constructors like this:\nIf you\u0026rsquo;re after more detail I highly recommend the HashiCorp docs on types and type constraints\nWhat about keys and values? # Info Key-value pairs might also be referred to as dictionaries, hash maps, or just maps; in general conversation it\u0026rsquo;s unlikely to be too pertinent so establish some shared understanding for yourself and others then crack on.\nKey-value pairs are just \u0026quot;some_text\u0026quot; = \u0026quot;some_value\u0026quot; - that\u0026rsquo;s it. Here\u0026rsquo;s some more examples in a Terraform locals block:\nlocals { fruit = \u0026#34;apple\u0026#34; charlotte = { favourite_food = \u0026#34;sandwiches\u0026#34; } } The key is the part on the left, the value is the part on the right. The key must always evaluate to a string type, though the value can be any supported attribute type that we saw in the table above.\nWithin the Terraform code, you can reference the value of these keys by using local.\u0026lt;key_name\u0026gt;, or if there\u0026rsquo;s sub items local.\u0026lt;key_name\u0026gt;.\u0026lt;key_name\u0026gt;, for as many keys as you need to.\nE.g. using local.fruit will return the value \u0026quot;apple\u0026quot;. Using local.charlotte[\u0026quot;favourite_food\u0026quot;] will return \u0026quot;sandwiches\u0026quot;. Using the [] syntax is a standard way to refer to keys, we\u0026rsquo;ll see more of that later.\nTypes (of types) # Good thing this isn\u0026rsquo;t confusing at all! When it comes to how things are typed in Terraform there\u0026rsquo;s two methods, and using language agnostic terms they are explicit (directly express something) and implicit (something is assumed/suggested/inferred).\nType constraints (explicit) # Note Type constraints or type enforcements are the Terraform terms for the general concept of strong typing, which may also be referred to as explicit typing, static typing, and many others; they all refer to similar practice of specifying what kind of data variables can accept.\nWithin Terraform variable blocks you can declare type constraints using type constructors which helps you set out rules for what kind of data you will allow into the variable. This can be done using the type attribute, such as:\nvariable \u0026#34;some_boolean_variable\u0026#34; { type = bool default = 23 } This variable type is bool, which means it can only accept one of true, false, \u0026quot;true\u0026quot;, or \u0026quot;false\u0026quot; as a value. Attempting to run a terraform validate over this code will give an error like the below because we are attempting to assign an incorrect value given the specified type constraint:\n│ Error: Invalid default value for variable │ │ on xx.tf line xx, in variable \u0026ldquo;some_boolean_variable\u0026rdquo;: │ xx: default = 23 │ │ This default value is not compatible with the variable\u0026rsquo;s type constraint: bool required, but have number.\nInfo If you thought the \u0026quot;true\u0026quot;/\u0026quot;false\u0026quot; values being allowed on a boolean type was odd, good catch! This is because Terraform\u0026rsquo;s automatic type conversion is being sneaky and converting values. Terraform will convert a string value of \u0026quot;true\u0026quot;/\u0026quot;false\u0026quot; to the boolean value true/false.\nInferred types (implicit) # In contrast to strong types, an inferred type is when Terraform makes a best effort guess as to what the data type might be. A quick reminder for the examples below that in a locals {} block, values are on the left side of the = and the expressions are on the right side.\nlocals { some_number = \u0026#34;23\u0026#34; todo_list = [\u0026#34;acquire snacks\u0026#34;, \u0026#34;prepare snacks\u0026#34;, \u0026#34;eat snacks\u0026#34;] permissions_object = { admin = { name = \u0026#34;Alice\u0026#34; age = 30 } } } Terraform will set these value types as:\nsome_number will be a string type because the expression is enclosed in double quotes (\u0026quot;\u0026quot;) which indicates a string todo_list will be a type tuple([string, string, string,]) and not a list(string) because we have wrapped the expression in square brackets [] which declare either lists, tuples, or sets - more on this later permissions_object will be an object({...}) type because we have declared it using the curly braces {} which declare objects or maps If you\u0026rsquo;re wondering if this behaviour is taking some liberties and you would prefer to set what kind of types you\u0026rsquo;re working with, there is hope! Enter the Terraform type conversion functions!\nThese allow you to inform Terraform what the type should be instead of letting it guess - let\u0026rsquo;s apply them to the previous local values:\nlocals { some_number = tonumber(\u0026#34;23\u0026#34;) todo_list = tolist([\u0026#34;acquire snacks\u0026#34;, \u0026#34;prepare snacks\u0026#34;, \u0026#34;eat snacks\u0026#34;]) permissions_object = tomap({ admin = { name = \u0026#34;Alice\u0026#34; age = 30 } }) } So we\u0026rsquo;ve made use of the tonumber(), tolist(), and tomap() type conversion functions which now mean:\nsome_number will be a number todo_list will be a list permissions_object will be a map(object({...})) In isolation this might sound a bit dull, but it\u0026rsquo;s really useful (and interesting!) once you start declaring and transforming more complex objects.\nComplex collection # Now that we\u0026rsquo;ve rounded out a bit of theory on data types and setting constraints, lets look at some typed variable blocks. I\u0026rsquo;ll also display the JSON version, as I found this really helps wrap my head around data as it gets a bit more complex (especially when we start looking at loops/iterations).\nLists # Some neat facts about lists are:\nThey\u0026rsquo;re ordered, which means moving the position of items might trigger a deployment change They can contain duplicate values, [\u0026quot;bob, \u0026quot;bob\u0026quot;, \u0026quot;bob\u0026quot;] will be treated as 3 elements All values must be the same type, as an example you can\u0026rsquo;t have a list which contains strings and objects Lists can be joined together using the concat() function List values can be accessed using: index lookup syntax: var.my_list[\u0026lt;index number\u0026gt;] element() function lookup: element(var.my_list, \u0026lt;index number\u0026gt;) Check out these examples to see what basic list variables look like:\nExample 1 - list of strings variable \u0026#34;list_string_variable\u0026#34; { type = list(string) description = \u0026#34;A list of items to purchase from the shop.\u0026#34; default = [\u0026#34;beans\u0026#34;, \u0026#34;ice-cream\u0026#34;, \u0026#34;oats\u0026#34;] } and it\u0026rsquo;s JSON representation:\n[ \u0026#34;beans\u0026#34;, \u0026#34;ice-cream\u0026#34;, \u0026#34;oats\u0026#34; ] Examples of accessing values for var.list_string_variable:\nvar.list_string_variable[0] would return beans element(var.list_string_variable, 1) would return ice-cream Example 2 - list of objects variable \u0026#34;list_object_variable\u0026#34; { type = list(object({ name = string age = number })) description = \u0026#34;A list of people.\u0026#34; default = [ { name = \u0026#34;Pradeep\u0026#34;, age = 30 }, { name = \u0026#34;Sertan\u0026#34;, age = 25 }, { name = \u0026#34;Anya\u0026#34;, age = 28 } ] } and it\u0026rsquo;s JSON representation:\n[ { \u0026#34;name\u0026#34;: \u0026#34;Pradeep\u0026#34;, \u0026#34;age\u0026#34;: 30 }, { \u0026#34;name\u0026#34;: \u0026#34;Sertan\u0026#34;, \u0026#34;age\u0026#34;: 25 }, { \u0026#34;name\u0026#34;: \u0026#34;Anya\u0026#34;, \u0026#34;age\u0026#34;: 28 } ] Examples of accessing values for var.list_object_variable:\nvar.list_object_variable[0].name would return Pradeep element(var.list_object_variable, 1).name would return Sertan Maps # I think maps are great - I\u0026rsquo;m confident you will come to love them also. Some neat facts about maps are:\nThey have no guaranteed order - it\u0026rsquo;s a feature not a bug; if you need to preserve order, use a list Maps always have keys, and keys are always strings Maps can be merged together using the merge() function Map values can be accessed using: key reference syntax: var.my_map[\u0026quot;key\u0026quot;] lookup() function: lookup(var.my_map, \u0026quot;key\u0026quot;, \u0026quot;default_value_if_key_not_found\u0026quot;) Check out these examples to see what basic map variables look like:\nExample 1 - map of strings variable \u0026#34;resource_tags\u0026#34; { type = map(string) description = \u0026#34;A map with string keys and values\u0026#34; default = { owner = \u0026#34;team-a\u0026#34; cost_centre = \u0026#34;123\u0026#34; env = \u0026#34;test\u0026#34; } } and it\u0026rsquo;s JSON representation:\n{ \u0026#34;owner\u0026#34;: \u0026#34;team-a\u0026#34;, \u0026#34;cost_centre\u0026#34;: \u0026#34;123\u0026#34;, \u0026#34;env\u0026#34;: \u0026#34;test\u0026#34; } Examples of accessing values for var.resource_tags:\nvar.resource_tags[\u0026quot;owner\u0026quot;] would return team-a lookup(var.resource_tags, \u0026quot;cost_centre\u0026quot;, \u0026quot;000\u0026quot;) would return 123 if present (and 000 if not) Example 2 - map of objects variable \u0026#34;virtual_machines\u0026#34; { type = map(object({ size = string os = string region = string })) description = \u0026#34;A map of virtual machines to create.\u0026#34; default = { web_server = { size = \u0026#34;small\u0026#34;, os = \u0026#34;ubuntu\u0026#34;, region = \u0026#34;uk south\u0026#34; } db_server = { size = \u0026#34;large\u0026#34;, os = \u0026#34;ubuntu\u0026#34;, region = \u0026#34;uk south\u0026#34; } dev_box = { size = \u0026#34;small\u0026#34;, os = \u0026#34;windows\u0026#34;, region = \u0026#34;uk west\u0026#34; } } } and it\u0026rsquo;s JSON representation:\n{ \u0026#34;web_server\u0026#34;: { \u0026#34;size\u0026#34;: \u0026#34;small\u0026#34;, \u0026#34;os\u0026#34;: \u0026#34;ubuntu\u0026#34;, \u0026#34;region\u0026#34;: \u0026#34;uk-south\u0026#34; }, \u0026#34;db_server\u0026#34;: { \u0026#34;size\u0026#34;: \u0026#34;large\u0026#34;, \u0026#34;os\u0026#34;: \u0026#34;ubuntu\u0026#34;, \u0026#34;region\u0026#34;: \u0026#34;uk-south\u0026#34; }, \u0026#34;dev_box\u0026#34;: { \u0026#34;size\u0026#34;: \u0026#34;small\u0026#34;, \u0026#34;os\u0026#34;: \u0026#34;windows\u0026#34;, \u0026#34;region\u0026#34;: \u0026#34;uk-west\u0026#34; } } Examples of accessing values for var.virtual_machines:\nvar.virtual_machines[\u0026quot;web_server\u0026quot;].size would return small lookup(var.virtual_machines, \u0026quot;dev_box\u0026quot;, \u0026quot;unknown\u0026quot;).os would return windows if present (and unknown if not) Sets # Sets might not be something you reach for all the time, but given their unique behaviour it\u0026rsquo;s great to know that some neat facts about sets are:\nThey are unordered and automatically remove duplicate values - [\u0026quot;bob\u0026quot;, \u0026quot;bob\u0026quot;, \u0026quot;bob\u0026quot;] will become just [\u0026quot;bob\u0026quot;] Set values cannot be accessed directly, you cannot use var.my_set[0] like you would with a list type Accessing a specific value requires iterating over it with a for_each, or using a tolist() type conversion function such as tolist(var.my_set)[0] - special quirk here, index 0 may not be what you think it is - check out the example to see why. Sets can be merged together using the setunion() function set types require all attribute values to be known at the plan stage so may not be suited for anything that would only resolve during an apply Here\u0026rsquo;s one basic example for a set variable:\nExample - set of strings variable \u0026#34;set_string_variable\u0026#34; { type = set(string) description = \u0026#34;A set of strings\u0026#34; default = [ \u0026#34;gamma\u0026#34; \u0026#34;alpha\u0026#34; \u0026#34;beta\u0026#34; ] } and it\u0026rsquo;s theoretical JSON representation:\n[ \u0026#34;alpha\u0026#34;, \u0026#34;beta\u0026#34;, \u0026#34;gamma\u0026#34; ] Examples of accessing values for var.set_string_variable:\ntolist(var.set_string_variable)[0] would return alpha Note Wait, why did you say theoretical JSON representation there and why would index 0 return alpha, shouldn\u0026rsquo;t it be the first item, gamma?\nRemember sets are unordered, and lists are ordered. When the set is converted to a list it is sorted lexicographically which for right now, means alphabetically. So the converted list order becomes [\u0026quot;alpha\u0026quot;, \u0026quot;beta\u0026quot;, \u0026quot;gamma\u0026quot;] and index 0 returns alpha.\nComplex structural # Objects # I love object. Some neat facts about objects are:\nThey allow you to declare a data structure! Objects can be merged together using the merge() function Object values can be accessed using: dot notation: var.my_object.attribute The autocomplete can look like this when declaring the variable in a *.tfvars file - pretty neat!\nCheck out these examples to see what basic object variables look like:\nExample 1 - object of strings variable \u0026#34;simple_object\u0026#34; { type = object({ name = string age = number height = number town_of_birth = string }) default = { age = 54 height = 172 name = \u0026#34;Bob\u0026#34; town_of_birth = \u0026#34;Bob Town\u0026#34; } } and it\u0026rsquo;s JSON representation:\n{ \u0026#34;age\u0026#34;: 54, \u0026#34;height\u0026#34;: 172, \u0026#34;name\u0026#34;: \u0026#34;Bob\u0026#34;, \u0026#34;town_of_birth\u0026#34;: \u0026#34;Bob Town\u0026#34; } Examples of accessing values for var.simple_object:\nvar.simple_object.name would return Bob var.simple_object.height would return 172 Example 2 - object with complex types Now this one is a little more interesting! We\u0026rsquo;re combining some of the previous types we\u0026rsquo;ve seen into a single variable.\nvariable \u0026#34;complex_object\u0026#34; { type = object({ name = string nicknames = list(string) details = object({ age = number height = number }) locations_visited = list(object({ date = string town = string })) }) default = { name = \u0026#34;Bob\u0026#34; nicknames = [\u0026#34;Bobby\u0026#34;, \u0026#34;Bobster\u0026#34;] details = { age = 54 height = 172 } locations_visited = [ { date = \u0026#34;2023-01-01\u0026#34; town = \u0026#34;Bob Town\u0026#34; }, { date = \u0026#34;2023-06-01\u0026#34; town = \u0026#34;Alice City\u0026#34; } ] } } and it\u0026rsquo;s JSON representation:\n{ \u0026#34;name\u0026#34;: \u0026#34;Bob\u0026#34;, \u0026#34;nicknames\u0026#34;: [\u0026#34;Bobby\u0026#34;, \u0026#34;Bobster\u0026#34;], \u0026#34;details\u0026#34;: { \u0026#34;age\u0026#34;: 54, \u0026#34;height\u0026#34;: 172 }, \u0026#34;locations_visited\u0026#34;: [ { \u0026#34;date\u0026#34;: \u0026#34;2023-01-01\u0026#34;, \u0026#34;town\u0026#34;: \u0026#34;Bob Town\u0026#34; }, { \u0026#34;date\u0026#34;: \u0026#34;2023-06-01\u0026#34;, \u0026#34;town\u0026#34;: \u0026#34;Alice City\u0026#34; } ] } Examples of accessing values for var.complex_object:\nvar.complex_object.name would return Bob var.complex_object.nicknames[1] would return Bobster var.complex_object.details.age would return 54 var.complex_object.location_visited[1].town would return Alice City Tuples # Alright! Finally we get to see what was going on with the tuple([string, string, string,]) up in the inferred types (implicit) section.\nTuples are a bit of a catch all; very flexible but it would be uncommon, dare I say not recommended, to explicitly declare variables as tuple types. The ones we\u0026rsquo;ve already reviewed would likely be better choices. Tuples mostly appear on intermediate variables, such as the ones declared in the locals {} blocks.\nThat said, some neat facts about tuples are:\nThey\u0026rsquo;re ordered, similar to lists They\u0026rsquo;re heterogeneous, i.e. they can have elements of different types; unlike lists and sets which are homogeneous where all elements must be the same type They must have the same number of schema and elements - e.g. a tuple([string]) type can be given a value of [\u0026quot;first\u0026quot;], but not [\u0026quot;first\u0026quot;, \u0026quot;second\u0026quot;] Anything declared in a locals {} block where the expression uses [] will be a tuple unless you precede it with a type conversion function such as tolist([]) Tuples can be joined together using the concat() function but results may vary depending on the complexity of the tuple Tuple values can be accessed using: index lookup syntax: var.my_tuple[\u0026lt;index number\u0026gt;] element() function lookup: element(var.my_tuple, \u0026lt;index number\u0026gt;) Check out this example to see what a basic tuple variable looks like:\nExample - mixed tuple variable \u0026#34;my_tuple\u0026#34; { type = tuple([string, number, bool]) description = \u0026#34;A tuple with a string, a number, and a boolean\u0026#34; default = [\u0026#34;example\u0026#34;, 42, true] } and it\u0026rsquo;s JSON representation:\n[ \u0026#34;example\u0026#34;, 42, true ] Examples of accessing values for var.my_tuple:\nvar.my_tuple[0] would return example element(var.my_tuple, 1) would return 42 and as a bonus, when we use the type() function available in the terraform console (more on that in another post), we can see it knows these are all different types:\n\u0026gt; type([\u0026#34;example\u0026#34;, 42, true]) tuple([ string, number, bool, ]) What\u0026rsquo;s all this about? # You made it! Hopefully you found some of that interesting, and you may be wondering why bother with all this theory stuff..\nFirstly, these things will come up a lot in Terraform (and other languages) and are generally considered essential foundational knowledge - you\u0026rsquo;ll save yourself a lot of time by becoming familiar with these concepts.\nSecondly, as we start to shift from creating one resources (singular) to N resources (multiple) we will be using the for expression and the for_each meta-argument, the latter of which can iterate (loop) over a map type.\nAs we learned about maps, they have keys! Whenever you use for_each, keys are going to be involved (whether you like it or not). We\u0026rsquo;ll explore this more in another post, but when you start referencing values from different resources you do so with the key, such as azurerm_resource_group.this[\u0026quot;rg_one\u0026quot;].id and azurerm_resource_group.this[\u0026quot;rg_two\u0026quot;].id which reference the Azure resource ids for two different resource groups deployed by the same resource block.\nNot only does understanding the keys help with looping and references, it also flows into how the resources can be organised in the state file - which is also something for another time.\nWrapping up \u0026amp; what\u0026rsquo;s next? # We\u0026rsquo;ve covered quite a lot in this post, nice work for sticking with it. We\u0026rsquo;re learned about primitive and complex data types and what the shape of them looks like.\nWe\u0026rsquo;ve learned that although list, set, and tuple types all use square brackets [] it doesn\u0026rsquo;t mean they\u0026rsquo;re the same thing, and each type has it\u0026rsquo;s own behaviours; the same goes for object and map with {}.\nWe\u0026rsquo;re gone over explicit/strong typing and how we can declare types on Terraform variable blocks, or use the type conversion functions within locals blocks to be more explicit. We touched on the B-side of this, being implicit/inferred typing where Terraform will make some assumptions on values created in locals, and also when it performs automatic type conversion behind the scenes.\nWe\u0026rsquo;ve learned how to refer to specific values/keys/indexes using syntax or the element() and lookup() functions, along with joining types together with the merge(), concat(), and setunion() Terraform language functions.\nIn data types part 2 we\u0026rsquo;ll dig a little deeper into type constraints and conversions. Until next time, stay curious.\n","date":"8 March 2026","externalUrl":null,"permalink":"/posts/terraform-azurerm-data-types-1/","section":"Posts","summary":"","title":"Shining a spotlight on Terraform data types - Part 1","type":"posts"},{"content":"","date":"8 March 2026","externalUrl":null,"permalink":"/tags/terraform/","section":"Tags","summary":"","title":"Terraform","type":"tags"},{"content":"","date":"8 March 2026","externalUrl":null,"permalink":"/series/terraform-and-azure/","section":"Series","summary":"","title":"Terraform and Azure","type":"series"},{"content":"When I first started working with Terraform I was overwhelmed by the myriad possible configurations. Terraform has been around for over 10 years which is plenty of time for configuration standards and practices to shift which can confuse newcomers looking to separate the wheat from the chaff.\nIn this post we\u0026rsquo;re going to step through the components of a very simple deployment to Azure from your local device. Make sure you\u0026rsquo;ve run through my setup guide - Preparing to work with Terraform and Azure for the first time first to get everything ready to rock.\nWhat does the deployment process look like? # This is a standard Terraform deployment flow. The first order of business is always going to be authenticating to the target cloud provider (Azure), and then executing several Terraform commands (which we\u0026rsquo;ll step through in more detail later).\nflowchart TB\rA[\"az login\"] --\u003e B[\"terraform init\"]\rB --\u003e C[\"terraform plan\"]\rC --\u003e D[\"terraform apply\"]\rD --\u003e E[\"Infrastructure created\"]\rclassDef azure fill:#085FAD,stroke:#003B70,color:#ffffff;\rclassDef terraform fill:#623CE4,stroke:#3A1E9E,color:#ffffff;\rclassDef deploy fill:#05A000,stroke:#3A1E9E,color:#ffffff,font-size:18px;\rclass A azure;\rclass B,C,D terraform;\rclass E deploy;\rOpinions are like.. # Everyone is going to have an opinion on how to do Terraform; over time you\u0026rsquo;ll gain enough experience to figure out your own path. Don\u0026rsquo;t worry about things being perfect (whatever that means), just get stuck in and practise.\nThat said, the HashiCorp style guide is worth reading - there\u0026rsquo;s a lot in there so pace yourself.\nMaking your first Terraform files # Info When Terraform executes it will process all of the Terraform files within the working directory in one go. Individual *.tf files cannot be processed selectively.\nWe\u0026rsquo;re going to step through creating the following five files within the working directory you set up in part one of this series.\nNote The contents of the following files could be merged into a single file and it wouldn\u0026rsquo;t change how it functions - splitting the concerns into different files is just a management/style choice.\nterraform.tf # Don\u0026rsquo;t worry about the contents of this one for now; we\u0026rsquo;ll cover this another time.\nproviders.tf # Terraform has thousands of providers which are simply integrations with particular systems or software. For our use case, we\u0026rsquo;re targeting Azure, so we need to declare the azurerm provider.\nDon\u0026rsquo;t worry about this file too much for now; we\u0026rsquo;ll review it another time.\nmain.tf # main.tf is usually the \u0026rsquo;entry point\u0026rsquo; which contains Terraform resource blocks that declare what resources to create in the targeted Azure subscription. In this example we declare that a resource group should be created.\nTo quickly break down a resource block:\nItem Meaning resource Starts to declare a Terraform resource block \u0026quot;azurerm_resource_group\u0026quot; The resource type to deploy (azurerm provider resources) \u0026quot;this\u0026quot; Resource name of your choosing, used when referencing its properties { ... } Everything inside are the attributes (arguments, properties, etc.) variables.tf # Variables allow you to run the same Terraform code multiple times and pass in different values for each deployment. For example, you might deploy to multiple regions: location could be uksouth in one deployment and ukwest in another.\nTo quickly break down a variable block:\nItem Meaning variable Starts to declare a Terraform variable block \u0026quot;resource_group_name\u0026quot; The variable name of your choosing description A useful explanation of the variable to assist others (and future you) to understand its purpose type The data type of the variable such as string (\u0026quot;some string\u0026quot;), number (23), bool (true/false) locals.tf # Locals is a special block when you can create and assign new values that aren\u0026rsquo;t declared using a Terraform variable block. Where variables have to be declared before deployment can begin, locals can be set during the deployment which makes them super useful.\nA quick example might be creating a computed resource name based on multiple variable inputs:\nlocals { \u0026#34;resource_name\u0026#34; = \u0026#34;${var.prefix}-${var.resource_type}-${var.suffix}\u0026#34; } which you can then reference elsewhere in the Terraform code using local.resource_name. We\u0026rsquo;ll go into this in more detail later, but if you ever need to make some on the fly values you can do so in this wonderful block.\noutputs.tf # Outputs capture what information should be returned after a deployment. In this example we output the name and resource id (which are fairly common things to output).\nTo quickly break down an output block:\nItem Meaning output Starts to declare a Terraform output block \u0026quot;resource_group_resource_id\u0026quot; Output name of your choosing value = Every output block must have this; this is where the value of the output is defined Make rocket go! # Important Make sure the account you are authenticating to Azure with has the Contributor or Owner role on the target subscription. You can view role assignments within the Azure Portal under the -\u0026gt; Access control (IAM) -\u0026gt; Role assignments.\nOpen up your shell of choice and change to the root of your working directory.\nFirst, log into Azure with the az login command and follow the prompts to select your target subscription. az login has changed login behaviour several times over the years, so I\u0026rsquo;ll avoid any specific advice.\nOnce logged in, you can verify which subscription you are connected to using az account show and look at the value of id (copy this somewhere, as we\u0026rsquo;ll need it later)\nList subscriptions with az account list. Change subscriptions with az account set --subscription \u0026lt;subscription id\u0026gt;.\nterraform init # The first of the three Terraform commands you will run is terraform init, which prepares the workspace so we can proceed to the plan and apply commands. This command does not deploy resources, and it is safe to run multiple times. Run terraform init now if you haven\u0026rsquo;t already done so, and make sure you are in your working directory where the *.tf files are.\nAdditional information can always be found in the HashiCorp docs.\nA successful terraform init should output something like this: And some new content, a .terraform directory and .terraform.lock.hcl file should appear in your working directory.\nterraform plan # A terraform plan operation attempts to inform the operator what will change if this Terraform code is deployed. The plan operation is not always perfect and has its quirks which you\u0026rsquo;ll discover over time, but for this deployment it should be accurate.\nThe plan will read the current state of remote resources (already in Azure), compare the current configuration (the Terraform in your working directory) to the state file, and tell you what it thinks will happen when you run a terraform apply.\nWe\u0026rsquo;ll explore the state file in another post. For now, think of it as a representation of your deployed resources which Terraform uses it as its \u0026lsquo;source of truth\u0026rsquo;.\nRun terraform plan in your shell now - you\u0026rsquo;ll be prompted to enter values manually for the variables we declared because they do not have default values; use the following values:\nresource_group_name -\u0026gt; rg-example-uks location -\u0026gt; uksouth subscription_id -\u0026gt; paste your Azure subscription id here (can be obtained by running az account show and copy the id property) A successful plan operation should look something like this:\nterraform apply # It\u0026rsquo;s time for terraform apply. Run this in your shell and provide the same values as before for resource_group_name, location, and subscription_id. Make sure you have your Azure Portal open to check the resources.\nYou\u0026rsquo;ll see a new plan displayed and also be prompted to confirm the deployment by typing yes. This approval step can be removed by adding the --auto-approve flag to the terraform apply command.\nA successful apply operation should look something like this:\nNote Although subscription ids are not secrets, they do expose information about your tenant and it\u0026rsquo;s generally a good practice to avoid putting them in public places.\nNote Because this is a local deployment, you should see some new files in your working directory called terraform.tfstate and .terraform.tfstate.lock.info. Ignore these for now. Generally, it is not a good idea to modify these files manually; there are specific commands and processes to use if you need to alter state files (which we\u0026rsquo;ll cover another time).\nWrap up # And we\u0026rsquo;re done! You\u0026rsquo;ve just completed your first Terraform deployment to Azure. We\u0026rsquo;ve gone through the essential steps of deploying infrastructure using Terraform, starting with authentication via az login and identifying the three critical commands that make up every Terraform deployment: terraform init, terraform plan, and terraform apply.\nKey concepts to remember: Terraform processes all .tf files in your working directory simultaneously, variables enable reusable configurations across different deployments, and the state file (terraform.tfstate) tracks your deployed resources. Now that you have a working deployment, try creating a few additional resources such as a storage account or virtual network! Don\u0026rsquo;t forget to check the resource pricing if you\u0026rsquo;re unsure what kind of cost is associated with it.\nI hope this has been useful, in future posts we\u0026rsquo;ll explore more aspects of Terraform as I understand them. Until next time, stay curious.\n","date":"21 February 2026","externalUrl":null,"permalink":"/posts/terraform-azurerm-lesson-01/","section":"Posts","summary":"","title":"Stepping through a simple Terraform deployment to Azure","type":"posts"},{"content":"This is exciting; for one reason or another it seems you feel the need to start interacting with Azure using HashiCorp\u0026rsquo;s Infrastructure-as-Code tool, Terraform. Happy days indeed!\nBefore we start banging out resources though lets go through setting up your local environment - here\u0026rsquo;s what you\u0026rsquo;ll need:\ngraph TD\rAS[Access to an Azure subscription] --\u003e D(Start writing Terraform!)\rAZ[Install Azure CLI] --\u003e D\rT[Install Terraform] --\u003e D\rC[Create a new working directory] --\u003e D\rV[Install VS Code + extensions] --\u003e D\rclassDef azure fill:#085FAD,stroke:#003B70,color:#ffffff;\rclassDef terraform fill:#623CE4,stroke:#3A1E9E,color:#ffffff;\rclassDef vscode fill:#559185,stroke:#003B70,color:#ffffff;\rclassDef neutral fill:#D3D0A7,stroke:#333,color:#111111;\rclassDef deploy fill:#05A000,stroke:#3A1E9E,color:#ffffff,font-size:18px;\rclass AS,AZ azure;\rclass T terraform;\rclass V vscode;\rclass C neutral;\rclass D deploy;\rIt might look like a lot but consider it an investment in your Terraform + Azure journey - we\u0026rsquo;ll step through how to get started on each of these things below.\nAccess to an Azure subscription # Info An Azure subscription is a logical bucket which can contain cloud resources. Terraform deployments target a subscription, so if you really want to get your hands dirty with Terraform you have to sign up for an Azure account/tenant which should have a subscription ready to go.\nIf you don\u0026rsquo;t have an Azure account sign up via this link or just Google for \u0026lsquo;Getting started with Azure\u0026rsquo;. It is a bit of a process so grab your credit/debit card along with a \u0026#x2615; (or beverage of choice) and get cracking. I\u0026rsquo;m not going to spend any time on this process as the onboarding journey should be pretty straight-forward.\nThink about your tenant and what you might want from it over time - is this just a playground for basic things or do you want to build it out over time? It might be tempting to call it something cavalier like MonkeyButtTest, though consider one day you might want to host some longer-lived resources, attach a domain, or show a potential employer what you\u0026rsquo;ve designed \u0026amp; built - so name it accordingly.\nWarning Cloud resources cost money - you are responsible for managing costs and budgets, along with ensuring your account details are secure to avoid a malicious actor compromising your tenant and spending all your money.\nIf you\u0026rsquo;ve managed to onboard successfully, you should be able to:\nLog into the Azure Portal Search for and navigate to Subscriptions via the portal search bar in the centre-top of the page Observe one subscription that\u0026rsquo;s ready to rock Install Azure CLI # Info Azure CLI is a command line tool that can interact with Azure and Entra using az commands, such as this one to create a resource group:\naz group create --location uksouth --resource-group \u0026quot;rg-test-resource-group\u0026quot;.\nFor simple local development Terraform + Azure CLI work well together, we\u0026rsquo;ll talk more about this in subsequent lessons. Check out the install guide for Azure CLI, it\u0026rsquo;s extensive and generally should be fairly straight forward to install.\nYou\u0026rsquo;ll know installation has been successful when you can run az version in your shell of choice and it returns something like:\n{ \u0026#34;azure-cli\u0026#34;: \u0026#34;2.XX.0\u0026#34;, \u0026#34;azure-cli-core\u0026#34;: \u0026#34;2.XX.0\u0026#34;, \u0026#34;azure-cli-telemetry\u0026#34;: \u0026#34;1.1.0\u0026#34;, \u0026#34;extensions\u0026#34;: {} } As a fun side note if you\u0026rsquo;re wondering why there\u0026rsquo;s lots of ways to interact with Azure, it\u0026rsquo;s because there is! Ultimately, everything will go through the Azure Resource Manager so you can select the best tool for the job knowing that despite the quirks of each tool, it all goes through the same gate.\ngraph TD\rAP[Azure portal] --\u003e ARM[Azure Resource Manager]\rAPS[Azure PowerShell] --\u003e SDK[SDKs]\rSDK --\u003e ARM\rACL[Azure CLI] --\u003e SDK\rRC[REST clients] --\u003e ARM\rARM --\u003e R[Azure resources]\rclassDef azure fill:#085FAD,stroke:#003B70,color:#ffffff;\rclassDef sdk fill:#623CE4,stroke:#3A1E9E,color:#ffffff;\rclassDef arm fill:#05A000,stroke:#3A1E9E,color:#ffffff;\rclass AP,APS,ACL,RC azure;\rclass SDK sdk;\rclass ARM arm;\rInstall Terraform # Terraform can be installed numerous ways and the end goal is to be able to run terraform version in your shell of choice and have it return something like:\nTerraform v1.XX.Y on linux_amd64 Terraform is just a single binary/executable file so if you\u0026rsquo;re getting super stuck, try downloading and extracting it into your working directory that we\u0026rsquo;ll set up next. Ideally though it should be in a system path location so that you can run terraform from any location and it will work.\nInfo If terraform (or any other executable) works but you can\u0026rsquo;t remember where it installed to, you can run Get-Command terraform | fl on Windows Terminal (PowerShell) or which terraform on Linux (and I think MacOS too) terminal - this should give you the full path to the binary.\nCreate a new working directory # All you need here is a blank directory on your local file system where we can make the magic happen. This directory will contain our terraform files (*.tf) and become our VS Code workspace - more on that later.\nIf you\u0026rsquo;re familiar with a source control you could set this up but it\u0026rsquo;s not essential at this stage.\nInstall VS Code + extensions # Download VS Code for your target operating system. If you\u0026rsquo;re on Windows, consider the User Installer over the System Installer as this doesn\u0026rsquo;t require local administrator access. It also updates frequently so even if you have local admin access via a separate account, you\u0026rsquo;ll be banging that password in every few days.\nThe following extensions are incredibly helpful, I highly recommend installing them. Open up VS Code and go to the Extensions tab on the left-hand side, or open the links below and click Install.\nHashiCorp Terraform - provides auto-complete/intellisense for HashiCorp Configuration Language (HCL) Microsoft Terraform - adds additional auto-complete syntax specifically for Azure resources Obviously AI is all the rage so if you have a Claude/Copilot/* account you can add those extensions too.\nWrapping up \u0026amp; what\u0026rsquo;s next? # The last step is opening your working directory within VS Code by going to File -\u0026gt; Open Folder... and navigating/selecting the directory. It may prompt you about trusting the directory, choosing yes should be fine if it\u0026rsquo;s your local filesystem.\nThis is now your workspace and we\u0026rsquo;re ready to rock! I haven\u0026rsquo;t written the subsequent lessons yet but I\u0026rsquo;ll add links here once that\u0026rsquo;s done and we\u0026rsquo;ll start writing some Terraform with gusto!\nAs a final fun sendoff, if you thought some of this setup was a bit time consuming and you don\u0026rsquo;t want to do it again when you change devices, you\u0026rsquo;re on to something! There\u0026rsquo;s plenty of ways to streamline ones local development setup, but one option worth checking out is Development Containers (aka Dev Containers) which can ensure you always have access to your desired development environment anywhere you can run a container (which should be almost any machine these days).\nVS Code supports developing inside containers, be sure to check it out at some point.\n","date":"14 February 2026","externalUrl":null,"permalink":"/posts/terraform-azure-setup/","section":"Posts","summary":"","title":"Preparing to work with Terraform and Azure for the first time","type":"posts"},{"content":"","date":"1 February 2026","externalUrl":null,"permalink":"/tags/200/","section":"Tags","summary":"","title":"200","type":"tags"},{"content":"A few years ago I was working on migrating a workload from one Azure subscription to another. Our migration strategy in this instance was to deploy the infrastructure \u0026amp; code to the new subscription with the requirement it would be dormant until we were ready to move forward.\nDormant means we didn\u0026rsquo;t want it accepting traffic or processing anything until we were ready. Given the workload connected to several other services (dependencies), such as an Azure SQL database and an event bus, I configured some dummy connection strings so the application wouldn\u0026rsquo;t be able to connect to anything.\nThe Unfolding of Events # It\u0026rsquo;s Friday afternoon and we\u0026rsquo;re feeling good; time to roll out that new infrastructure which shouldn\u0026rsquo;t change anything, congratulate ourselves on a job well done, and ease ourselves into the weekend.\nMonday morning comes around and we get wind of a production defect in a completely separate system to the one we\u0026rsquo;re migrating - dead letters in our event bus queues due to huh, that\u0026rsquo;s weird, failed SQL logins (Login failed for user '\u0026lt;token-identified principal\u0026gt;'). We started following breadcrumbs and raised a support ticket on the side, it never hurts to make Microsoft earn their support money \u0026#x1f609;.\nMicrosoft come back to us fairly quickly and said the error was caused by too many failed login attempts which had triggered something called DoSGuard which blocked the originating IP address.\nWe ran the below query(sourced from Microsoft docs1) on the SQL server\u0026rsquo;s master database and it showed a dramatic increase in failed logins from around Friday afternoon when we deployed the supposedly dormant infrastructure.\nInfo SELECT start_time, end_time, event_category, event_type, event_subtype, event_subtype_desc, severity, event_count, description FROM sys.event_log WHERE event_type = \u0026#39;connection_failed\u0026#39; AND event_subtype = 4 AND start_time \u0026gt;= \u0026#39;2026-01-30 00:00:00\u0026#39; AND end_time \u0026lt;= \u0026#39;2026-02-02 00:00:00\u0026#39;;At this point I\u0026rsquo;m still thinking \u0026lsquo;correlation does not imply causation\u0026rsquo; and I was stuck on:\nWhy did the login failures match up with the infrastructure deployment when I was sure I had stripped out the connection strings? Even if it was related, why would it be affecting a different system? The first question was an easy one, eventually I realised I\u0026rsquo;d simply missed a connection string. One of the components had the SQL connection string configured as an Azure Bicep parameter which was a deviation from the other components.\nThis oversight meant the component had been reaching out since Friday afternoon, knocking on the SQL door trying to get in before any access had been granted to the managed identity of the web app.\nFor the second question, the dots finally started connecting when Microsoft provided the IP address that was being blocked and we quickly understood the problem - the workload was running on a compute/networking resource called an App Service Environment (ASE) which hosted multiple workloads.\nIf we have a quick look at the networking for an ASE we can see that the application workloads all use a single outbound IP address:\nImage credit to Microsoft So now finally, all the pieces have fallen into place:\nA workload had been deployed to a shared compute/networking resource That workload was not expected to \u0026lsquo;do\u0026rsquo; anything, but one component was still configured with a valid SQL connection string The component bombarded our SQL server with login attempts, triggering DoSGuard against the originating IP which was shared by multiple workloads Other systems attempting to access SQL were being denied which affected their operation and led to the opening of the aforementioned dead-letter incident Mitigation with a serving of humble pie # The immediate mitigation was to turn off the web app which was harassing the SQL server, removing the source of the failed logins and allowing other systems to get on with their business.\nJust to be sure, we re-deployed the application with a fake connection string until we were ready to pick up the migration.\nHaving learned about the built-in security feature first-hand we wrote a small memo for the wider engineering team to consider in case someone else hit the same issue.\nAlthough making mistakes is a part of life it can be really hard to not beat yourself up when you\u0026rsquo;re the one that overlooked something. I\u0026rsquo;ve often struggled with this but over the years I\u0026rsquo;m slowly re-framing the concept of failure and shifting it towards being a lesson learned and a stepping stone to future successes.\nThe migration was completed later in the week, and there was much rejoicing.\nAdditional reading # DoSGuard # Although Microsoft didn\u0026rsquo;t provide exact figures for the DoSGuard trigger (just \u0026rsquo;too many in a given period\u0026rsquo;), they did say that once triggered the block time was 5 minutes.\nhttps://techcommunity.microsoft.com/blog/azuredbsupport/connections-rejected-by-dosguard-error-18456-state-113/3775487 https://learn.microsoft.com/en-us/azure/security/fundamentals/infrastructure-sql#dosguard App Service Environment # https://learn.microsoft.com/en-us/azure/app-service/environment/networking#addresses https://learn.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-event-log-azure-sql-database?view=azuresqldb-current#query-login-failures-for-users\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"1 February 2026","externalUrl":null,"permalink":"/posts/azure-sql-dosguard/","section":"Posts","summary":"","title":"DoSGuard - The Silent Protector","type":"posts"},{"content":"","date":"1 February 2026","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"","date":"1 February 2026","externalUrl":null,"permalink":"/tags/sql/","section":"Tags","summary":"","title":"Sql","type":"tags"},{"content":" Me # Initially from Brisbane, Australia I was fortunate enough to immigrate to the UK in 2019 to work at a London FinTech. Most of my career prior to this was primarily on-premises style System Administration work where I had to rack and cable actual physical server and storage hardware, if you can believe it.\nI really enjoy working at a cloud-first organisation and I\u0026rsquo;ve been fortunate with opportunities to apply myself to a wide range of problems. I\u0026rsquo;ve run vendor selections and project implementations, designed and implemented cloud architectures, written and deployed infrastructure code \u0026amp; CI/CD pipelines, created bespoke tooling, and last but not least met some really great people along the way.\nEarlier life # I\u0026rsquo;ve always been a curious cat and I spent my youth poking around my dad\u0026rsquo;s garage (he was a telecommunications installer) which had all manner of tools, motors, wires, gadgets etc and it wasn\u0026rsquo;t long until that curiosity compelled me to start disassembling (and occasionally re-assembling) toys and other junk in the vicinity.\nTime passed and we gained our first computer (a 386 with 4MB of RAM and a ~200MB hard-drive!) which didn\u0026rsquo;t escape my meddling; I learned so much with this first system, and over time became proficient with building/maintaining general PC systems.\nMy first technology role was at a small games company where I was initially the general maintenance/handy-person for anything even remotely adjacent to IT such as assembling furniture, putting up whiteboards, running network cabling, crawling around in rooves and all that fun stuff. Over time the let me loose on enterprise server hardware and software and my journey to the dark side (becoming a sys admin) was complete.\nOther interests # Games # I really enjoy playing PC (mostly) and console (occasionally) games. Some of my favourites have been:\nDestiny 2 Factorio \u0026amp; Satisfactory Helldivers 2, Arc Raiders Hades Frostpunk Subnautica XCOM 2, Phoenix Point Books # The family book stash had some classics such as Dune and Lord of the Rings - reading these at an early age laid the foundation for my love of science fiction \u0026amp; fantasy. Some of my all time favourites are:\nHyperion Cantos (Dan Simmons) Children of Time (Adrian Tchaikovsky) The Broken Earth (N.K. Jemisin) The Vagrant Trilogy (Peter Newman) The Kingkiller Chronicle (Patrick Rothfuss) Furies of Calderon (Jim Butcher) The Locked Tomb (Tamsyn Muir) The Expanse (James S. A. Corey) The Iron Druid Chronicles (Kevin Hearne) Peter F Hamilton in general I was ~26 when I read the first Stormlight Archive book (The Way of Kings) by Brandon Sanderson and I promised myself I wouldn\u0026rsquo;t read any more until the whole series was published; I made this mistake with Wheel of Time and kept falling out of sync with the characters after long periods between releases. I thought it would make a great 40th birthday present but we\u0026rsquo;re only up to book 5/10 so it seems I was a bit optimistic; perhaps an early retirement present instead.\nFood # My diet is a bit of a double-whammy, plant-based and gluten-free, but I very much enjoy making and eating food. The eating part was revealed to me whilst on holidays in my mid-thirties as I was literally running to a cafe I\u0026rsquo;d found which had an 11 o\u0026rsquo;clock cut-off for ordering pancakes. Unfortunately I arrived too late to order pancakes, but just on time to gain valuable insight into one of my core motivators.\nCreating a blog will be a good excuse to write down some of my favourite plant-based recipes, restaurants, and grocery items.\nDoes plant-based mean vegan? # I don\u0026rsquo;t think so, the vegan definition includes some non-diet related lifestyle choices. Although I\u0026rsquo;m pretty close to it, I don\u0026rsquo;t consider existing as a human vegan so feel comfortable sticking with the plant-based tag.\nDoes Standard Issue Engineer mean anything in particular? # It does! It\u0026rsquo;s a little extension of Standard Issue Cat (SIC). If you\u0026rsquo;ve not heard the term Standard Issue Cat before, do yourself a favour and check out this great guide on Reddit. Although arguably not as fancy as some models, SICs (as is common with mongrel breeds) are robust and excellent specimens, not to be disregarded. In this fast paced world full of pressures and labels maybe it\u0026rsquo;s ok to just be a Standard Issue Engineer that does their best.\nAI policy # In this day and age starting a blog feels pretty silly. I started doing it because I enjoy the process of stepping through a subject, validating or renewing my understanding of how things fit together, and attempting to convey my \u0026lsquo;world model\u0026rsquo; about how this thing works to someone else. Whether or not it\u0026rsquo;s any good who knows, but each post has brought updated insights which I really enjoy; and it\u0026rsquo;s something I can work through at my own pace.\nAll of this is a roundabout way to say I don\u0026rsquo;t use AI to write my posts - there would be no struggle, no delving, no simmering or musing, no spark when a little discovery extends my web of understanding - how boring!\nThat said, as much as I enjoy the process despite only speaking/writing english I seem to be terrible at it so I do run a prompt over the post at the end which is doing a spelling/grammar sweep - it seems I consistently misunderstand when to use its vs it\u0026rsquo;s so that keeps me pretty humble.\nAnyway, good luck to you traveller - I hope something on my blog was interesting to you.\n","externalUrl":null,"permalink":"/about/","section":"","summary":"","title":"About Me","type":"page"}]