<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Amaan&apos;s Engineering Notes</title><description>DevOps, cloud security, and platform engineering across AWS, OCI and Azure, by Amaan Ul Haq Siddiqui.</description><link>https://amaanx86.github.io/</link><language>en</language><item><title>Forward Deployed Engineer is an old job with a new name, plus an agent</title><link>https://amaanx86.github.io/blog/forward-deployed-engineer-what-transfers/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/forward-deployed-engineer-what-transfers/</guid><description>Shipping systems into environments you don&apos;t own, getting them through security review, and handing them to a team that has to run them is not new work. Agents add a real layer on top. Here&apos;s what carried over when I started building agents on AWS, and what I had to learn.</description><pubDate>Sat, 31 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You’ve been asked to build an agent for a customer. It runs in their AWS account, on their data, behind their identity provider, and their security team hasn’t approved Bedrock yet. Somebody on their side will have to keep it running after you’re gone.&lt;/p&gt;
&lt;p&gt;Congratulations, you’re a Forward Deployed Engineer now. That’s the title AWS and Anthropic are hiring for, and recruiters have started putting it on anything that involves a customer and an LLM.&lt;/p&gt;
&lt;p&gt;I’ve been doing the non-AI version of this job for quite some time. Build inside the customer’s account, get it past their security review, hand it to their team. Lately more and more of that work has had an agent sitting in the middle of it: AgentCore runtimes, Strands agents, tools behind a gateway, a worker pulling off a queue. And the honest surprise was how little of the work was the agent itself.&lt;/p&gt;
&lt;p&gt;So this is what carried over from infrastructure work, and what I actually had to learn.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-theyre-actually-hiring-for&quot;&gt;What they’re actually hiring for&lt;/h2&gt;
&lt;p&gt;Read a couple of these job posts and they’re describing the same person. Someone who embeds with a customer, builds on the customer’s data, owns the thing in production, and comes away with patterns the next engagement can reuse. There’s one line in Anthropic’s post that I think gets it exactly right:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You don’t need to have shipped agents to production. You do need deep experience building and operating production systems, hands-on time with agents, and the judgment to lead architecture and advisory work with enterprise customers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Prompting is the easy part. What’s hard to find is someone who can get a system through a messy enterprise (legacy systems, SSO nobody owns, compliance, procurement) and leave it in a state the customer can actually run.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-stuff-i-already-knew-how-to-do&quot;&gt;The stuff I already knew how to do&lt;/h2&gt;
&lt;p&gt;Discovery hasn’t changed. Nobody walks in saying “we need an agent with three tools and a memory store.” They say “my team spends half the day on this” or “the auditors are on our back.” You still have to find the real constraint before you draw anything, and it’s almost never the model. It’s data residency, or a network team that needs three weeks to open egress, or a change board that meets every other Thursday. Find that in week one and you design around it. Find it in week six and you’re rebuilding.&lt;/p&gt;
&lt;p&gt;Then there’s the security review, which is where these projects actually stall. Not the code. Someone wants a data-flow diagram. Someone wants to know which principal calls the model, where prompts get logged, and for how long. Procurement has never approved Bedrock before and wants to know what it is. If you’ve spent years filling in security questionnaires for normal infrastructure, you’ll notice the answers are mostly the same ones you’ve always given. Which IAM role, what CloudTrail records, who can read the logs. Raise those questions yourself early and the agent ships. Wait for the customer’s security team to raise them and it stays a demo.&lt;/p&gt;
&lt;p&gt;Handover is the part I care about most, because it’s the real test of the work. Infrastructure as code their engineers can read. A CI pipeline they own, using OIDC, with no access keys sitting in a variables page. A runbook written for their on-call engineer, not for me. With an agent the runbook gets longer, because there are new things to explain: how to roll back a prompt, how to fail over to the backup model, what to do when the dead-letter queue starts filling up at 2am.&lt;/p&gt;
&lt;p&gt;And if you maintain Terraform modules or CI templates, you already have the habit of turning repeat work into something reusable. With agents the reusable pieces are different (a tool contract, an eval harness, a gateway setup) but it’s the same muscle.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-agent-is-the-smallest-box-on-the-diagram&quot;&gt;The agent is the smallest box on the diagram&lt;/h2&gt;
&lt;p&gt;The first time I drew a production agent system end to end, the agent runtime was one box. Everything around it was the stuff we’ve always built.&lt;/p&gt;
&lt;p&gt;Inbound events don’t call the agent directly. They land in a queue, and a worker picks them up and invokes the agent, because nobody should be sitting on a synchronous request to a model that might take thirty seconds to answer. If a message keeps failing, it goes to a dead-letter queue after a few attempts, and a CloudWatch alarm pages a human. An agent that fails quietly is the worst kind.&lt;/p&gt;
&lt;p&gt;There’s always a path back to a person. If the system can’t finish something within a reasonable window, it hands off. I’d design that path before anything else, honestly, because it’s the thing everyone forgets until the first bad day.&lt;/p&gt;
&lt;p&gt;The agent doesn’t call your APIs directly either. Tools sit behind AgentCore Gateway, which exposes Lambda functions as MCP tools with IAM auth, so every tool call is an authenticated, logged request against a role you scoped. Memory is its own component, and it’s data like any other data, with the same retention and residency questions. And there’s a primary model and a fallback, wired up in the application, so one throttled model doesn’t take the whole thing down.&lt;/p&gt;
&lt;p&gt;None of that is AI work. It’s distributed systems work, and it’s most of the job. That’s why I think the “forward deployed” part of the title matters more than the “AI” part.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;treat-the-agent-like-a-privileged-principal&quot;&gt;Treat the agent like a privileged principal&lt;/h2&gt;
&lt;p&gt;This is where infrastructure and security people have a real edge, and where most agent demos quietly fall apart.&lt;/p&gt;
&lt;p&gt;Think about what an agent with tools actually is. It’s a process that decides at runtime which APIs to call, with arguments partly shaped by whatever text a user or a retrieved document fed it. That’s a confused-deputy problem. Prompt injection doesn’t have to break the model. It just has to talk the model into using a tool it should never have had in the first place.&lt;/p&gt;
&lt;p&gt;So I ask the same things I’d ask about any workload, just more nervously. What can it touch? Each tool gets its own scoped role, not one big role for the whole agent. Whose identity is it acting as, the user’s or a service account that can see everyone’s data? What’s the blast radius if it makes a bad call? Reads by default, writes behind a human. And can I reconstruct what happened afterwards, from CloudTrail or Azure Monitor, without guessing?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://amaanx86.github.io/_astro/fde-agent-trust-boundary.KhKIdOQb_1gHTRx.webp&quot; alt=&quot;Agent trust boundary on AWS: untrusted user input and retrieved documents reach an agent runtime on Bedrock AgentCore, which calls the foundation model through Bedrock Guardrails. A policy check in the tool layer sends reads straight to an order lookup Lambda whose role allows only GetItem and Query on a DynamoDB table, and holds writes for human approval before a refund Lambda with a capped write role calls the payments API. CloudTrail and CloudWatch record every model and tool call&quot; width=&quot;1768&quot; height=&quot;884&quot; /&gt;&lt;/p&gt;
&lt;p&gt;On AWS, least privilege for a lookup tool is just boring IAM. Which is the point:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  &quot;Version&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;2012-10-17&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  &quot;Statement&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;Sid&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;ReadOrdersForLookupToolOnly&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;Effect&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;Allow&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;Action&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;dynamodb:GetItem&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;dynamodb:Query&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;Resource&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;arn:aws:dynamodb:us-east-1:123456789012:table/orders&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;Sid&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;ApplyTheOneGuardrailThisAgentUses&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;Effect&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;Allow&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;Action&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;bedrock:ApplyGuardrail&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;      &quot;Resource&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;arn:aws:bedrock:us-east-1:123456789012:guardrail/abcd1234efgh&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;dynamodb:*&lt;/code&gt;, no &lt;code&gt;Resource: &quot;*&quot;&lt;/code&gt;, nothing that writes. On Azure you’d do the same thing with a managed identity per tool and a tight role assignment in Entra ID.&lt;/p&gt;
&lt;p&gt;The best thing you can put in an agent design doc is a short list of things the system will never do. Pick the actions that stay with a human, write them down, and then check that no tool role can do them. I’ve found that one decision does more for the blast radius than any amount of guardrail tuning.&lt;/p&gt;
&lt;p&gt;Lots of people can get an agent to call a tool. Not many can sit in front of a customer’s security team, explain why that agent can’t be talked into deleting a table, and show them the policy and the audit trail that prove it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-i-had-to-learn&quot;&gt;What I had to learn&lt;/h2&gt;
&lt;p&gt;I’m still learning most of this, so take it as notes rather than answers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evals.&lt;/strong&gt; In normal software a test passes or fails, and the same input gets you the same output. Agents don’t work like that. They’re non-deterministic, they’re usually wrong in subtle ways rather than obvious ones, and a prompt or model change can make things worse on inputs nobody thought to test. The instinct to gate deploys carries over fine. What doesn’t carry over is knowing what to measure. You end up building a golden dataset with the customer’s domain experts, picking between exact-match checks, rubric scoring and LLM-as-judge, and arguing about a pass threshold the business will actually sign off on. That’s product and data work. It’s also never finished, because the cases that matter most are the ones production shows you later.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://amaanx86.github.io/_astro/fde-eval-loop.BumkaEs7_2iY5H3.webp&quot; alt=&quot;Eval loop: a pull request that changes a prompt, tool, or model triggers a CI job that assumes an AWS role through OIDC for one hour, runs the eval suite against the golden set, and blocks the merge if the pass rate is below threshold. Passing changes deploy to the production agent, live traffic is sampled and scored, and new failure cases are added back to the golden dataset in S3&quot; width=&quot;1548&quot; height=&quot;519&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The CI side is the easy bit, and it looks like any other gate:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;agent-evals&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  pull_request&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    paths&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;agent/**&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;prompts/**&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;evals/**&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;permissions&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  id-token&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;write&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;   # OIDC to AWS, no stored keys&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  contents&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  evals&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;          role-to-assume&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;arn:aws:iam::123456789012:role/agent-evals-ci&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;          aws-region&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;us-east-1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;          role-duration-seconds&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;3600&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;      # Fails the job if the pass rate on the golden set drops below threshold&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;uv run python evals/run.py --dataset evals/golden.jsonl --min-pass-rate 0.92&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Twenty lines of YAML. The &lt;code&gt;golden.jsonl&lt;/code&gt; it points at takes weeks of sitting with people who know what a right answer looks like.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Retrieval.&lt;/strong&gt; Turning on Bedrock Knowledge Bases or Azure AI Search takes an afternoon. Getting answers people trust is a different story: chunking, metadata, stale documents, access control on the source files, and that one PDF that’s actually a scanned fax. I understood the plumbing on day one. Tuning recall on a messy corpus took a lot longer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Guardrails.&lt;/strong&gt; Bedrock Guardrails and Azure AI Content Safety handle content filters, denied topics and PII redaction, and you should turn them on. But they won’t decide whether the agent is allowed to issue a refund over a certain amount, or whether a tool call needs a human to approve it. That logic lives in your application and your tool design.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On-call.&lt;/strong&gt; You already know how distributed systems break. The model brings its own failures on top. The provider updates something and behaviour shifts. A prompt change regresses. A tool returns garbage and the agent confidently acts on it. A conversation loops and quietly burns through tokens. So the dashboards change. You end up watching per-session traces, tool-call success rates, eval scores on sampled live traffic, and cost per completed task, alongside the usual latency and 5xx graphs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;if-youre-coming-from-infrastructure&quot;&gt;If you’re coming from infrastructure&lt;/h2&gt;
&lt;p&gt;Build one agent end to end and run it for a month. Not in a notebook. Deploy it properly, with IaC, OIDC-based CI, scoped tool roles, a DLQ with an alarm, tracing, and a budget alert. Write a handful of evals before you write the prompts, even twenty hand-labelled examples. It changes how you design, because you stop asking “does it work?” and start asking “how often does it fail, and on what?”&lt;/p&gt;
&lt;p&gt;Draw the whole system, not just the agent. If your diagram has one box labelled “agent” and nothing around it, that’s a demo.&lt;/p&gt;
&lt;p&gt;And lean into the security side. It’s the gap most agent builders have, and it’s the thing enterprise buyers worry about most. You’re not competing with ML specialists there. You’re the person they need on the team.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The FDE title will probably get stretched until it doesn’t mean much, same as “DevOps engineer” did. The job underneath is real, though. Sit with the customer, ship into their production environment, own it when it breaks, and leave behind something their team can run. If you’ve been doing infrastructure delivery for a while, you already do most of it. The agent part you can learn, the same way we learned everything else: by running it in production and finding out where it breaks.&lt;/p&gt;
</content:encoded><category>fde</category><category>ai</category><category>agents</category><category>aws</category><category>career</category><category>platform-engineering</category></item><item><title>Desirable Difficulties</title><link>https://amaanx86.github.io/blog/deliberate-difficulties-get-uncomfortable-teach-repeat/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/deliberate-difficulties-get-uncomfortable-teach-repeat/</guid><description>Desirable difficulties is a cognitive psychology idea: making learning harder on purpose slows you down now and makes it stick later. This is the loop I run on to keep growing as a platform engineer, and the one I&apos;m running now to get my head around AI/ML.</description><pubDate>Fri, 31 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most people learn the fast way now. Copy the snippet the AI gives you, follow the tutorial, ship it. That’s fine, up to a point. But if that’s the whole loop you stay shallow, and you end up with a career’s worth of experience that’s really one year repeated ten times.&lt;/p&gt;
&lt;p&gt;There’s a name for the alternative, and it’s from cognitive psychology, not a productivity thread. It’s called &lt;strong&gt;desirable difficulties&lt;/strong&gt;: intentional friction in how you learn slows your progress now but drastically improves what sticks, and the part that matters at work, your ability to apply it to a problem you’ve never seen. The easy path feels productive. The hard path is productive. Those aren’t the same thing, and your brain is very good at lying to you about which one you’re on.&lt;/p&gt;
&lt;p&gt;So here’s the loop I run on: read, experiment, get uncomfortable, teach, repeat. If it were that simple everyone would already do it, so let me be specific.&lt;/p&gt;
&lt;p&gt;One thing up front. Before the open source, before the Go, before any of the code, I’m a DevOps and platform engineer. My problems are shaped like the systems I run: infrastructure that has to stay up, a migration that can’t lose data, a legacy service nobody wants to touch. The loop is the same everywhere.&lt;/p&gt;
&lt;p&gt;One more thing, because you’ll notice it. I wrote a lot of this with an AI in the loop, on a site I build with one. That’s not the shortcut I opened by warning about. What keeps you shallow isn’t the tool, it’s reaching for it without thinking: pasting what it hands you, shipping it, never asking why it works or what breaks when you pull a line. Used the other way it gets you to the hard part faster instead of letting you skip it. I still own every call here, still have to verify the output and rewrite it until it says what I actually mean, and the moment I stop doing that is the moment it makes me worse, not better. The tool didn’t change the discipline. It raised the cost of skipping it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;read-what-tells-you-why-not-just-how&quot;&gt;Read what tells you &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;how&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;Tutorials teach you what. Books and real docs teach you why. That difference is the whole game.&lt;/p&gt;
&lt;p&gt;A tutorial hands you a working result and a false sense of understanding. You followed the steps, it ran, you feel like you learned something. You didn’t, you pattern-matched, and the moment the real problem leaves the happy path you’re back to searching. Read the actual docs, not the quick-start. The quick-start is a tutorial in a trench coat, built to get you running, not to teach you the tool. The reference, the design rationale, the section nobody reads about why a default is what it is: that’s where the depth is.&lt;/p&gt;
&lt;p&gt;The work I’m proudest of was less about writing code than reading how the thing was already built, and why, so what I added felt native instead of bolted on. Same with any platform call worth making. You don’t get the reasoning behind a hard default, the kind that only clicks once you’ve watched it save you, from a quick-start. The people who know their tools deeply are, not by accident, the ones who enjoy the work more.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;break-things-on-purpose&quot;&gt;Break things on purpose&lt;/h2&gt;
&lt;p&gt;Reading alone doesn’t cut it. You know the feeling when you copy code, it works, and you move on? That’s the trap. It ran, so you stopped thinking.&lt;/p&gt;
&lt;p&gt;The people who grow fast ask the annoying question: why does this work, and what breaks if I pull this one line? Then they pull it and watch it break. On a platform that’s a throwaway cluster you wreck on purpose, a failover you trigger in staging before it triggers itself in production, a Terraform module you tear down to see what the state really depends on. That’s desirable difficulties at its purest. Watching it fail teaches you what the line was for, and copy-paste can’t, because copy-paste skips the part where you’d have had to think.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;live-at-the-edge-of-what-you-can-do&quot;&gt;Live at the edge of what you can do&lt;/h2&gt;
&lt;p&gt;Experimenting means trying things that feel uncomfortable, and that’s the part people quietly avoid.&lt;/p&gt;
&lt;p&gt;Building the same thing for two years, or reaching only for the first tool you learned, is a comfort zone, and a comfort zone is where growth goes to die. There’s a name for the productive spot, the &lt;strong&gt;zone of proximal development&lt;/strong&gt;: the band just past what you can do alone. Not so hard you’re lost, but hard enough you have to think and maybe ask for help. Too easy, nothing rewires. Too hard, you bounce off.&lt;/p&gt;
&lt;p&gt;So aim there on purpose. Take the ticket nobody wants. Most of what I know about how systems fail came from being pushed out of my depth: the migration with no clean rollback, the outage in a layer I didn’t understand until it was on fire, the tool I had to learn under pressure because something depended on it. Right now that edge is AI/ML, getting genuinely fluent instead of nodding along. The discomfort isn’t a warning sign. It’s your brain laying down new pathways. Read it as a green light.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;reach-for-the-hard-problems-because-nobody-hands-them-out&quot;&gt;Reach for the hard problems, because nobody hands them out&lt;/h2&gt;
&lt;p&gt;The growth-making problems don’t get assigned. You go get them.&lt;/p&gt;
&lt;p&gt;Early on especially, you have to walk up to your lead and say it out loud: “I want something bigger.” Volunteer for the work everyone avoids. The legacy service people are scared of. The modernization that means understanding a system nobody documented. The migration with the data-residency constraint. The backlog item that’s sat there because it’s genuinely hard. That’s the work that teaches.&lt;/p&gt;
&lt;p&gt;And own the outcome, not the task. There’s a real gap between “I did the ticket” and “I made it work in production and I’ll be the one who understands it when it pages at 2am.” Responsibility is a skill, and you only get better at it by carrying some.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;open-source-is-the-most-underrated-growth-tool-there-is&quot;&gt;Open source is the most underrated growth tool there is&lt;/h2&gt;
&lt;p&gt;Once you’re doing harder work you want real feedback on it. Open source is the best way to level up that I know.&lt;/p&gt;
&lt;p&gt;There’s a myth it’s for senior engineers who’ll flame you for a misplaced brace. It isn’t. Your first contribution can be a docs fix, and the moment it lands you’re inside a real production codebase, watching how professionals structure, name, and test things, getting review from those same people. The way in is simple: find a project you actually use, open the issues tab, filter for the newcomer-tagged ones, start there. I’ve contributed upstream to projects far bigger than anything I’d have built alone, and it held every time. Maintainers with a higher bar than mine pulled my bar up with them.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;surround-yourself-with-people-whose-standards-you-envy&quot;&gt;Surround yourself with people whose standards you envy&lt;/h2&gt;
&lt;p&gt;There’s an old line that you’re the average of the five people you spend the most time with. It’s a cliche because it’s true, and it’s true for engineering. If everyone around you is fine with messy infra and half-shipped features, that becomes your normal, and your normal becomes your ceiling.&lt;/p&gt;
&lt;p&gt;Find people whose work makes you think “I’m not there yet.” Communities, maintainers on projects you contribute to, engineers who hold a line you respect. You don’t need a formal mentor with scheduled 1:1s. You need regular exposure to a higher bar, because standards are contagious both ways. Being the sharpest person in every room feels good and teaches you nothing.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;teach-it-and-watch-every-gap-light-up&quot;&gt;Teach it, and watch every gap light up&lt;/h2&gt;
&lt;p&gt;Once you’ve read, experimented, and been pushed by better engineers, teach. This sounds backwards while you still feel like you’re learning. It’s the highest-leverage thing on the list.&lt;/p&gt;
&lt;p&gt;Feynman’s whole point was that if you can’t explain something simply, you don’t really understand it. Nothing exposes the gap between “I understand this” and “I can explain this” faster than trying to explain it. Write the post, walk a colleague through the design, record a two-minute screen capture. The instant you teach it, every gap lights up: the hand-wavy bit you skated over, the thing you thought you knew but can’t put into words. Closing those gaps is where it consolidates.&lt;/p&gt;
&lt;p&gt;This post is that. The loop taught me something, so I’m teaching it back, which is how I find out whether I understood it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-loop&quot;&gt;The loop&lt;/h2&gt;
&lt;p&gt;That’s it. &lt;strong&gt;Read&lt;/strong&gt; the source that explains why. &lt;strong&gt;Experiment&lt;/strong&gt; and break things on purpose. &lt;strong&gt;Get uncomfortable&lt;/strong&gt; at the edge of what you can do and reach for the work nobody wants. Surround yourself with &lt;strong&gt;higher standards&lt;/strong&gt;. Then &lt;strong&gt;teach&lt;/strong&gt; it, and repeat.&lt;/p&gt;
&lt;p&gt;None of the steps are clever. What makes it work is that every one is a deliberate difficulty, trading short-term comfort for long-term capability on purpose. That’s not friction you streamline away. The friction is the mechanism. The engineers who compound over a decade didn’t find the smoothest path. They kept choosing the uncomfortable one until it stopped feeling like a choice.&lt;/p&gt;
&lt;p&gt;Make it harder on purpose.&lt;/p&gt;
</content:encoded><category>career</category><category>learning</category><category>devops</category><category>platform-engineering</category><category>growth</category></item><item><title>I Stopped Clicking Through the AWS Pricing Calculator. Now I Just Describe the Architecture.</title><link>https://amaanx86.github.io/blog/aws-pricing-calculator-mcp/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/aws-pricing-calculator-mcp/</guid><description>The AWS Pricing Calculator MCP lets Claude build a real estimate from a sentence and hand back a shareable calculator.aws URL. No credentials, no clicking. For presales engineers this is the difference between a same-day quote and &apos;let me get back to you next week.&apos;</description><pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you have built an estimate in the AWS Pricing Calculator by hand, you know the drill. Open calculator.aws, search a service, click in, stare at twenty fields half of which you do not need, guess at the ones the form does not explain, pick a region, repeat for every service. Then redo the whole thing next week when the customer asks what it looks like in Frankfurt.&lt;/p&gt;
&lt;p&gt;For presales that is not a small annoyance. It is the gap between giving a number on the call and saying “let me get back to you.” I wired the &lt;a href=&quot;https://github.com/aws-samples/sample-aws-pricing-calculator-mcp&quot;&gt;AWS Pricing Calculator MCP&lt;/a&gt; into Claude, and the first real estimate I built took one sentence.&lt;/p&gt;
&lt;h2 id=&quot;what-it-is&quot;&gt;What it is&lt;/h2&gt;
&lt;p&gt;An MCP server - an AWS Samples project - that exposes the Pricing Calculator as tools an agent can call. You describe the workload, the agent assembles the estimate, the server saves it to the real calculator, and you get a shareable &lt;code&gt;calculator.aws&lt;/code&gt; URL back. Same link you would have built by hand, minus the form.&lt;/p&gt;
&lt;p&gt;Three things make it usable in front of a customer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No AWS credentials.&lt;/strong&gt; It hits the public, unauthenticated &lt;code&gt;calculator.aws&lt;/code&gt; endpoints. You are not pointing it at an account or assuming a role. There is no blast radius.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live definitions.&lt;/strong&gt; It pulls the calculator manifest at runtime - about 436 services - so it is current, not a snapshot from six months ago.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real, editable estimates.&lt;/strong&gt; The URL it returns opens in the actual calculator. Tweak it, send it, whatever. The agent just did the boring part.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It runs over &lt;code&gt;stdio&lt;/code&gt; for local clients like Claude Desktop, Kiro, and Cursor, or over HTTP (&lt;code&gt;MCP_TRANSPORT=http&lt;/code&gt;) if you want it hosted. It also handles the &lt;code&gt;aws-iso&lt;/code&gt; and &lt;code&gt;aws-eusc&lt;/code&gt; partitions, which matters for sovereign and regulated work.&lt;/p&gt;
&lt;h2 id=&quot;context-is-the-whole-job&quot;&gt;Context is the whole job&lt;/h2&gt;
&lt;p&gt;The honest part: it is amazing &lt;em&gt;when you feed it the right context&lt;/em&gt;. Ask for “an estimate for a web app” and you get back a web app someone else imagined. The calculator never knew your traffic - you did. The MCP does not change that.&lt;/p&gt;
&lt;p&gt;What it changes is the translation. Once you know the shape - two m5.large instances, an ALB, 500 GB of S3, daily backups - turning that into a saved, priced, shareable estimate is instant instead of twenty minutes of clicking. You bring the requirements, the agent does the assembly. That split is the right one.&lt;/p&gt;
&lt;h2 id=&quot;a-real-estimate-start-to-finish&quot;&gt;A real estimate, start to finish&lt;/h2&gt;
&lt;p&gt;Here is one I just built. I asked for a small web tier in &lt;code&gt;us-east-1&lt;/code&gt;: two &lt;code&gt;m5.large&lt;/code&gt; instances on demand with gp3 storage, one Application Load Balancer, 500 GB of S3 Standard, and AWS Backup holding 30 days of daily EBS recovery points. I named it “Presales Demo.”&lt;/p&gt;
&lt;p&gt;The agent searched the services, pulled the fields, filled them, ran a preflight check, and saved it:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;EC2 + ALB + S3 + Backup - Presales Demo&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  &quot;sharable_url&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;https://calculator.aws/#/estimate?id=59b31cbdf4a0a628b10748930fe53c8c023fd080&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  &quot;services&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;service&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;ec2Enhancement&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,         &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;group&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;Prod&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;service&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;applicationLoadBalancer&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;group&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;Prod&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;service&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;amazonS3Standard&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,        &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;group&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;Prod&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;service&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;ebsBackup&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,               &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;&quot;group&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;Prod&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;a href=&quot;https://calculator.aws/#/estimate?id=59b31cbdf4a0a628b10748930fe53c8c023fd080&quot;&gt;link is live&lt;/a&gt; - it opens in the calculator with all four services priced and grouped under “Prod,” ready to edit or hand off. I never opened a browser to make it.&lt;/p&gt;
&lt;p&gt;The preflight is the part I lean on. Some calculator fields are optional to the save API but required by the pricing engine - leave one out and the estimate saves at $0. On my first pass it caught that EC2 needed &lt;code&gt;tenancy&lt;/code&gt; set and flagged it instead of saving a wrong number. A confidently wrong estimate is worse than no estimate, so a tool that refuses to ship one is doing me a favor.&lt;/p&gt;
&lt;h2 id=&quot;import-swap-region-re-export&quot;&gt;Import, swap region, re-export&lt;/h2&gt;
&lt;p&gt;The other half of presales is reworking last quarter’s estimate, not starting fresh. The MCP imports any estimate by URL or ID - as JSON to edit (swap the region, bump the counts, re-export) or as Markdown to hand an LLM for analysis. The region-swap loop drops from “rebuild it in the UI” to import, change one field, export.&lt;/p&gt;
&lt;h2 id=&quot;why-it-matters-for-presales&quot;&gt;Why it matters for presales&lt;/h2&gt;
&lt;p&gt;The old loop: gather requirements, spend twenty minutes clicking them in, fat-finger a field, redo it, reply by end of week. The estimate was the bottleneck.&lt;/p&gt;
&lt;p&gt;The new loop: describe the architecture while the customer is still on the call, get a shareable URL before it ends. “Actually it is closer to 80 million requests” - fine, update it, the link is still warm. The estimate moves to where the requirements live, which is the conversation. That is the real change, more than the speed.&lt;/p&gt;
&lt;h2 id=&quot;running-it&quot;&gt;Running it&lt;/h2&gt;
&lt;p&gt;It is an AWS Samples repo, so install follows the README: clone it, install deps, register it as an MCP server in your client. After that you do not call tools by name - you describe the architecture and let the agent reach for them in order. Source, the verified configs, and setup are in the &lt;a href=&quot;https://github.com/aws-samples/sample-aws-pricing-calculator-mcp&quot;&gt;repo on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The summary is simple. It does not know AWS pricing for you and it does not invent your requirements. It deletes the twenty minutes between knowing what the customer needs and having a link to send. For anyone building these all day, that twenty minutes was the job.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Also published on &lt;a href=&quot;https://amaanx86.hashnode.dev/i-stopped-clicking-through-the-aws-pricing-calculator-now-i-just-describe-the-architecture&quot;&gt;Hashnode&lt;/a&gt;, &lt;a href=&quot;https://dev.to/amaanx86/i-stopped-clicking-through-the-aws-pricing-calculator-now-i-just-describe-the-architecture-2leg&quot;&gt;DEV.to&lt;/a&gt;, and &lt;a href=&quot;https://builder.aws.com/content/3FlCdo3esEVEOIDf0FvYp0rVTjJ/i-stopped-clicking-through-the-aws-pricing-calculator-now-i-just-describe-the-architecture&quot;&gt;AWS Builder Center&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>aws</category><category>mcp</category><category>claude</category><category>presales</category><category>cost-optimization</category><category>tools</category></item><item><title>I Got Tired of Passing --profile on Every OCI CLI Command</title><link>https://amaanx86.github.io/blog/ocswitch-oci-profile-switcher/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/ocswitch-oci-profile-switcher/</guid><description>ocswitch is a small shell script that manages multiple OCI profiles and persists the active one across all terminal sessions. No more --profile flags, no more editing config files.</description><pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is a problem you only run into if you work for Oracle directly or you work at a Cloud/DevOps MSP managing OCI for multiple clients. Most people will never touch it. If you are reading this, you are probably not most people.&lt;/p&gt;
&lt;p&gt;The OCI CLI already demands a &lt;code&gt;--compartment-id&lt;/code&gt; on almost every command. Compartment IDs are long, they are not memorable, and you need a different one depending on what you are looking at. That was already a nightmare before you add multiple tenancies to the picture. Once you are juggling prod, staging, dev, and a handful of client accounts, passing &lt;code&gt;--profile&lt;/code&gt; on top of &lt;code&gt;--compartment-id&lt;/code&gt; on every single command becomes the kind of friction that breaks you.&lt;/p&gt;
&lt;p&gt;So I wrote &lt;code&gt;ocswitch&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-the-oci-cli-actually-gives-you&quot;&gt;What the OCI CLI Actually Gives You&lt;/h2&gt;
&lt;p&gt;The OCI CLI reads config from &lt;code&gt;~/.oci/config&lt;/code&gt; by default. You can point it at a different file with &lt;code&gt;OCI_CONFIG_FILE&lt;/code&gt;, and select a named profile within that file with &lt;code&gt;OCI_CLI_PROFILE&lt;/code&gt;. That is the full surface area you have to work with.&lt;/p&gt;
&lt;p&gt;The multi-profile story in the official tooling is basically: put multiple &lt;code&gt;[PROFILE_NAME]&lt;/code&gt; sections in one config file and pass &lt;code&gt;--profile PROFILE_NAME&lt;/code&gt; every time. If you forget the flag, you hit whichever profile is in &lt;code&gt;[DEFAULT]&lt;/code&gt;. This works but it is tedious, and it is session-scoped - export the env var in one terminal, open another, you are back to default.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ocswitch&lt;/code&gt; fixes the persistence problem by writing the active profile to &lt;code&gt;~/.oci/active_profile&lt;/code&gt; and restoring it at shell startup.&lt;/p&gt;
&lt;h2 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h2&gt;
&lt;p&gt;Profiles are separate config files in &lt;code&gt;~/.oci/&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;~/.oci/config_prod&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;~/.oci/config_staging&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;~/.oci/config_dev&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One file per tenancy, named &lt;code&gt;config_&amp;lt;profile&amp;gt;&lt;/code&gt;. When you switch:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;ocswitch&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; prod&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things happen: &lt;code&gt;~/.oci/active_profile&lt;/code&gt; gets written with the string &lt;code&gt;prod&lt;/code&gt;, and &lt;code&gt;OCI_CONFIG_FILE&lt;/code&gt; plus &lt;code&gt;OCI_CLI_PROFILE&lt;/code&gt; are exported in the current shell. Every &lt;code&gt;oci&lt;/code&gt; command you run after that hits the prod tenancy.&lt;/p&gt;
&lt;p&gt;The persistence part lives at the top of the script, outside the function, so it runs every time the shell sources the file:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;_oci_state&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;${&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;HOME&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;}/.oci/active_profile&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [[ &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;-f&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;$_oci_state&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; ]]; &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  _oci_profile&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cat&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;$_oci_state&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  _oci_cfg&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;${&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;HOME&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;}/.oci/config_${&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;_oci_profile&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [[ &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;-f&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;$_oci_cfg&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; ]]; &lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;    export&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; OCI_CONFIG_FILE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;$_oci_cfg&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;    export&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; OCI_CLI_PROFILE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;config_${&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;_oci_profile&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;  fi&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open a new terminal, source kicks in, reads the state file, exports the right vars. The active profile follows you across sessions without any extra steps.&lt;/p&gt;
&lt;h2 id=&quot;what-it-looks-like&quot;&gt;What It Looks Like&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://amaanx86.github.io/_astro/ocswitch-demo.BT5R50ed_ZMaccM.webp&quot; alt=&quot;ocswitch terminal output&quot; width=&quot;1470&quot; height=&quot;596&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ocswitch list&lt;/code&gt; shows all detected profiles (anything matching &lt;code&gt;~/.oci/config_*&lt;/code&gt;) with the active one highlighted in Oracle red. &lt;code&gt;ocswitch clear&lt;/code&gt; removes the state file and unsets both env vars.&lt;/p&gt;
&lt;p&gt;Tab completion is included for both bash and zsh. Hit tab after &lt;code&gt;ocswitch&lt;/code&gt; and you get &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;clear&lt;/code&gt;, and all your profile names.&lt;/p&gt;
&lt;h2 id=&quot;the-ocid-bonus&quot;&gt;The ocid Bonus&lt;/h2&gt;
&lt;p&gt;There is a second function in the script: &lt;code&gt;ocid&lt;/code&gt;. It reads the active config, pulls the tenancy OCID and user OCID out of it, and calls the OCI IAM API to resolve the human-readable names:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Tenant Name: mycompany-production&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Tenant ID:   ocid1.tenancy.oc1..aaaa...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;User Email:  user@example.com&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Useful when you have switched between a few profiles and want to confirm exactly which tenancy you are currently pointing at before running something destructive.&lt;/p&gt;
&lt;h2 id=&quot;install&quot;&gt;Install&lt;/h2&gt;
&lt;h3 id=&quot;zsh&quot;&gt;zsh&lt;/h3&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;zsh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; https://gist.githubusercontent.com/amaanx86/2621a181e2f4b572a1904d65748d66bd/raw/ocswitch.zsh&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  -o&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ~/.oci/ocswitch.zsh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &apos;source ~/.oci/ocswitch.zsh&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; &gt;&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ~/.zshrc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ~/.zshrc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;bash&quot;&gt;bash&lt;/h3&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; https://gist.githubusercontent.com/amaanx86/2621a181e2f4b572a1904d65748d66bd/raw/ocswitch.bash&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  -o&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ~/.oci/ocswitch.bash&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &apos;source ~/.oci/ocswitch.bash&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt; &gt;&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ~/.bashrc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;source&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ~/.bashrc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ocswitch               # show help + logo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ocswitch list          # list profiles (active highlighted)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ocswitch &amp;#x3C;profile&gt;     # switch profile (persists across all terminals)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ocswitch clear         # clear active profile&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;profile-setup&quot;&gt;Profile Setup&lt;/h2&gt;
&lt;p&gt;Each profile is a standard OCI config file at &lt;code&gt;~/.oci/config_&amp;lt;profile&amp;gt;&lt;/code&gt;. The section header inside the file uses the same name as the file itself, not &lt;code&gt;[DEFAULT]&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ini&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;[config_prod]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;=ocid1.user.oc1..aaaa...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;fingerprint&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;=xx:xx:xx:...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;tenancy&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;=ocid1.tenancy.oc1..aaaa...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;region&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;=us-ashburn-1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#D73A49;--shiki-dark:#F97583&quot;&gt;key_file&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;=~/.oci/oci_api_key.pem&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So &lt;code&gt;~/.oci/config_staging&lt;/code&gt; has &lt;code&gt;[config_staging]&lt;/code&gt; inside, &lt;code&gt;~/.oci/config_dev&lt;/code&gt; has &lt;code&gt;[config_dev]&lt;/code&gt;, and so on.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The full source is in &lt;a href=&quot;https://gist.github.com/amaanx86/2621a181e2f4b572a1904d65748d66bd&quot;&gt;this gist&lt;/a&gt;. Two files, one for bash and one for zsh, no external dependencies beyond the OCI CLI itself.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Also published on &lt;a href=&quot;https://forums.oracle.com/ords/apexds/post/i-got-tired-of-passing-profile-on-every-oci-cli-command-7904&quot;&gt;Oracle Forums&lt;/a&gt;, &lt;a href=&quot;https://dev.to/amaanx86/i-got-tired-of-passing-profile-on-every-oci-cli-command-3jpd&quot;&gt;DEV.to&lt;/a&gt;, and &lt;a href=&quot;https://amaanx86.hashnode.dev/i-got-tired-of-passing-profile-on-every-oci-cli-command&quot;&gt;Hashnode&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>oracle-cloud</category><category>shell</category><category>cli</category><category>infrastructure</category><category>tools</category></item><item><title>DNS as Code: Managing Cloudflare Records with DNSControl</title><link>https://amaanx86.github.io/blog/dns-as-code-with-dnscontrol-and-cloudflare/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/dns-as-code-with-dnscontrol-and-cloudflare/</guid><description>Manual DNS edits in dashboards leave no audit trail and no review process. DNSControl turns your Cloudflare zone into a Git-backed, PR-reviewed, CI-deployed system - here is a working POC you can fork.</description><pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every DNS change I have made through a dashboard felt wrong. Click the right zone, add the right record, hope you did not fat-finger the value. No diff. No review. No way to know what the zone looked like six months ago without digging through provider audit logs that may or may not exist.&lt;/p&gt;
&lt;p&gt;DNS controls traffic. It determines where your mail goes, which servers handle your HTTPS, which CAs are allowed to issue certificates for your domain. It deserves the same discipline as application code: version control, peer review, automated deployment.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://dnscontrol.org&quot;&gt;DNSControl&lt;/a&gt; is how you get there. It is an open-source tool from Stack Exchange that treats your DNS zone as code. You define records in a config file, run a preview to see what would change, and push to apply. It supports Cloudflare, Route53, and about 30 other providers.&lt;/p&gt;
&lt;p&gt;This post walks through my working POC at &lt;a href=&quot;https://github.com/amaanx86/cloudflare-dnscontrol&quot;&gt;amaanx86/cloudflare-dnscontrol&lt;/a&gt;, built on &lt;code&gt;runonaws.dev&lt;/code&gt; on Cloudflare DNS. Everything here is forkable.&lt;/p&gt;
&lt;h2 id=&quot;the-model&quot;&gt;The model&lt;/h2&gt;
&lt;p&gt;The repo has a simple structure:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dnsconfig.js           # Main DNSControl config&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;domains/               # One JSON file per subdomain&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  @.json               # Apex / root records&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  amaan.json           # amaan.runonaws.dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  stg.json             # stg.runonaws.dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;utils/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  reserved.json        # Subdomain names that are protected&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  internal.json        # Subdomains fully ignored by DNSControl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;.github/workflows/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  preview-records.yml  # PR: dnscontrol preview → PR comment&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  publish-records.yml  # Push to main: dnscontrol push → live&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dnsconfig.js&lt;/code&gt; is the single source of truth for the zone. &lt;code&gt;domains/&lt;/code&gt; is where contributors make changes. &lt;code&gt;utils/&lt;/code&gt; holds policy that applies zone-wide. Adding a subdomain means dropping a JSON file in &lt;code&gt;domains/&lt;/code&gt; and opening a PR. Nothing else.&lt;/p&gt;
&lt;h2 id=&quot;declaring-a-subdomain&quot;&gt;Declaring a subdomain&lt;/h2&gt;
&lt;p&gt;Each subdomain is a JSON file in &lt;code&gt;domains/&lt;/code&gt;. The filename is the subdomain label, so &lt;code&gt;amaan.json&lt;/code&gt; becomes &lt;code&gt;amaan.runonaws.dev&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    &quot;owner&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;        &quot;username&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;amaanx86&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;        &quot;email&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;[email protected]&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    &quot;records&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;        &quot;CNAME&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;amaanx86.github.io&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;        &quot;TXT&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;google-site-verification=aXFvR-FIaRAyOQDhDkfHac3qIX56pg9TwSaJpnq-S3A&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    &quot;proxied&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;owner&lt;/code&gt; field is not interpreted by DNSControl. It is accountability metadata that answers the question every ops team eventually asks: whose record is this and who do you contact when it breaks? In a shared zone it is the difference between a record you can quietly delete and one that needs a conversation first.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxied: true&lt;/code&gt; enables Cloudflare’s orange-cloud proxy. &lt;code&gt;false&lt;/code&gt; is DNS-only. Every subdomain declares this explicitly so there is no zone-wide default to accidentally inherit. NS delegations, mail servers, and internal services should not be proxied, and an explicit field per record makes that impossible to get wrong silently.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;records&lt;/code&gt; object supports A, AAAA, CAA, CNAME, DS, MX, NS, SRV, TLSA, and TXT. Multiple values per type where the record type allows it.&lt;/p&gt;
&lt;h2 id=&quot;what-to-ignore&quot;&gt;What to ignore&lt;/h2&gt;
&lt;p&gt;DNSControl’s default behavior is to reconcile your zone to exactly match the code. That is dangerous at the apex. Root-domain records like NS and DNSKEY are auto-managed by Cloudflare. If DNSControl deletes them because they are not in your code, you lose DNS for the entire domain.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;IGNORE&lt;/code&gt; directive tells DNSControl to leave certain records alone:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;IGNORE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;@&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;NS&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;IGNORE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;@&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;DNSKEY&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;IGNORE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;@&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;MX&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;IGNORE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;@&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;TXT&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;IGNORE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;*._domainkey&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;TXT&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;IGNORE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;_acme-challenge&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;TXT&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;IGNORE&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;_dmarc&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;TXT&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this setup, CAA records are the only root-domain records managed as code. CAA controls which certificate authorities can issue certs for your domain, so having it in version control with a review process is worth the overhead. Everything else at the apex is either auto-managed by Cloudflare or changes rarely enough that the dashboard is fine.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;_acme-challenge&lt;/code&gt; and &lt;code&gt;*._domainkey&lt;/code&gt; ignores keep ACME certificate issuance and DKIM from getting blown away by a push. &lt;code&gt;utils/internal.json&lt;/code&gt; holds subdomains that DNSControl should not touch at all, useful for anything managed by a separate system.&lt;/p&gt;
&lt;h2 id=&quot;reserving-subdomains&quot;&gt;Reserving subdomains&lt;/h2&gt;
&lt;p&gt;If anyone can add a subdomain via PR, nothing stops someone from claiming &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;api&lt;/code&gt;, &lt;code&gt;billing&lt;/code&gt;, or &lt;code&gt;auth&lt;/code&gt; before the platform team does.&lt;/p&gt;
&lt;p&gt;Reserved subdomains solve this by pre-populating those names with placeholder A records pointing to &lt;code&gt;203.0.113.0&lt;/code&gt;. That address is from TEST-NET-3 (&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5737.html&quot;&gt;RFC 5737&lt;/a&gt;), a documentation-reserved block that will never route anywhere real. The subdomain exists in DNS, Cloudflare proxies it, requests to it return a Cloudflare error. Claiming it requires removing the placeholder, which requires a PR, which requires approval.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;utils/reserved.json&lt;/code&gt; covers about 80 names: &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;api&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;, &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;billing&lt;/code&gt;, &lt;code&gt;blog&lt;/code&gt;, &lt;code&gt;cdn&lt;/code&gt;, &lt;code&gt;mail&lt;/code&gt;, and the rest of the usual candidates.&lt;/p&gt;
&lt;h2 id=&quot;the-zone-timestamp&quot;&gt;The zone timestamp&lt;/h2&gt;
&lt;p&gt;Every push writes a TXT record at &lt;code&gt;_zone-updated&lt;/code&gt; with the current Unix timestamp. You can check it any time:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;dig&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; TXT&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; _zone-updated.runonaws.dev&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; +short&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two uses: confirming a push actually landed, and spotting records that look stale. If the timestamp predates your last push, something went wrong in the pipeline.&lt;/p&gt;
&lt;h2 id=&quot;the-cicd-pipeline&quot;&gt;The CI/CD pipeline&lt;/h2&gt;
&lt;p&gt;Two GitHub Actions workflows handle the deployment lifecycle.&lt;/p&gt;
&lt;p&gt;On pull request, the preview workflow runs &lt;code&gt;dnscontrol preview&lt;/code&gt; against Cloudflare and posts the output as a PR comment. Reviewers see exactly which records will be added, modified, or deleted before anything merges. The comment updates on force-push so it always reflects the current branch state. No dashboard access needed to understand what a change does.&lt;/p&gt;
&lt;p&gt;On push to main, the publish workflow runs &lt;code&gt;dnscontrol push&lt;/code&gt; and the changes go live.&lt;/p&gt;
&lt;p&gt;Both workflows only fire on changes to &lt;code&gt;domains/*&lt;/code&gt;, &lt;code&gt;dnsconfig.js&lt;/code&gt;, &lt;code&gt;utils/reserved.json&lt;/code&gt;, or the workflow files themselves. Unrelated repo commits do not trigger a DNS push.&lt;/p&gt;
&lt;p&gt;The Cloudflare API token needs Zone.DNS write permission scoped to the specific zone, not the whole account.&lt;/p&gt;
&lt;h2 id=&quot;what-the-workflow-looks-like-in-practice&quot;&gt;What the workflow looks like in practice&lt;/h2&gt;
&lt;p&gt;Someone needs to add &lt;code&gt;staging.runonaws.dev&lt;/code&gt; pointing to an ALB. They:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create a branch: &lt;code&gt;dns/add-staging&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add &lt;code&gt;domains/staging.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    &quot;owner&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;        &quot;username&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;amaanx86&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;        &quot;email&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;[email protected]&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    &quot;records&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;        &quot;CNAME&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;my-alb-1234567890.us-east-1.elb.amazonaws.com&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;    &quot;proxied&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open a PR&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Preview workflow posts the diff as a comment: one CNAME being added&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reviewer confirms the target and approves&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Merge to main, Cloudflare updates within seconds&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The contributor never opened the Cloudflare dashboard. The reviewer saw the exact change before it applied. The commit history shows who added the record, when, and why.&lt;/p&gt;
&lt;h2 id=&quot;the-sync-catches-what-the-dashboard-quietly-added&quot;&gt;The sync catches what the dashboard quietly added&lt;/h2&gt;
&lt;p&gt;Here is where the reconciliation model earns its keep. This is the actual preview comment the GitHub Actions bot posted on &lt;a href=&quot;https://github.com/amaanx86/cloudflare-dnscontrol/pull/10&quot;&gt;PR #10&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;******************** Domain: runonaws.dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;4 corrections (cloudflare)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;± MODIFY _zone-updated.runonaws.dev TXT (&quot;1758268077330&quot; ttl=300) -&gt; (&quot;1779036943050&quot; ttl=300)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- DELETE argo.runonaws.dev A 192.168.1.57 proxy=false ttl=1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- DELETE test.runonaws.dev CNAME e73da4f6-a287-41eb-bfe9-563f73037d68.cfargotunnel.com. proxy=true ttl=1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;+ CREATE test.runonaws.dev A 203.0.113.0 proxy=true ttl=1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;argo.runonaws.dev&lt;/code&gt; and &lt;code&gt;test.runonaws.dev&lt;/code&gt; were added directly in the Cloudflare dashboard, outside of the repo. DNSControl has no knowledge of them. From its perspective, those records do not belong in the zone and will be deleted on the next push. The preview comment is the warning before that happens.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;test.runonaws.dev&lt;/code&gt; shows up as both a DELETE and a CREATE. The CNAME pointing to a Cloudflare Tunnel was manually added; the replacement A record pointing to &lt;code&gt;203.0.113.0&lt;/code&gt; comes from &lt;code&gt;reserved.json&lt;/code&gt;. So the sync removes the manual record and installs the reserved placeholder in its place.&lt;/p&gt;
&lt;p&gt;This is exactly the behavior you want, but it means any record added through the dashboard will eventually get wiped. Before merging a PR, check the preview output for unexpected DELETEs. If a record that should stay is listed for deletion, port it into a &lt;code&gt;domains/&lt;/code&gt; JSON file first. Once it is in the repo, it is safe.&lt;/p&gt;
&lt;p&gt;The alternative is to merge and let the sync clean up the manual additions. Either way, the zone ends up matching the code.&lt;/p&gt;
&lt;h2 id=&quot;provider-portability&quot;&gt;Provider portability&lt;/h2&gt;
&lt;p&gt;DNSControl abstracts the provider. Switching from Cloudflare to Route53 is a one-line change in &lt;code&gt;dnsconfig.js&lt;/code&gt;. The domain JSON files, reserved and internal lists, and CI pipelines stay the same. Zone definitions are not coupled to the DNS provider.&lt;/p&gt;
&lt;p&gt;The POC uses Cloudflare because that is where &lt;code&gt;runonaws.dev&lt;/code&gt; lives, but the same setup runs against any of the 30+ supported providers.&lt;/p&gt;
&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;/h2&gt;
&lt;p&gt;The repo is at &lt;a href=&quot;https://github.com/amaanx86/cloudflare-dnscontrol&quot;&gt;amaanx86/cloudflare-dnscontrol&lt;/a&gt;. Fork it, swap in your domain name and Cloudflare API token, and populate &lt;code&gt;domains/&lt;/code&gt; with your existing records.&lt;/p&gt;
&lt;p&gt;The hardest part is the initial import. DNSControl’s &lt;code&gt;get-zones&lt;/code&gt; command can export an existing zone to its DSL format, giving you a starting point for the JSON conversion. Do it once to get in sync, and every change after that is a PR.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;DNS is infrastructure. Treat it like infrastructure.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Also published on &lt;a href=&quot;https://amaanx86.hashnode.dev/dns-as-code-managing-cloudflare-records-with-dnscontrol&quot;&gt;Hashnode&lt;/a&gt;, &lt;a href=&quot;https://dev.to/amaanx86/dns-as-code-managing-cloudflare-records-with-dnscontrol-4c0j&quot;&gt;DEV.to&lt;/a&gt;, and &lt;a href=&quot;https://builder.aws.com/content/3E84BWcXBpy4ymEFgJxvsY8Icw7/dns-as-code-managing-cloudflare-records-with-dnscontrol&quot;&gt;AWS Builder Center&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>dns</category><category>cloudflare</category><category>infrastructure</category><category>devops</category><category>automation</category><category>github-actions</category></item><item><title>Bypassing Cloudflare for True Origin Health Checks with Blackbox Exporter and CoreDNS</title><link>https://amaanx86.github.io/blog/bypassing-cloudflare-origin-health-checks/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/bypassing-cloudflare-origin-health-checks/</guid><description>When Cloudflare masks origin failures, your health checks lie to you. A CoreDNS sidecar that overrides DNS resolution lets Blackbox Exporter probe origins directly while keeping TLS and Host headers intact.</description><pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You have 100+ web endpoints behind Cloudflare. You need to know if the origin is healthy. Not the edge. Not the CDN cache. The actual server behind it.&lt;/p&gt;
&lt;p&gt;Prometheus Blackbox Exporter is the standard answer for HTTP probing. Point it at a URL, it makes a request, you get &lt;code&gt;probe_success&lt;/code&gt;, &lt;code&gt;probe_duration_seconds&lt;/code&gt;, &lt;code&gt;probe_http_status_code&lt;/code&gt;. Clean, simple, works.&lt;/p&gt;
&lt;p&gt;Until you put Cloudflare in front of everything.&lt;/p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;/h2&gt;
&lt;p&gt;When Blackbox probes &lt;code&gt;https://app.example.com&lt;/code&gt;, DNS resolves to Cloudflare’s edge. The request hits their network, gets proxied, maybe served from cache. Your probe comes back 200 OK. Meanwhile the origin server is on fire and has been returning 502s for twenty minutes. Cloudflare is masking the failure by serving a stale response or its own error page with a 200-ish status.&lt;/p&gt;
&lt;p&gt;Even when it does proxy through to origin, you are measuring Cloudflare-to-origin latency plus edge processing time. Your &lt;code&gt;probe_duration_seconds&lt;/code&gt; metric is useless for understanding actual server response time.&lt;/p&gt;
&lt;p&gt;The health check is lying to you. It is monitoring Cloudflare’s availability, not yours.&lt;/p&gt;
&lt;h2 id=&quot;naive-approaches-that-do-not-work&quot;&gt;Naive approaches that do not work&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;“Just use the origin IP directly.”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://198.51.100.10&lt;/code&gt; - sure, but now TLS fails. The certificate is issued for &lt;code&gt;app.example.com&lt;/code&gt;, not for an IP address. You could skip TLS verification (&lt;code&gt;insecure_skip_verify: true&lt;/code&gt;), but then you are not testing what your users actually experience. And many applications inspect the &lt;code&gt;Host&lt;/code&gt; header for routing. Hitting a raw IP with no SNI or wrong Host header gets you a 404 or default backend, not a real health check.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“Use /etc/hosts on the monitoring server.”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Map &lt;code&gt;app.example.com&lt;/code&gt; to the origin IP in &lt;code&gt;/etc/hosts&lt;/code&gt;. This works for exactly one server. Then you have 80 endpoints across a fleet of monitoring nodes and you are managing hosts files by hand. You update an origin IP and forget to update the hosts file. You add a new domain and forget to add the entry. It is a maintenance problem disguised as a solution.&lt;/p&gt;
&lt;p&gt;Also, depending on your deployment model, you might not even control &lt;code&gt;/etc/hosts&lt;/code&gt;. And even if you do, the change is system-wide. Every process on that host now bypasses Cloudflare for those domains, which is probably not what you want.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“Set a custom DNS server in Prometheus.”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Prometheus does not probe endpoints itself. It tells Blackbox Exporter to probe them. Changing DNS settings on the Prometheus host does nothing - Blackbox is the one resolving the domain. You need to control DNS resolution at the Blackbox process level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“Curl with –resolve.”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Works great for ad-hoc debugging. Not a thing you can configure in Blackbox Exporter. The prober does not expose a per-target DNS override. It resolves using whatever DNS the process has access to.&lt;/p&gt;
&lt;h2 id=&quot;architecture&quot;&gt;Architecture&lt;/h2&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                         Public DNS (Cloudflare)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                         Returns edge IPs -- NOT what we want&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                  |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                  X  (bypassed)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                  |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;+----------------------------+    |    +----------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|      Prometheus            |    |    |      CoreDNS Sidecar       |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|                            |    |    |                            |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|  &quot;probe app.example.com&quot;   |    |    |  hosts.override:           |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|         |                  |    |    |    198.51.100.10            |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|         v                  |    |    |        app.example.com     |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|  +------------------+      |    |    |        api.example.com     |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|  | Blackbox Exporter |------+----------&gt;  203.0.113.50            |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|  |                  |  DNS query |    |        docs.example.org   |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|  |  dns: 172.20.0.53|&amp;#x3C;----------+    |                            |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|  +------------------+      |    |    |  fallthrough -&gt; 8.8.8.8    |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|         |                  |    |    +----------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;+---------|------------------+    |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          |                       |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          | TLS + SNI: app.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          | Host: app.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          | IP: 198.51.100.10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          v&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;+----------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|   Origin Load Balancer     |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|   198.51.100.10:443        |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|                            |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|   Serves real certificate  |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|   Routes by Host header    |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;+----------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          v&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;+----------------------------+&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|   Application Server       |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|   Actual origin response   |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;+----------------------------+&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The trick is one line in the Blackbox Exporter config: point its DNS at the CoreDNS sidecar instead of the system resolver. CoreDNS returns origin IPs for overridden domains, falls through to public DNS for everything else. Blackbox never knows the difference - it just resolves and probes like normal.&lt;/p&gt;
&lt;h2 id=&quot;the-actual-solution-a-dns-sidecar&quot;&gt;The actual solution: a DNS sidecar&lt;/h2&gt;
&lt;p&gt;The insight is simple once you see it: if you control what DNS server Blackbox Exporter uses, you control where its probes land. You do not need to touch Blackbox’s config at all. You do not need to modify how Prometheus scrapes. You just need a DNS resolver that lies about specific domains - returns your origin load balancer IP instead of Cloudflare’s edge IP.&lt;/p&gt;
&lt;p&gt;CoreDNS is perfect for this. It is a lightweight, pluggable DNS server. The relevant config is about six lines:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;. {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    hosts /etc/coredns/hosts.override {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        fallthrough&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    forward . 8.8.8.8&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    log&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    errors&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is it. CoreDNS loads a hosts file for overridden domains. Anything not in the file falls through to &lt;code&gt;8.8.8.8&lt;/code&gt; for normal resolution.&lt;/p&gt;
&lt;p&gt;The hosts override file is a plain hosts-format file. Nothing exotic:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Origin LB for production&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;198.51.100.10  app.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;198.51.100.10  api.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;198.51.100.10  cdn.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Origin LB for staging&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;198.51.100.20  staging.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;198.51.100.20  staging-api.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Origin LB for partner sites&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;203.0.113.50   docs.example.org&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;203.0.113.50   status.example.org&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;203.0.113.71   shop.example.net&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;203.0.113.72   blog.example.net&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each line maps a domain to the public IP of the actual load balancer in front of the origin servers. Multiple domains can point to the same LB. Commented lines for decommissioned endpoints. Straightforward.&lt;/p&gt;
&lt;p&gt;Then you point Blackbox Exporter’s DNS resolution at this CoreDNS instance. If CoreDNS is running at &lt;code&gt;172.20.0.53&lt;/code&gt;, you set Blackbox’s DNS to that address. The mechanism depends on your runtime - a &lt;code&gt;dns&lt;/code&gt; directive, a &lt;code&gt;resolv.conf&lt;/code&gt; mount, whatever your environment supports. The point is that Blackbox resolves domains through CoreDNS, not through the system default.&lt;/p&gt;
&lt;h2 id=&quot;what-happens-at-probe-time&quot;&gt;What happens at probe time&lt;/h2&gt;
&lt;p&gt;The flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Prometheus tells Blackbox: probe &lt;code&gt;https://app.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Blackbox resolves &lt;code&gt;app.example.com&lt;/code&gt; via CoreDNS at &lt;code&gt;172.20.0.53&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CoreDNS checks its hosts override file, finds the entry, returns &lt;code&gt;198.51.100.10&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Blackbox connects to &lt;code&gt;198.51.100.10:443&lt;/code&gt;, sends TLS ClientHello with SNI &lt;code&gt;app.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The load balancer presents its certificate for &lt;code&gt;app.example.com&lt;/code&gt;, TLS completes normally&lt;/li&gt;
&lt;li&gt;Blackbox sends the HTTP request with &lt;code&gt;Host: app.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The application responds directly - no Cloudflare in the path&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;TLS works because the domain name is preserved. The origin LB has the real certificate. SNI and Host header are correct because Blackbox thinks it is talking to &lt;code&gt;app.example.com&lt;/code&gt; - it just happens to have resolved to a different IP than Cloudflare’s.&lt;/p&gt;
&lt;p&gt;This is the key: you are not changing what Blackbox probes, you are changing where the name resolves to. Everything else - TLS negotiation, Host header, path, response validation - stays exactly the same.&lt;/p&gt;
&lt;h2 id=&quot;the-prometheus-side&quot;&gt;The Prometheus side&lt;/h2&gt;
&lt;p&gt;Nothing changes in how you write your scrape config. Blackbox probing uses the standard multi-target pattern:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;job_name&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;blackbox_https_prod&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  metrics_path&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/probe&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  scrape_interval&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;60s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  scrape_timeout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;15s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  params&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    module&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;http_2xx&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  static_configs&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;targets&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;app.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;api.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;shop.example.net&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;blog.example.net&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;docs.example.org&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  relabel_configs&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;source_labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__address__&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;      target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__param_target&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;      replacement&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;https://$1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;source_labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__address__&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;      target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;instance&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__address__&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;      replacement&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&amp;#x3C;blackbox-host&gt;:9115&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Targets are just domain names. The relabel chain rewrites them into probe parameters and sets the &lt;code&gt;instance&lt;/code&gt; label to the human-readable domain. Nothing here is aware of Cloudflare or DNS overrides. That is all handled one layer down.&lt;/p&gt;
&lt;p&gt;This separation matters. The person adding a new endpoint to monitoring does not need to know about the DNS plumbing. They add the domain to the target list. If the domain needs a DNS override, someone adds it to the hosts file. Two different concerns, two different files.&lt;/p&gt;
&lt;h2 id=&quot;alerting-on-actual-origin-health&quot;&gt;Alerting on actual origin health&lt;/h2&gt;
&lt;p&gt;With Cloudflare out of the path, your alerts are honest. A &lt;code&gt;probe_success == 0&lt;/code&gt; means the origin is actually down, not that Cloudflare is having a bad day.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;alert&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;EndpointDown&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  expr&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;probe_success{job=~&quot;blackbox_https_(prod|nonprod)&quot;} == 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;0m&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    severity&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;critical&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  annotations&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    summary&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;Endpoint DOWN: {{ $labels.instance }}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;alert&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;EndpointHighLatency&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  expr&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;probe_duration_seconds{job=~&quot;blackbox_https_(prod|nonprod)&quot;} &gt; 3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;10m&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    severity&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;warning&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  annotations&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    summary&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;High latency: {{ $labels.instance }} ({{ $value | printf &lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;%.2f&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; }}s)&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;for: 0m&lt;/code&gt; on the down alert - no waiting. If the origin is unreachable, you want to know immediately. The latency alert uses &lt;code&gt;for: 10m&lt;/code&gt; because transient spikes happen and you do not want noise. These thresholds are tunable, but the point is that the data feeding them is real. You are measuring origin behavior, not CDN behavior.&lt;/p&gt;
&lt;h2 id=&quot;why-not-just-use-etchosts-at-scale&quot;&gt;Why not just use /etc/hosts at scale&lt;/h2&gt;
&lt;p&gt;It is worth expanding on why the hosts-file-on-the-monitoring-host approach breaks down, because it is the first thing most people try.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scope creep.&lt;/strong&gt; You start with five domains. Then twenty. Then eighty. Each load balancer IP change requires updating the file and restarting the service. Miss one and you are probing the wrong backend for weeks without knowing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blast radius.&lt;/strong&gt; System-wide DNS overrides affect everything on the host. Your monitoring stack bypasses Cloudflare - great. But so does every other process. If you are running anything else on that host that legitimately needs to hit the CDN edge, you have broken it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reproducibility.&lt;/strong&gt; The hosts file is stateful, manual, and easy to drift. CoreDNS with a hosts file is also just a flat file, but it is scoped to a single process (Blackbox), versioned in your repo, and trivially reproducible. Rebuild the monitoring stack from scratch and the DNS overrides come with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multiple monitoring replicas.&lt;/strong&gt; If you run more than one Prometheus instance for redundancy (and you should), each one has its own Blackbox Exporter. Managing hosts files across multiple hosts is the kind of thing that works until it doesn’t. A dedicated DNS sidecar per replica is self-contained.&lt;/p&gt;
&lt;h2 id=&quot;operational-considerations&quot;&gt;Operational considerations&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Adding a new endpoint.&lt;/strong&gt; Two steps: add the domain to Prometheus targets, add the domain-to-IP mapping in the CoreDNS hosts file. Reload both. Five-minute task.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IP changes.&lt;/strong&gt; When a load balancer gets a new IP, update one line in the hosts file, restart CoreDNS. Blackbox automatically starts resolving to the new address on its next probe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Domains not behind Cloudflare.&lt;/strong&gt; The &lt;code&gt;fallthrough&lt;/code&gt; directive in CoreDNS means anything not in the hosts file gets resolved normally via &lt;code&gt;8.8.8.8&lt;/code&gt;. So you can mix Cloudflare-proxied and direct endpoints in the same Blackbox instance. No special handling needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debugging.&lt;/strong&gt; &lt;code&gt;dig @172.20.0.53 app.example.com&lt;/code&gt; tells you exactly what Blackbox will resolve. If the probe is failing, check whether CoreDNS returns the right IP. If it does, the problem is downstream. If it does not, your hosts file is wrong. Clean separation of concerns.&lt;/p&gt;
&lt;h2 id=&quot;the-broader-pattern&quot;&gt;The broader pattern&lt;/h2&gt;
&lt;p&gt;This is not really about Cloudflare specifically. The same approach works for any situation where public DNS does not resolve to where you want your probes to land:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Services behind any CDN or reverse proxy (Akamai, Fastly, AWS CloudFront)&lt;/li&gt;
&lt;li&gt;Internal services with split-horizon DNS where monitoring runs in a different network zone&lt;/li&gt;
&lt;li&gt;Migration scenarios where you want to probe the new backend before cutting DNS over&lt;/li&gt;
&lt;li&gt;Canary deployments where you want to health-check a new origin before adding it to the pool&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern is always the same: run a lightweight DNS resolver as a sidecar, override the specific records you need, let everything else pass through. Your probing tool does not need to know about any of this. It just resolves names and makes requests, which is exactly what it should do.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The setup is simple. CoreDNS is maybe 30MB of memory. The hosts file is trivially maintainable. And the result is that when your pager goes off at 3am, you know it is because the origin is actually down - not because Cloudflare rotated an edge node.&lt;/p&gt;
&lt;p&gt;That is the kind of signal worth waking up for.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Also published on &lt;a href=&quot;https://dev.to/amaanx86/bypassing-cloudflare-for-true-origin-health-checks-with-blackbox-exporter-and-coredns-4fhg&quot;&gt;DEV.to&lt;/a&gt;, &lt;a href=&quot;https://builder.aws.com/content/3Czz3pnlo6MHI2CmsxGuQh959CS/bypassing-cloudflare-for-true-origin-health-checks-with-blackbox-exporter-and-coredns&quot;&gt;AWS Builder Center&lt;/a&gt;, and &lt;a href=&quot;https://amaanx86.hashnode.dev/bypassing-cloudflare-for-true-origin-health-checks-with-blackbox-exporter-and-coredns&quot;&gt;Hashnode&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>observability</category><category>prometheus</category><category>cloudflare</category><category>dns</category><category>infrastructure</category><category>monitoring</category></item><item><title>The Git habits that made reviewers trust me before they read a single line of code</title><link>https://amaanx86.github.io/blog/git-discipline-that-built-trust/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/git-discipline-that-built-trust/</guid><description>How a structured branching strategy, conventional commits, and knowing when to use cherry-pick instead of a full merge helped me stand out when contributing to projects on GitHub, GitLab, Azure DevOps, and AWS CodeCommit.</description><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most developers treat Git as a save button. Commit when something works. Push before you go to sleep. That’s it. And honestly, it gets the job done, right up until you start sending PRs to projects with real reviewers, or working on teams where the commit history actually has to make sense to someone else.&lt;/p&gt;
&lt;p&gt;Here’s the thing: reviewers form an opinion about you before they open a single file. The branch name, the commit messages, the PR description, all of it is visible before they click into the diff. I figured this out sending contributions to projects across GitHub, GitLab, Azure DevOps, and AWS CodeCommit. The feedback loop was fast. Clean work got reviewed and merged. Noisy work got follow-up questions, or sat in the queue.&lt;/p&gt;
&lt;p&gt;So here’s what I actually do.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;three-permanent-branches-clear-purpose-for-each&quot;&gt;Three permanent branches, clear purpose for each&lt;/h2&gt;
&lt;p&gt;I keep three long-lived branches. That’s it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;: production only. Nothing goes here without a PR that passed UAT. This branch is protected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;uat&lt;/code&gt;&lt;/strong&gt;: staging and pre-production testing. Business users validate here before anything touches main.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;dev&lt;/code&gt;&lt;/strong&gt;: where everything starts. Features, fixes, experiments, all of it begins here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The exact names are not the point, and I want to be clear about that before anyone argues about it. Some projects call production &lt;code&gt;master&lt;/code&gt;, some &lt;code&gt;main&lt;/code&gt;. Pre-prod might be &lt;code&gt;uat&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, or &lt;code&gt;stage&lt;/code&gt;. The integration branch might be &lt;code&gt;dev&lt;/code&gt; or &lt;code&gt;devel&lt;/code&gt;. That choice belongs to the maintainer, and more often it is already settled by how the company, the SaaS team, or the OSS project has always done it. When you join, you inherit the convention; you don’t get to relitigate it. What actually matters is that there are stable, named tiers, that everyone knows which tier means what, and that the protected one is genuinely protected. Match the local vocabulary and apply the same discipline underneath it. I use &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;uat&lt;/code&gt;, and &lt;code&gt;dev&lt;/code&gt; here because that is the naming on the projects I am describing.&lt;/p&gt;
&lt;p&gt;The reason for a dedicated pre-prod branch (not just a tag or a deploy config) is that it gives you a stable checkpoint. When you find a bug there, you know it’s code that passed dev but failed real-world testing. That’s a different category of bug than something that never left the developer’s machine, and it deserves a different branch to fix it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;short-lived-branches-with-a-naming-pattern-that-explains-itself&quot;&gt;Short-lived branches with a naming pattern that explains itself&lt;/h2&gt;
&lt;p&gt;When I start any new piece of work I create a branch from the right source and name it properly:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Feature from dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; dev&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; pull&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -b&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; feat/user-authentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Bug found during UAT testing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; uat&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -b&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; bugfix/cart-crash-fix&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Production emergency&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -b&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; hotfix/prod-db-connection&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The prefixes matter. &lt;code&gt;feat/&lt;/code&gt;, &lt;code&gt;bugfix/&lt;/code&gt;, &lt;code&gt;hotfix/&lt;/code&gt;, &lt;code&gt;test/&lt;/code&gt;: any reviewer or CI system that sees these knows the category of the work before it looks at anything else. And because the branch name describes the problem, not the implementation, it reads the same to a human and to a ticket system.&lt;/p&gt;
&lt;p&gt;Hotfixes are the one case where you merge in three directions: back into &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;uat&lt;/code&gt;, and &lt;code&gt;dev&lt;/code&gt;. Missing one creates drift and you’ll feel it later.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conventional-commits-the-part-that-changed-how-i-write-history&quot;&gt;Conventional commits: the part that changed how I write history&lt;/h2&gt;
&lt;p&gt;This is the single highest-leverage change I made to how I use Git. The format is simple:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C;type&gt;(&amp;#x3C;scope&gt;): &amp;#x3C;description&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Types I use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat&lt;/code&gt;: new feature&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix&lt;/code&gt;: bug fix&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docs&lt;/code&gt;: documentation only&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refactor&lt;/code&gt;: restructured code, no behavior change&lt;/li&gt;
&lt;li&gt;&lt;code&gt;perf&lt;/code&gt;: performance improvement&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt;: tests added or updated&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chore&lt;/code&gt;: build tooling, CI, dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A real example from one of my projects:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;feat(auth): add JWT refresh token rotation&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Implements sliding expiration using a short-lived access token (15m)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;and a rotating refresh token (7d). Old refresh tokens are invalidated&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;immediately on use to prevent token replay.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Closes: #142&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://amaanx86.github.io/_astro/git-conventional-commits-history.DLcNKTg6_Zt1zBf.webp&quot; alt=&quot;My commit history showing conventional commits across a real project&quot; width=&quot;1284&quot; height=&quot;368&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When I send a PR with a history like this, reviewers can read the commit log like a changelog. They don’t have to reverse-engineer what I did from the diff. And when something breaks in production six months later, the person doing the post-mortem can &lt;code&gt;git log --oneline&lt;/code&gt; and actually understand the sequence of events.&lt;/p&gt;
&lt;p&gt;Semantic versioning falls out of this naturally too. If all your commits since the last tag are &lt;code&gt;fix:&lt;/code&gt;, you ship a patch. If there’s a &lt;code&gt;feat:&lt;/code&gt;, it’s a minor. If anything has a &lt;code&gt;BREAKING CHANGE&lt;/code&gt; in the footer, it’s a major. Your &lt;code&gt;CHANGELOG.md&lt;/code&gt; can be generated automatically. Your release tags follow &lt;code&gt;v1.2.3&lt;/code&gt;. No guessing.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; tag&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -a&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; v1.0.0&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;Release version 1.0.0&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --tags&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cherry-pick-when-you-need-one-thing-from-a-branch-not-the-whole-branch&quot;&gt;Cherry-pick: when you need one thing from a branch, not the whole branch&lt;/h2&gt;
&lt;p&gt;This is the tool most developers don’t reach for until they’re stuck, and then they use it wrong. Here’s the actual use case.&lt;/p&gt;
&lt;p&gt;I had a project where &lt;code&gt;uat&lt;/code&gt; and &lt;code&gt;main&lt;/code&gt; had diverged significantly. Both branches had been accumulating changes. We needed the &lt;code&gt;azure-pipelines.yml&lt;/code&gt; from &lt;code&gt;uat&lt;/code&gt; in &lt;code&gt;main&lt;/code&gt;, just that one file, because getting the pipeline consistent across environments was blocking the team. A full merge would have pulled in unvalidated changes, and that is not acceptable for a protected branch.&lt;/p&gt;
&lt;p&gt;So instead of a merge request, I used cherry-pick to move exactly the commits that touched that file.&lt;/p&gt;
&lt;p&gt;First, find the commits that changed the file in the source branch:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; uat&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; log&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; uat&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; azure-pipelines.yml&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --oneline&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;b5df122 feat: update Azure Pipelines configuration to specify custom agent pool and demands&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;44cbc1e feat: update image tag for crmiskuat container to use &apos;uat&apos; version&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;8fee9b4 feat: add Azure Pipelines configuration for OCI image build and push&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Switch to the target branch and apply them in order (oldest first):&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; cherry-pick&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; 8fee9b4&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; 44cbc1e&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; b5df122&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If there’s a conflict on just that file:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Resolve the conflict in your editor, then:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; azure-pipelines.yml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; cherry-pick&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --continue&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates new commits on &lt;code&gt;main&lt;/code&gt; with the same changes but different hashes. The original commits stay on &lt;code&gt;uat&lt;/code&gt;. You get a clean, traceable history on &lt;code&gt;main&lt;/code&gt; without dragging in anything that wasn’t ready.&lt;/p&gt;
&lt;p&gt;The key thing to understand: cherry-pick is not a shortcut for avoiding code review. You still open a PR with these commits. The difference is that the PR is surgical. It does one thing, and reviewers can verify exactly what that thing is.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-pr-description-template-i-actually-fill-out&quot;&gt;The PR description template I actually fill out&lt;/h2&gt;
&lt;p&gt;A PR is a document. If the description is empty or says “fix stuff,” you’re forcing the reviewer to read the entire diff to understand context that you already have in your head.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;markdown&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold&quot;&gt;## Linked Issue / Ticket&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;&amp;#x3C;!-- GitHub/GitLab: Closes #123 | Jira: PROJECT-456 | Azure DevOps: AB#789 --&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;Closes #&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold&quot;&gt;## Summary&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;Brief description of what this MR accomplishes.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold&quot;&gt;## Changes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; Specific change with context&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; Another change and why it was necessary&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold&quot;&gt;## Testing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [ ] Unit tests added/updated&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [ ] Integration tests pass&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [ ] Manual testing completed in dev/UAT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold&quot;&gt;## Screenshots (if applicable)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold&quot;&gt;## Checklist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [ ] Code follows project standards&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [ ] Self-review done&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [ ] No new warnings&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#E36209;--shiki-dark:#FFAB70&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; [ ] Documentation updated&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not bureaucracy. It’s a forcing function for you to think through what you did before someone else has to. Half the time, filling out the testing checklist is when I catch something I missed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;post-merge-cleanup&quot;&gt;Post-merge cleanup&lt;/h2&gt;
&lt;p&gt;After a branch merges, delete it. Both locally and remotely.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; branch&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; feat/user-authentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --delete&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; feat/user-authentication&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A repository with 200 stale branches is a repository nobody trusts. Clean branches signal that someone is actually maintaining this.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-this-matters-more-now-not-less&quot;&gt;Why this matters more now, not less&lt;/h2&gt;
&lt;p&gt;The argument I used to hear against structured commits was that it slows you down. Write the code, get it working, the history is just metadata.&lt;/p&gt;
&lt;p&gt;That argument has fully inverted. AI code review tools, automated changelog generation, semantic release pipelines: all of them work significantly better when the commit history is structured. When I use an AI assistant to review a PR now, it reads the conventional commits and understands the intent without me explaining it. When I ask it to generate release notes, it has something to work from.&lt;/p&gt;
&lt;p&gt;But more fundamentally, across reviewers at AWS, GitHub, GitLab, and Azure, the pattern was the same. The PRs that got attention were the ones that looked like the contributor had thought it through. Clean branch name, useful commits, filled-out description. It signals that you respect the reviewer’s time, and that is the actual thing that builds trust.&lt;/p&gt;
&lt;p&gt;The technical content still has to be right. But a well-structured contribution gets a genuine review. A sloppy one gets skimmed.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-actual-workflow-in-one-place&quot;&gt;The actual workflow in one place&lt;/h2&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Start a feature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; dev&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt; &amp;#x26;&amp;#x26; &lt;/span&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; pull&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; checkout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -b&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; feat/your-feature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Work in small, meaningful commits&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; src/specific-file.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; commit&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;feat(auth): add token expiry validation&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Push and open PR to dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; feat/your-feature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# After merge, clean up&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; branch&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; feat/your-feature&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --delete&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; feat/your-feature&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. No magic. Just consistency applied over enough time that it becomes automatic.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Also published on &lt;a href=&quot;https://dev.to/amaanx86/the-git-habits-that-made-reviewers-trust-me-before-they-read-a-single-line-of-code-anh&quot;&gt;DEV.to&lt;/a&gt;, &lt;a href=&quot;https://builder.aws.com/content/3CO3bh5vYaHmiPuQHDnpH4bRBDD/the-git-habits-that-made-reviewers-trust-me-before-they-read-a-single-line-of-code&quot;&gt;AWS Builder Center&lt;/a&gt;, and &lt;a href=&quot;https://amaanx86.hashnode.dev/the-git-habits-that-made-reviewers-trust-me-before-they-read-a-single-line-of-code&quot;&gt;Hashnode&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>git</category><category>workflow</category><category>devops</category><category>career</category><category>open-source</category></item><item><title>Built My Own Prometheus Service Discovery for Oracle Cloud Because a 3-Year-Old PR Never Got Merged</title><link>https://amaanx86.github.io/blog/oci-prometheus-service-discovery/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/oci-prometheus-service-discovery/</guid><description>When the upstream OCI service discovery PR sat open for three years, I built the other end of the HTTP SD interface myself. This is running in production across 10+ OCI tenancies.</description><pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There is a specific kind of frustration reserved for when you know a problem is solved, you can see the solution, and you still cannot use it. That is how this project started.&lt;/p&gt;
&lt;h2 id=&quot;the-context-setting-up-observability-from-scratch&quot;&gt;The Context: Setting Up Observability From Scratch&lt;/h2&gt;
&lt;p&gt;I was building out observability from scratch across Oracle Cloud Infrastructure - multiple tenancies, multiple regions, a decent number of compute instances spread across compartments. The goal was full coverage: every VM enrolled in monitoring, no gaps, no guesswork.&lt;/p&gt;
&lt;p&gt;When you are starting from zero, one of the first things you ask is how Prometheus is going to discover what it needs to scrape. For AWS you have EC2 service discovery built right in. Same for GCP, Azure, Hetzner, DigitalOcean. You configure credentials, set some filters, and Prometheus takes care of the rest.&lt;/p&gt;
&lt;p&gt;OCI is not on that list.&lt;/p&gt;
&lt;p&gt;I searched. I found a pull request in the Prometheus repository opened by an engineer at Oracle. It was exactly what I needed. It was also three years old and had never been merged. Comments, reviews, back and forth, and then silence. The PR is still open today. If you have gone looking for OCI service discovery in Prometheus you have probably landed on that same page, felt a brief moment of hope, and then noticed the date.&lt;/p&gt;
&lt;p&gt;So I built it myself.&lt;/p&gt;
&lt;h2 id=&quot;what-i-actually-needed&quot;&gt;What I Actually Needed&lt;/h2&gt;
&lt;p&gt;The requirement was simple: new VM comes up, Prometheus finds it and starts scraping. No manual steps in that loop. Not because I was fixing a broken process - there was no process yet. I was designing this from scratch and I was not going to design it with a gap where a human has to remember to update a config file.&lt;/p&gt;
&lt;p&gt;The concern was blind spots. Infrastructure grows, VMs get provisioned, things change. If enrollment is manual, coverage is only as good as the last person who remembered to do it. I wanted observability that was structurally complete, not best-effort.&lt;/p&gt;
&lt;p&gt;The workflow I landed on:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tag the VM in OCI.&lt;/strong&gt; When provisioning a new instance, add a tag - something like &lt;code&gt;prometheus:scrape = true&lt;/code&gt;. That is the only enrollment step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open the Prometheus port on the network security group.&lt;/strong&gt; Allow the Prometheus server IP to reach port 9100. One rule, specific source, no broad exposure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Run the node exporter playbook.&lt;/strong&gt; An Ansible playbook installs and starts node exporter. Done.&lt;/p&gt;
&lt;p&gt;That is the full enrollment flow. The VM is in monitoring. No touching the Prometheus config. No reloading Prometheus. No rollout restart of a Kubernetes deployment. No SSH back into the server to verify anything.&lt;/p&gt;
&lt;p&gt;The proxy polls OCI, finds every instance with the right tag across all configured tenancies and compartments, and hands the target list to Prometheus via the HTTP service discovery API. Prometheus picks it up on its next refresh cycle. The whole thing is automatic by design, not patched in after the fact.&lt;/p&gt;
&lt;h2 id=&quot;what-i-built&quot;&gt;What I Built&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;oci-prometheus-sd-proxy&lt;/code&gt; is a lightweight Go service that implements the &lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/http_sd/&quot;&gt;Prometheus HTTP Service Discovery&lt;/a&gt; API for OCI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://amaanx86.github.io/_astro/oci-sd-proxy-architecture.DpK0dzju_Z1JidJV.webp&quot; alt=&quot;SD Proxy Architecture&quot; width=&quot;2240&quot; height=&quot;1420&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You point Prometheus at it like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;scrape_configs&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;job_name&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;oci_instances&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    http_sd_configs&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&apos;http://oci-sd-proxy:8080/v1/targets&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        authorization&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;          type&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;Bearer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;          credentials&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&apos;YOUR_TOKEN&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        refresh_interval&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;60s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    scrape_interval&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;30s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    scrape_timeout&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;10s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    metrics_path&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;/metrics&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;    relabel_configs&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;source_labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__meta_oci_instance_name&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;instance&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;source_labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__meta_oci_tenancy_name&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;tenancy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;source_labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__meta_oci_compartment_name&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;compartment&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;source_labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__meta_oci_region&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;region&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;source_labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__meta_oci_availability_domain&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;availability_domain&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;source_labels&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;__meta_oci_instance_shape&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;        target_label&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;shape&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The proxy handles the rest. It scans OCI, filters by tag, and returns targets with rich metadata: tenancy, compartment, region, availability domain, shape, fault domain, private IP, and all your custom OCI tags as Prometheus labels. Use them for relabeling, alerting rules, dashboards - whatever you need.&lt;/p&gt;
&lt;p&gt;A few things I cared about when building it:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-tenancy from day one.&lt;/strong&gt; The proxy handles all tenancies in parallel from a single config file. One deployment, full coverage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rate limiting.&lt;/strong&gt; OCI’s API will return 429s if you push it too hard. The proxy uses a token bucket proactively and a retry policy reactively. Discovery does not silently fail under load.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security.&lt;/strong&gt; Bearer token auth, distroless container, read-only config mounts. In an HA setup it sits on the local network, only reachable by Prometheus. It does not need to be internet-facing and it should not be.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Caching.&lt;/strong&gt; Discovery results are cached so Prometheus always gets a response, even if the OCI API is momentarily slow or rate limiting.&lt;/p&gt;
&lt;h2 id=&quot;battle-tested&quot;&gt;Battle Tested&lt;/h2&gt;
&lt;p&gt;This is running in production across 10+ Oracle Cloud tenancies - different regions, different compartments, different team setups. It has handled OCI API slowness, tenancy permission edge cases, and the general entropy that comes with real infrastructure at scale.&lt;/p&gt;
&lt;p&gt;The thing that still satisfies me is watching a new VM appear in Grafana within a minute of it booting, with nobody doing anything to make that happen. That is what good observability infrastructure should feel like. You build it right once and it stays right.&lt;/p&gt;
&lt;h2 id=&quot;why-this-did-not-exist-already&quot;&gt;Why This Did Not Exist Already&lt;/h2&gt;
&lt;p&gt;OCI is a smaller player compared to AWS or GCP and Prometheus contributors naturally prioritize the platforms most of their users are on. Oracle engineers clearly wanted to solve this - that PR is evidence of that - but getting a first-party integration merged upstream is a long road with no guarantees.&lt;/p&gt;
&lt;p&gt;The HTTP SD API that Prometheus exposes is actually the right answer for this situation. It lets any platform plug in without touching the Prometheus codebase. I just had to build the other end of that interface.&lt;/p&gt;
&lt;h2 id=&quot;try-it&quot;&gt;Try It&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href=&quot;https://github.com/amaanx86/oci-prometheus-sd-proxy&quot;&gt;github.com/amaanx86/oci-prometheus-sd-proxy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: &lt;a href=&quot;https://oci-prometheus-sd-proxy.readthedocs.io/&quot;&gt;oci-prometheus-sd-proxy.readthedocs.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The docs cover installation, the full configuration reference, OCI IAM permissions, and Docker Compose deployment examples. If you are building observability on OCI and want automatic enrollment from day one, this is what I use.&lt;/p&gt;
&lt;p&gt;And if you were one of the people who commented on that PR hoping it would eventually land - same. This is what I built instead.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://forums.oracle.com/ords/apexds/post/prometheus-service-discovery-for-oci-via-http-sd-production-5441&quot;&gt;Oracle Forums - Prometheus Service Discovery for OCI via HTTP SD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.linkedin.com/posts/amaanulhaqsiddiqui_devops-prometheus-oraclecloud-activity-7437797204855087104-3F0q?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAEWbcAcBZTdKHcYrh82gRuQUI9J1PqDtYXA&quot;&gt;LinkedIn post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Also published on &lt;a href=&quot;https://dev.to/amaanx86/built-my-own-prometheus-service-discovery-for-oracle-cloud-because-a-3-year-old-pr-never-got-merged-2fme&quot;&gt;DEV.to&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>observability</category><category>prometheus</category><category>oracle-cloud</category><category>go</category><category>infrastructure</category><category>open-source</category></item><item><title>End-to-End Supply Chain Security for a Go Project: TUF on CI, cosign, and SLSA L3</title><link>https://amaanx86.github.io/blog/supply-chain-security-tuf-on-ci/</link><guid isPermaLink="true">https://amaanx86.github.io/blog/supply-chain-security-tuf-on-ci/</guid><description>How I wired together tuf-on-ci, Sigstore, SLSA L3 provenance, and GitHub Pages to build a fully verifiable release pipeline for oci-prometheus-sd-proxy with no long-lived signing keys anywhere.</description><pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Adding &lt;code&gt;cosign sign&lt;/code&gt; to a CI pipeline and calling it “signed releases” is a bit like putting a lock on a glass door. The lock works. The glass does not. Signing the image proves a specific digest was signed by a specific identity at a specific time. It says nothing about whether the source commit matches what was built, whether the build environment was clean, or whether someone replaced the release asset after the fact.&lt;/p&gt;
&lt;p&gt;I had been going deep on supply chain security for a while - reading through TUF specs, the Sigstore design docs, how Fulcio issues short-lived certificates, how Rekor works as an append-only transparency log. At some point I came across the OpenSSF Best Practices requirements and saw the full picture laid out as a checklist. I could have just signed the image and moved on. Instead I used &lt;a href=&quot;https://github.com/amaanx86/oci-prometheus-sd-proxy&quot;&gt;oci-prometheus-sd-proxy&lt;/a&gt; - a project that does OCI Prometheus service discovery - as the thing to actually build it on. I wanted to understand each layer well enough to explain it, not just wire it up. What I ended up with: cosign keyless signing, a CycloneDX SBOM attestation, SLSA L3 build provenance, and TUF metadata distribution via &lt;a href=&quot;https://github.com/amaanx86/oci-prometheus-sd-proxy-tuf-on-ci&quot;&gt;tuf-on-ci&lt;/a&gt; published to GitHub Pages. No long-lived keys anywhere in the pipeline.&lt;/p&gt;
&lt;h2 id=&quot;why-each-layer-exists&quot;&gt;Why each layer exists&lt;/h2&gt;
&lt;p&gt;This is the question worth answering properly, because you can absolutely ship just cosign and be in a better position than 95% of projects. So why go further?&lt;/p&gt;
&lt;p&gt;HTTPS protects against network-level tampering. It does not protect against a compromised GitHub account pushing a backdoored release, or a build that was modified after it completed, or an asset quietly swapped post-publication.&lt;/p&gt;
&lt;p&gt;cosign on a container image proves that a specific digest was signed by a specific OIDC identity with the event recorded in Rekor’s transparency log. That is genuinely useful - but it only covers the image. It says nothing about the source ref, the build environment, or the workflow that produced it. Someone could sign a backdoored binary and the cosign verification would pass.&lt;/p&gt;
&lt;p&gt;SLSA L3 provenance fills that gap, and it is the layer I found most interesting to wire up. The provenance is generated in a separate, isolated signing job with its own OIDC identity, not in the main build job. That isolation is what makes L3 meaningful: an attacker who compromises the main build job cannot forge L3 provenance because they do not control the signing job’s OIDC token. The provenance attests to the exact source ref, the exact workflow, and the exact runner environment. You can look at a SLSA L3 attestation and know that the image you are running came from that commit in that repo via that verified builder.&lt;/p&gt;
&lt;p&gt;TUF adds something orthogonal to all of the above - it is about &lt;em&gt;distribution trust&lt;/em&gt;, not just signing trust. It adds a role-based metadata layer where clients can verify that a release target was authorised by the project’s key holders, that the metadata has not been rolled back to a previous version, and that the metadata is actually fresh. The design difference that matters: TUF survives key compromise in a way that a single cosign keypair does not. If my cosign key leaked tomorrow, every past signature would be under a cloud. With TUF, key rotation is a defined protocol. The damage is bounded and recoverable.&lt;/p&gt;
&lt;p&gt;The point of having all four is that verification is fully independent. Verifying the image signature, the build provenance, and the TUF metadata chain are three separate operations against different data sources. Compromising any single one of them is not enough to ship a malicious release undetected. You need to compromise all of them simultaneously - and the transparency logs make doing that silently very hard.&lt;/p&gt;
&lt;h2 id=&quot;two-repos-one-trust-boundary&quot;&gt;Two repos, one trust boundary&lt;/h2&gt;
&lt;p&gt;The implementation lives across two repositories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/amaanx86/oci-prometheus-sd-proxy&quot;&gt;amaanx86/oci-prometheus-sd-proxy&lt;/a&gt;&lt;/strong&gt; - the application, the build pipeline, the release workflow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/amaanx86/oci-prometheus-sd-proxy-tuf-on-ci&quot;&gt;amaanx86/oci-prometheus-sd-proxy-tuf-on-ci&lt;/a&gt;&lt;/strong&gt; - TUF metadata, managed by tuf-on-ci, published to GitHub Pages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keeping them separate was a deliberate trust boundary decision, not just organisation. The app CI can push a signing branch to the TUF repo, but it cannot merge that branch or sign &lt;code&gt;targets.json&lt;/code&gt;. That step requires my OIDC identity authenticating via Sigstore in a browser - not the CI system’s token. An attacker who steals the app repo’s CI tokens hits a hard wall at the TUF signing step. They can push a branch, but they cannot authorise the release. The human is the last gate.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;App repo CI (run #24290258383)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── builds image (linux/amd64, linux/arm64)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── pushes to GHCR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── cosign attest --type cyclonedx sbom.cyclonedx.json (SBOM)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── cosign sign (image signature, OIDC -&gt; Fulcio cert -&gt; Rekor)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── cosign attest --type  release-metadata.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── slsa-github-generator -&gt; SLSA L3 provenance (isolated job)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── pushes sign/release-1-4-2-rc-24290258383 branch -&gt; TUF repo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;TUF repo (tuf-on-ci) [PR #8]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── signing-event.yml detects branch push -&gt; opens signing PR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── Maintainer: tuf-on-ci-sign (browser OIDC -&gt; @amaanx86 identity)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── PR merged -&gt; online-sign.yml refreshes snapshot + timestamp&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── publish.yml -&gt; GitHub Pages&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── test.yml -&gt; smoke-tests TUF client (scheduled)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Users / Policy Engines&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── cosign verify - checks image signature against Rekor&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── cosign verify-attestation --type cyclonedx - checks SBOM&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── cosign verify-attestation --type  - checks release-metadata&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── slsa-verifier verify-image - checks SLSA L3 provenance&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  └── TUF client (python-tuf ngclient) - fetches metadata from GitHub Pages, verifies chain&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-build-pipeline&quot;&gt;The build pipeline&lt;/h2&gt;
&lt;p&gt;The workflow (&lt;code&gt;docker-build-push.yml&lt;/code&gt;) triggers on release publication. After pushing the multi-arch image (linux/amd64 and linux/arm64) to GHCR it does five more things.&lt;/p&gt;
&lt;h3 id=&quot;sbom-generation-and-attestation&quot;&gt;SBOM generation and attestation&lt;/h3&gt;
&lt;p&gt;First, Syft generates a CycloneDX SBOM for the pushed image, which gets attached as a cosign attestation:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cosign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; attest&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --yes&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --predicate&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; sbom.cyclonedx.json&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --type&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; cyclonedx&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;  ghcr.io/amaanx86/oci-prometheus-sd-proxy@sha256:759e255e607f623e0b1ee4ea9df02b2aefd89e2c9ec979842ee2e6f8b21772fd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SBOM is also uploaded as a release asset (the 80.7 KB &lt;code&gt;*.cyclonedx.json&lt;/code&gt; artifact visible on the workflow run).&lt;/p&gt;
&lt;h3 id=&quot;cosign-image-signing&quot;&gt;cosign image signing&lt;/h3&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cosign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; sign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --yes&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;  ghcr.io/amaanx86/oci-prometheus-sd-proxy@sha256:759e255e607f623e0b1ee4ea9df02b2aefd89e2c9ec979842ee2e6f8b21772fd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No &lt;code&gt;--key&lt;/code&gt; flag. cosign uses the GitHub Actions OIDC token to get an ephemeral certificate from Fulcio, signs the digest, and records the operation in Rekor. The private key is generated in memory and never stored. The Rekor entry pins the workflow identity to the specific digest at a specific timestamp.&lt;/p&gt;
&lt;p&gt;Note the image is signed by digest, not by tag. Tags are mutable; the digest is what the signature actually covers.&lt;/p&gt;
&lt;p&gt;No secret to rotate. No key to leak.&lt;/p&gt;
&lt;h3 id=&quot;release-metadata-attestation&quot;&gt;Release-metadata attestation&lt;/h3&gt;
&lt;p&gt;The workflow generates a &lt;code&gt;release-metadata.json&lt;/code&gt; with the image digest, source commit, release tag, and build timestamp, then attaches it as an attestation under a custom predicate type:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cosign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; attest&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --yes&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --predicate&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; release-metadata.json&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --type&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; https://github.com/amaanx86/oci-prometheus-sd-proxy/release-metadata&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;  ghcr.io/amaanx86/oci-prometheus-sd-proxy@sha256:759e255e607f623e0b1ee4ea9df02b2aefd89e2c9ec979842ee2e6f8b21772fd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using a project-specific type URI keeps the attestation namespaced and lets &lt;code&gt;cosign verify-attestation --type &amp;lt;uri&amp;gt;&lt;/code&gt; fetch exactly this attestation rather than every in-toto statement on the image.&lt;/p&gt;
&lt;h3 id=&quot;slsa-l3-provenance&quot;&gt;SLSA L3 provenance&lt;/h3&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;with&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  image&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;ghcr.io/amaanx86/oci-prometheus-sd-proxy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#22863A;--shiki-dark:#85E89D&quot;&gt;  digest&lt;/span&gt;&lt;span style=&quot;--shiki-light:#24292E;--shiki-dark:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;${{ needs.build-and-push.outputs.digest }}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;slsa-github-generator runs as a reusable workflow with its own isolated OIDC identity. The provenance attestation is generated and signed there, not in the main build job. L3 specifically requires this isolation - the verified builder identity in the provenance is &lt;code&gt;https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0&lt;/code&gt;, distinct from the app workflow’s identity.&lt;/p&gt;
&lt;h3 id=&quot;triggering-the-tuf-signing-event&quot;&gt;Triggering the TUF signing event&lt;/h3&gt;
&lt;p&gt;Last step: clone the TUF repo and push a signing branch with a new target file at &lt;code&gt;targets/oci-prometheus-sd-proxy/releases/v1.4.2-rc.json&lt;/code&gt;. The branch name embeds the run ID to avoid collisions: &lt;code&gt;sign/release-1-4-2-rc-24290258383&lt;/code&gt; (dots in the version become hyphens, run ID appended). This branch push is what kicks off the TUF side of the pipeline.&lt;/p&gt;
&lt;p&gt;The Python tuf library (v5+) is used inline to update &lt;code&gt;metadata/targets.json&lt;/code&gt; with the new target entry before committing - the targets metadata version is incremented and signatures cleared, ready for the human signing step.&lt;/p&gt;
&lt;h2 id=&quot;tuf-on-ci&quot;&gt;tuf-on-ci&lt;/h2&gt;
&lt;p&gt;tuf-on-ci manages the TUF metadata lifecycle entirely within a GitHub repository. All online signing uses GitHub Actions OIDC. All offline signing uses Sigstore OIDC (browser-based). No key files anywhere.&lt;/p&gt;
&lt;p&gt;The TUF repo has four workflows. &lt;code&gt;signing-event.yml&lt;/code&gt; fires on any &lt;code&gt;sign/**&lt;/code&gt; branch push, opens a PR, and annotates it with what needs signing. &lt;code&gt;online-sign.yml&lt;/code&gt; runs after a signing PR is merged and refreshes &lt;code&gt;snapshot.json&lt;/code&gt; and &lt;code&gt;timestamp.json&lt;/code&gt; using the Actions OIDC token. &lt;code&gt;publish.yml&lt;/code&gt; deploys everything to GitHub Pages. &lt;code&gt;test.yml&lt;/code&gt; runs on a schedule and verifies the full metadata chain with a real TUF client to catch expiry or breakage before any user does.&lt;/p&gt;
&lt;h3 id=&quot;signing-a-release&quot;&gt;Signing a release&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;signing-event.yml&lt;/code&gt; opens PR #8 with title “Signing event: sign/release-1-4-2-rc-24290258383”, I run:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;tuf-on-ci-sign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; sign/release-1-4-2-rc-24290258383&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This opens a browser window for Sigstore OIDC. I authenticate as @amaanx86 via GitHub. Fulcio issues an ephemeral certificate tied to that identity, the tool signs &lt;code&gt;targets.json&lt;/code&gt;, and pushes the signature to the branch. Rekor gets an entry proving the person at @amaanx86 authorised this specific targets update at this specific time.&lt;/p&gt;
&lt;p&gt;Worth being clear on what “offline” means here: it requires a human with a verified identity, not a CI token. It does not require an air-gapped machine. The private key is still ephemeral.&lt;/p&gt;
&lt;p&gt;After merge, &lt;code&gt;online-sign.yml&lt;/code&gt; takes over and refreshes snapshot and timestamp automatically using the Actions OIDC token. No human needed for that part.&lt;/p&gt;
&lt;h3 id=&quot;what-gets-published&quot;&gt;What gets published&lt;/h3&gt;
&lt;p&gt;GitHub Pages at &lt;code&gt;amaanx86.github.io/oci-prometheus-sd-proxy-tuf-on-ci/metadata/&lt;/code&gt; serves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;root.json&lt;/code&gt; - signed by @amaanx86 via Sigstore OIDC; defines trusted key holders for all roles&lt;/li&gt;
&lt;li&gt;&lt;code&gt;targets.json&lt;/code&gt; - signed by @amaanx86; lists all authorised release targets with digests&lt;/li&gt;
&lt;li&gt;&lt;code&gt;snapshot.json&lt;/code&gt; - signed by GitHub Actions OIDC; prevents any metadata file from being swapped with an older version&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timestamp.json&lt;/code&gt; - signed by GitHub Actions OIDC; has a short validity window to prevent freeze attacks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each release gets a target file at &lt;code&gt;targets/oci-prometheus-sd-proxy/releases/v1.4.2-rc.json&lt;/code&gt;. The TUF metadata key (used inside &lt;code&gt;targets.json&lt;/code&gt;) is &lt;code&gt;oci-prometheus-sd-proxy/releases/v1.4.2-rc.json&lt;/code&gt; - the path is relative to the targets directory, not the repo root.&lt;/p&gt;
&lt;h2 id=&quot;verifying-v142-rc&quot;&gt;Verifying v1.4.2-rc&lt;/h2&gt;
&lt;p&gt;All verification uses the digest, not the tag, since the tag is a mutable pointer.&lt;/p&gt;
&lt;p&gt;Image signature:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cosign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; verify&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --certificate-identity=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;https://github.com/amaanx86/oci-prometheus-sd-proxy/.github/workflows/docker-build-push.yml@refs/tags/v1.4.2-rc&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --certificate-oidc-issuer=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;https://token.actions.githubusercontent.com&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;  ghcr.io/amaanx86/oci-prometheus-sd-proxy@sha256:759e255e607f623e0b1ee4ea9df02b2aefd89e2c9ec979842ee2e6f8b21772fd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SBOM attestation:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cosign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; verify-attestation&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --certificate-identity=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;https://github.com/amaanx86/oci-prometheus-sd-proxy/.github/workflows/docker-build-push.yml@refs/tags/v1.4.2-rc&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --certificate-oidc-issuer=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;https://token.actions.githubusercontent.com&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --type&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; cyclonedx&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;  ghcr.io/amaanx86/oci-prometheus-sd-proxy@sha256:759e255e607f623e0b1ee4ea9df02b2aefd89e2c9ec979842ee2e6f8b21772fd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Release-metadata attestation:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;cosign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; verify-attestation&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --certificate-identity=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;https://github.com/amaanx86/oci-prometheus-sd-proxy/.github/workflows/docker-build-push.yml@refs/tags/v1.4.2-rc&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --certificate-oidc-issuer=&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;&quot;https://token.actions.githubusercontent.com&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --type&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;https://github.com/amaanx86/oci-prometheus-sd-proxy/release-metadata&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;  ghcr.io/amaanx86/oci-prometheus-sd-proxy@sha256:759e255e607f623e0b1ee4ea9df02b2aefd89e2c9ec979842ee2e6f8b21772fd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SLSA L3 provenance (requires digest reference - slsa-verifier rejects mutable tags):&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;slsa-verifier&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; verify-image&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt;  &quot;ghcr.io/amaanx86/oci-prometheus-sd-proxy@sha256:759e255e607f623e0b1ee4ea9df02b2aefd89e2c9ec979842ee2e6f8b21772fd&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --source-uri&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;github.com/amaanx86/oci-prometheus-sd-proxy&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --source-tag&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;v1.4.2-rc&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# Verified build using builder &quot;...generator_container_slsa3.yml@refs/tags/v2.0.0&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# at commit a68d4cd5d29cc6b865c6804fe63adff14ac74b27&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6A737D;--shiki-dark:#6A737D&quot;&gt;# PASSED: SLSA verification passed&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TUF metadata chain verification is documented in detail - including the full client walkthrough and what each step validates - in the &lt;a href=&quot;https://oci-prometheus-sd-proxy.readthedocs.io/en/latest/releasing.html&quot;&gt;release verification docs&lt;/a&gt;. The short version: a compliant TUF client walks root trust, snapshot consistency, and timestamp freshness before fetching the target. If the metadata is expired, rolled back, or the signature chain is invalid, the client raises before returning anything. That freshness check is what separates TUF from static signature verification.&lt;/p&gt;
&lt;h2 id=&quot;what-this-covers-and-where-the-gaps-are&quot;&gt;What this covers and where the gaps are&lt;/h2&gt;
&lt;p&gt;This pipeline secures the release artifact and its provenance chain. An operator fetching the image can independently verify who built it, from what source, in what environment, and that the release was authorised by a human identity. That is a lot more than most projects ship with.&lt;/p&gt;
&lt;p&gt;But supply chain security has layers, and the release artifact is only one of them. A few honest gaps:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Go module dependencies.&lt;/strong&gt; The SBOM shows what modules are in the binary, and &lt;code&gt;go.sum&lt;/code&gt; pins their hashes. But &lt;code&gt;govulncheck&lt;/code&gt; and a periodic dependency audit are what actually catch known-vulnerable transitive dependencies. The attestation proves the SBOM is authentic; it does not tell you the SBOM is safe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Runtime enforcement.&lt;/strong&gt; Signing and provenance only matter if someone checks them at deploy time. Right now verification is a manual step. The more interesting place this is going is integrating the cosign attestations and SLSA provenance into a Kyverno or OPA/Gatekeeper policy engine that enforces admission control in Kubernetes. Policy engines like Kyverno can query Sigstore and reject any image that lacks a valid SLSA L3 attestation from the correct workflow identity - automatically, at admission time, not as a manual verification step. That closes the loop between what we proved at build time and what is allowed to run in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TUF root key compromise.&lt;/strong&gt; If the @amaanx86 GitHub account itself was compromised, an attacker could rotate the root TUF key in a way that would pass client verification. TUF supports threshold signatures across multiple root key holders to mitigate this, which becomes relevant as the project scales to multiple maintainers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The dependency of your dependencies.&lt;/strong&gt; None of this solves a compromised &lt;code&gt;slsa-github-generator&lt;/code&gt; or a backdoored Syft release. That is a solved problem in theory (pin action hashes, verify the tools themselves) but it is worth naming.&lt;/p&gt;
&lt;h2 id=&quot;what-i-am-building-toward&quot;&gt;What I am building toward&lt;/h2&gt;
&lt;p&gt;The next step that interests me most is closing the loop at runtime.&lt;/p&gt;
&lt;p&gt;The next meaningful investment is runtime enforcement. Kyverno ClusterPolicies that require verified SLSA provenance before admission, OPA rules that check SBOM attestations against a known-safe package policy, and Sigstore-aware image admission are all achievable with what the pipeline already produces. The attestations are already there. The policy layer that consumes them is the missing piece.&lt;/p&gt;
&lt;p&gt;After that, expanding to multi-maintainer TUF with hardware-backed root keys and threshold signatures would make the trust model genuinely robust at scale.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Full implementation details and the verification workflow are documented at &lt;a href=&quot;https://oci-prometheus-sd-proxy.readthedocs.io/en/latest/releasing.html&quot;&gt;oci-prometheus-sd-proxy.readthedocs.io/en/latest/releasing.html&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Also published on &lt;a href=&quot;https://dev.to/amaanx86/end-to-end-supply-chain-security-for-a-go-project-tuf-on-ci-cosign-and-slsa-l3-1575&quot;&gt;DEV.to&lt;/a&gt;, &lt;a href=&quot;https://builder.aws.com/content/3CEE0bcWsPBxnBBvV3nbjMf0Pwi/end-to-end-supply-chain-security-for-a-go-project-tuf-on-ci-cosign-and-slsa-l3&quot;&gt;AWS Builder Center&lt;/a&gt;, and &lt;a href=&quot;https://amaanx86.hashnode.dev/end-to-end-supply-chain-security-for-a-go-project-tuf-on-ci-cosign-and-slsa-l3&quot;&gt;Hashnode&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>security</category><category>supply-chain</category><category>tuf</category><category>sigstore</category><category>slsa</category><category>go</category><category>openssf</category></item></channel></rss>