<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: newbmiao</title>
    <description>The latest articles on DEV Community by newbmiao (@newbmiao).</description>
    <link>https://dev.to/newbmiao</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1800049%2F1bef1c24-9b59-41b8-972e-01e31f78f990.jpg</url>
      <title>DEV Community: newbmiao</title>
      <link>https://dev.to/newbmiao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/newbmiao"/>
    <language>en</language>
    <item>
      <title>1.1 Where the story begins: the shared account</title>
      <dc:creator>newbmiao</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:31:32 +0000</pubDate>
      <link>https://dev.to/newbmiao/11-where-the-story-begins-the-shared-account-2hll</link>
      <guid>https://dev.to/newbmiao/11-where-the-story-begins-the-shared-account-2hll</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;On Tuesday afternoon, the dev application of the &lt;strong&gt;relay&lt;/strong&gt; business team went down.&lt;/p&gt;

&lt;p&gt;The app was timing out on its database connection. A round of container restarts changed nothing. Someone checked the database: it was running, and no firewall had changed. The private path was broken — the database's &lt;strong&gt;Private Endpoint&lt;/strong&gt; (a private network endpoint) had been modified the night before, and &lt;strong&gt;private DNS&lt;/strong&gt; no longer matched the current &lt;strong&gt;PE&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;Dee from the platform team opened the &lt;strong&gt;Activity Log&lt;/strong&gt; (the operations audit log).&lt;/p&gt;

&lt;p&gt;There was exactly one operator: &lt;code&gt;shared-deploy@example.com&lt;/code&gt;. No individual &lt;strong&gt;UPN&lt;/strong&gt; (user login name) — just the shared account's mailbox.&lt;/p&gt;




&lt;p&gt;Later, a relay developer owned up to it: the team had been doing &lt;strong&gt;PE migration config&lt;/strong&gt; the night before, using that account; at the time it seemed it &lt;strong&gt;wouldn't affect the running dev environment&lt;/strong&gt;, so they wrapped up and left. The impact arrived anyway, and the log still said only &lt;code&gt;shared-deploy@example.com&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;During the postmortem, an older problem resurfaced.&lt;/p&gt;




&lt;p&gt;Back when headcount was small and the platform team was swamped, a packed schedule meant the business teams dreaded every trip to the "grant permission" door.&lt;/p&gt;




&lt;p&gt;So everyone asked for an emergency &lt;strong&gt;shared account&lt;/strong&gt;, and the platform handed out a big one: &lt;strong&gt;Contributor&lt;/strong&gt; (a built-in role) on the &lt;strong&gt;Subscription&lt;/strong&gt;. For resources in both the &lt;strong&gt;dev&lt;/strong&gt; and &lt;strong&gt;prod&lt;/strong&gt; subscriptions, this one key could reach just about everything — swap &lt;strong&gt;PE&lt;/strong&gt;s, touch the network, delete things, create things. Most of it landed on that single mailbox.&lt;/p&gt;




&lt;p&gt;People didn't run releases with it — the &lt;strong&gt;pipeline&lt;/strong&gt; used a &lt;strong&gt;service connection&lt;/strong&gt; — but the thing at the other end of the wire was still subscription-level &lt;strong&gt;Contributor&lt;/strong&gt;, just as wide.&lt;/p&gt;




&lt;p&gt;With three to five people, a shout in the group chat kept everyone aware of who touched what; once &lt;strong&gt;pulse&lt;/strong&gt; and &lt;strong&gt;harbor&lt;/strong&gt; joined, the unspoken mutual awareness stopped scaling. Nobody could keep track of who touched whose piece.&lt;/p&gt;




&lt;p&gt;The log couldn't say who, yet the account could move a very large surface — dev and prod simmering in one pot.&lt;/p&gt;

&lt;p&gt;Dee, still staring at the log, dropped the line: "We can't let &lt;code&gt;shared-deploy@example.com&lt;/code&gt; keep signing for everyone forever."&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison: why one account can't carry accountability
&lt;/h2&gt;

&lt;p&gt;Back then, to keep developers unblocked by platform scheduling, the platform had given &lt;code&gt;shared-deploy@example.com&lt;/code&gt; a key covering the whole Subscription. It could swap PEs, delete storage, and operate across dev and prod alike; when something broke, the Activity Log could only point at the key — it could not answer "which member of which team made this change."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"azuread_service_principal"&lt;/span&gt; &lt;span class="s2"&gt;"shared_deploy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;display_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"shared-deploy"&lt;/span&gt; &lt;span class="c1"&gt;# corresponds to shared-deploy@example.com&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subscription"&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;subscription_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dev_subscription_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_role_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"shared_deploy_dev_subscription"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;azurerm_subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="n"&gt;role_definition_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Contributor"&lt;/span&gt;
  &lt;span class="n"&gt;principal_id&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;azuread_service_principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared_deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;scope&lt;/strong&gt; sat on the entire &lt;strong&gt;Subscription&lt;/strong&gt;; one &lt;strong&gt;principal&lt;/strong&gt; covered several teams and both &lt;strong&gt;dev&lt;/strong&gt; and &lt;strong&gt;prod&lt;/strong&gt; — the log can only ever point at "that one key." There's the root of it.&lt;/p&gt;




&lt;p&gt;The fix is not another shared account. Resources are first grouped into each team's &lt;strong&gt;RG&lt;/strong&gt; by team and environment, and permissions follow that edge; people join their own &lt;strong&gt;SGRP&lt;/strong&gt;s and only touch what lives inside their own &lt;strong&gt;Resource Group&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: let each team own its resources
&lt;/h2&gt;

&lt;p&gt;The platform moved relay's ACA, database, and storage into &lt;strong&gt;relay&lt;/strong&gt;'s &lt;strong&gt;RG&lt;/strong&gt;, and pulse's resources into &lt;strong&gt;pulse&lt;/strong&gt;'s &lt;strong&gt;RG&lt;/strong&gt;. The permissions developers need narrow down by resource type and environment; when no built-in role fits a capability, grant just that single resource. Which team owns which slice of resources now stands on Azure's own resource boundary.&lt;/p&gt;

&lt;p&gt;Permissions are also granted &lt;strong&gt;per team&lt;/strong&gt;: the platform binds &lt;strong&gt;RBAC&lt;/strong&gt; to the team &lt;strong&gt;SGRP&lt;/strong&gt; (a security group in Entra), not to individuals. People follow the group. Joins and departures only touch the group roster; each team manages its own &lt;strong&gt;RG&lt;/strong&gt;, and nobody chases per-person changes anymore. Only then does the &lt;strong&gt;Activity Log&lt;/strong&gt; get a chance to land on a specific person, a specific resource, and a specific team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dev&lt;/strong&gt; and &lt;strong&gt;prod&lt;/strong&gt; each get one &lt;code&gt;dev_resource_groups&lt;/code&gt; / &lt;code&gt;prod_resource_groups&lt;/code&gt; table, nested under &lt;code&gt;teams.&amp;lt;team&amp;gt;&lt;/code&gt;. Filled out for &lt;strong&gt;relay&lt;/strong&gt;, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="n"&gt;teams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;relay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"owen.relay@example.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# mirrors SGRP-RELAY membership; join multiple SGRPs if on multiple teams&lt;/span&gt;
    &lt;span class="n"&gt;dev_resource_groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;relay_dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;subscription_key&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt;
        &lt;span class="n"&gt;resource_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RG-RELAY-DEV-SEA"&lt;/span&gt;
        &lt;span class="n"&gt;roles&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Container Apps Contributor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Key Vault Secrets Officer"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;prod_resource_groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;relay_prd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;subscription_key&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
        &lt;span class="n"&gt;resource_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RG-RELAY-PRD-SEA"&lt;/span&gt;
        &lt;span class="n"&gt;roles&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Container Apps Operator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Storage Blob Data Reader"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared &lt;strong&gt;ACR&lt;/strong&gt; stays out of &lt;code&gt;teams&lt;/code&gt;, in its own &lt;code&gt;shared_resource_access&lt;/code&gt; table — one ACR, each granted team listed separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="n"&gt;shared_resource_access&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;org_acr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;subscription_key&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
    &lt;span class="n"&gt;resource_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RG-EG-HUB-INF"&lt;/span&gt;
    &lt;span class="n"&gt;resource_name&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"acreg"&lt;/span&gt;
    &lt;span class="n"&gt;resource_type&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Microsoft.ContainerRegistry/registries"&lt;/span&gt;
    &lt;span class="n"&gt;team_grants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;relay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Reader"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;pulse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Reader"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row in the tables expands into one role assignment, leaving only the team's identity at the resource boundary — the shared account no longer sits there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_role_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"relay_sgrp_dev_aca"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relay_dev_sea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
  &lt;span class="n"&gt;role_definition_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Container Apps Contributor"&lt;/span&gt;
  &lt;span class="n"&gt;principal_id&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;azuread_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sgrp_relay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Preparation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Who does the prep&lt;/strong&gt;: for each team, assemble a "people + SGRP + RG + resources + rough permissions" sheet. It's good enough if it survives a walkthrough in a meeting or a doc.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inventory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;List per team: &lt;strong&gt;who&lt;/strong&gt; (member UPNs), &lt;strong&gt;which resources&lt;/strong&gt; they occupy (ACA, PG, KV…), and which &lt;strong&gt;RG&lt;/strong&gt; / Subscription they land in; mark mixed or misassigned ones &lt;strong&gt;to-migrate&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team SGRP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Create&lt;/strong&gt; one Entra &lt;strong&gt;SGRP&lt;/strong&gt; per team (e.g. &lt;code&gt;SGRP-RELAY&lt;/code&gt;) — one group per team; &lt;code&gt;azuread_group&lt;/code&gt; via Terraform also works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team members&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bind member UPNs&lt;/strong&gt; into the matching SGRP (going forward, &lt;code&gt;teams.&amp;lt;team&amp;gt;.members&lt;/code&gt; mirrors this); people on multiple teams join multiple SGRPs — don't merge into one big group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team RGs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fix one team RG name per &lt;strong&gt;dev / prod&lt;/strong&gt;; &lt;strong&gt;create if missing&lt;/strong&gt; (e.g. &lt;code&gt;RG-&amp;lt;TEAM&amp;gt;-DEV-SEA&lt;/code&gt; / &lt;code&gt;RG-&amp;lt;TEAM&amp;gt;-PRD-SEA&lt;/code&gt;). At this stage only confirm boundaries and naming; resources sitting in mixed RGs stay on the sheet's &lt;strong&gt;to-migrate&lt;/strong&gt; list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rough permissions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Filter the &lt;strong&gt;Activity Log&lt;/strong&gt; for &lt;code&gt;shared-deploy@example.com&lt;/code&gt;: the shared-account era's write operations per &lt;strong&gt;resourceId&lt;/strong&gt;, attributed to the right team / resource type, so no team or type is missed; then draft built-in roles per &lt;strong&gt;dev / prod&lt;/strong&gt; (e.g. dev ACA &lt;strong&gt;Contributor&lt;/strong&gt;, prod ACA &lt;strong&gt;Operator&lt;/strong&gt;); capabilities without a fitting built-in role are marked &lt;strong&gt;single-resource&lt;/strong&gt; + role name. A &lt;strong&gt;rough list&lt;/strong&gt; is enough for now&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;relay's inventory, as an example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;team&lt;/th&gt;
&lt;th&gt;SGRP&lt;/th&gt;
&lt;th&gt;dev RG&lt;/th&gt;
&lt;th&gt;prod RG&lt;/th&gt;
&lt;th&gt;Resource summary&lt;/th&gt;
&lt;th&gt;dev rough roles&lt;/th&gt;
&lt;th&gt;prod rough roles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;relay&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SGRP-RELAY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RG-RELAY-DEV-SEA&lt;/code&gt; · &lt;code&gt;SUB-EG-APP-DEV&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RG-RELAY-PRD-SEA&lt;/code&gt; · &lt;code&gt;SUB-EG-APP-PRD&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ACA, PG, KV, Storage&lt;/td&gt;
&lt;td&gt;ACA Contributor, PG @ server…&lt;/td&gt;
&lt;td&gt;ACA Operator; no human read/write on prod KV secrets…&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;subscription_key&lt;/code&gt; is simply &lt;code&gt;dev&lt;/code&gt; / &lt;code&gt;prod&lt;/code&gt;; the platform's subscription table resolves the &lt;strong&gt;subscription ID&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Move resources into the team RGs by environment&lt;/strong&gt;&lt;br&gt;
Following the inventory's &lt;strong&gt;to-migrate&lt;/strong&gt; column, move ACA, databases, storage, and the like into the team's &lt;strong&gt;dev&lt;/strong&gt; or &lt;strong&gt;prod&lt;/strong&gt; RG. Only after the move does &lt;strong&gt;scope&lt;/strong&gt; match the RBAC tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Write the sheet into the configuration&lt;/strong&gt;&lt;br&gt;
Cross-check the "rough permissions" against the &lt;strong&gt;Activity Log&lt;/strong&gt;, then fill &lt;code&gt;teams.&amp;lt;team&amp;gt;&lt;/code&gt; per team: &lt;code&gt;members&lt;/code&gt;, plus &lt;strong&gt;dev&lt;/strong&gt; / &lt;strong&gt;prod&lt;/strong&gt; &lt;code&gt;dev_resource_groups&lt;/code&gt; / &lt;code&gt;prod_resource_groups&lt;/code&gt;; inventory rows marked "single-resource" go into &lt;code&gt;resource_access&lt;/code&gt;; the shared &lt;strong&gt;ACR&lt;/strong&gt; goes into &lt;code&gt;shared_resource_access&lt;/code&gt;. A person on two teams → two &lt;code&gt;members&lt;/code&gt; entries, permissions unioned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tighten the shared ACR one more notch&lt;/strong&gt;&lt;br&gt;
Several teams share one &lt;strong&gt;ACR&lt;/strong&gt;; read access via &lt;code&gt;shared_resource_access&lt;/code&gt; is enough. If pull/push must also be separated by repository prefix (e.g. &lt;code&gt;relay/…&lt;/code&gt;, &lt;code&gt;pulse/…&lt;/code&gt;), pick one path and stay on it: either an &lt;strong&gt;ABAC&lt;/strong&gt; condition on the team identity, or &lt;strong&gt;scope tokens&lt;/strong&gt;. Condition/token details live in the example directory's &lt;code&gt;README.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;relay's final table (&lt;code&gt;dev_resource_groups&lt;/code&gt; / &lt;code&gt;prod_resource_groups&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;dev&lt;/th&gt;
&lt;th&gt;prod&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ACA apps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Container Apps Contributor&lt;/td&gt;
&lt;td&gt;Container Apps Operator&lt;/td&gt;
&lt;td&gt;dev: free to tweak revisions and troubleshoot; &lt;strong&gt;prod&lt;/strong&gt; releases go through the &lt;strong&gt;pipeline&lt;/strong&gt; (the service connection uses a deployment identity), so team members don't need standing &lt;strong&gt;Contributor&lt;/strong&gt;/&lt;strong&gt;Container Apps Contributor&lt;/strong&gt; — &lt;strong&gt;Operator&lt;/strong&gt; covers status checks and limited operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key Vault&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Key Vault Secrets Officer, etc.&lt;/td&gt;
&lt;td&gt;Usually no human read/write on &lt;strong&gt;prod secrets&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Daily development shouldn't touch production secrets; one-off initialization is handled by the platform team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Storage Account Contributor&lt;/td&gt;
&lt;td&gt;Storage Blob Data Reader&lt;/td&gt;
&lt;td&gt;prod data plane is commonly read-only; blob writes go to the &lt;strong&gt;pipeline&lt;/strong&gt; or a backend identity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Example &amp;amp; verification
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;th&gt;What it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[permissions/examples/team-permissions/](#https://github.com/NewbMiao/terraform-on-azure-examples/tree/main/permissions/team-permissions)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Three teams' &lt;code&gt;teams&lt;/code&gt;, relay's single-resource grant, and the shared &lt;strong&gt;ACR&lt;/strong&gt; with optional &lt;strong&gt;scope tokens&lt;/strong&gt;; one &lt;strong&gt;provider&lt;/strong&gt; alias per &lt;code&gt;dev&lt;/code&gt; / &lt;code&gt;prod&lt;/code&gt;, consistent with the "one provider set per subscription" mental model on the permissions side. See that directory's &lt;code&gt;README.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; permissions/examples/team-permissions
terraform init
terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;code&gt;terraform plan&lt;/code&gt; diff: do the &lt;strong&gt;SGRP&lt;/strong&gt;s, per-team &lt;strong&gt;RG&lt;/strong&gt;s, and role names line up? On the &lt;strong&gt;prod&lt;/strong&gt; side of the table, it should still be &lt;strong&gt;Operator&lt;/strong&gt;-class — the whole subscription must not get painted &lt;strong&gt;Contributor&lt;/strong&gt; again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quiz
&lt;/h2&gt;

&lt;p&gt;Why bind RBAC to the &lt;strong&gt;SGRP&lt;/strong&gt; instead of assigning roles to members directly on the &lt;strong&gt;RG&lt;/strong&gt;? When a team is just two or three people, isn't the group ceremony overkill?&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;That round of &lt;strong&gt;PE&lt;/strong&gt; changes left one fear hanging over relay: what if it turns red again in &lt;strong&gt;prod&lt;/strong&gt; — the first sentence in the meeting room would still be "who did it," and the answer can't be a single mailbox.&lt;/p&gt;

&lt;p&gt;After the change, the &lt;strong&gt;Activity Log&lt;/strong&gt; can at least trace back to the team and the person: people move through &lt;strong&gt;SGRP&lt;/strong&gt;s, things land in each team's own &lt;strong&gt;RG&lt;/strong&gt;, &lt;strong&gt;dev&lt;/strong&gt; and &lt;strong&gt;prod&lt;/strong&gt; mind their own business — no more one key taking the blame for the whole crew.&lt;/p&gt;

&lt;p&gt;But keep this in mind: a &lt;strong&gt;Contributor&lt;/strong&gt; grant is still hanging on the subscription, and asking "who" still goes in circles — don't grant more than the few RGs each team is actually willing to own.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>devops</category>
    </item>
    <item>
      <title>Terraform Wasn't the Hard Part</title>
      <dc:creator>newbmiao</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:22:55 +0000</pubDate>
      <link>https://dev.to/newbmiao/terraform-wasnt-the-hard-part-3h59</link>
      <guid>https://dev.to/newbmiao/terraform-wasnt-the-hard-part-3h59</guid>
      <description>&lt;p&gt;&lt;em&gt;An introduction to Terraform on Azure — Platform Governance in Practice&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I learned Terraform the usual way: I opened the documentation, created a resource group, added a VNet, and broke things in a subscription where breaking things was allowed.&lt;/p&gt;

&lt;p&gt;The early questions had tidy answers. Where should variables go? How should I structure a module? What does &lt;code&gt;plan&lt;/code&gt; show me before &lt;code&gt;apply&lt;/code&gt; changes anything? Books and examples were good at this part. After a while, I could read a plan without holding my breath.&lt;/p&gt;

&lt;p&gt;The difficult part started when the code belonged to more than one team.&lt;/p&gt;




&lt;h2&gt;
  
  
  The questions change
&lt;/h2&gt;

&lt;p&gt;Once several teams share the same platform, the questions stop being about one Terraform feature at a time. They look more like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Azure Activity Log shows that a shared account changed a private endpoint, but not which person or team used it. How do we give teams enough access to work without losing accountability?&lt;/li&gt;
&lt;li&gt;A database moves behind a private endpoint. The application can reach it, but the deployment pipeline cannot. What else has to move when a service becomes private?&lt;/li&gt;
&lt;li&gt;A routine capacity change produces a plan that would also restore last month's container image. Which fields belong to the platform configuration, and which belong to the application release?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each problem starts with an operational event, not an obscure piece of HCL. The work is to narrow it into something verifiable, decide whether Terraform should own it, and choose the right boundary and order of change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I am writing this series
&lt;/h2&gt;

&lt;p&gt;I wanted to write about that gap: the space between knowing Terraform and using it safely in a platform shared by several teams.&lt;/p&gt;

&lt;p&gt;The series won't move through Terraform features one by one. It will follow operational problems and bring in Terraform where it helps. Sometimes the answer is in &lt;code&gt;tfvars&lt;/code&gt;; sometimes it is a module boundary or a question of state ownership. The important part is understanding why that boundary exists before encoding it.&lt;/p&gt;

&lt;p&gt;The setting is a fictional organization called &lt;strong&gt;EG&lt;/strong&gt;. It has dev and prod subscriptions, three application teams named &lt;strong&gt;relay&lt;/strong&gt;, &lt;strong&gt;pulse&lt;/strong&gt;, and &lt;strong&gt;harbor&lt;/strong&gt;, and a platform engineer named &lt;strong&gt;Dee&lt;/strong&gt; who is often pulled into the middle.&lt;/p&gt;

&lt;p&gt;Each post starts with a problem that could arrive in a support channel: a change nobody can trace, a pipeline that can no longer reach Key Vault, or an application that needs to move behind a private endpoint. From there, we'll work out what happened, who owns which part, and whether Terraform should manage it.&lt;/p&gt;

&lt;p&gt;The story is arranged in three acts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Act I · Who can change what          Act II · How far can we narrow access
  Permission autonomy                   Hardening (network · private access · edge)
        │                                        │
        └────────────────┬───────────────────────┘
                         ▼
                Act III · Can we keep production running
                Operations · monitoring · cost
                         │
                         ▼
                 Toolbox · quality and efficiency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Act I deals with people, teams, permissions, and deployment identities. Act II moves into networking, private access, and the public edge. Act III looks at what happens after the platform is running: drift, monitoring, cost, and maintenance.&lt;/p&gt;

&lt;p&gt;There is also a toolbox section for Checkov, TFLint, terraform-docs, Infracost, &lt;code&gt;terraform test&lt;/code&gt;, and pre-commit. These tools matter, but only after we've decided what rule they are supposed to enforce.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to expect
&lt;/h2&gt;

&lt;p&gt;This is not a syntax course. It is for people who already know enough Terraform to create resources, then discover that creating them was the simple part.&lt;/p&gt;

&lt;p&gt;If you've ever looked at a valid plan and still wondered whether applying it was the right thing to do, you are in the right place.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
