[{"data":1,"prerenderedAt":1073},["ShallowReactive",2],{"authors":3,"\u002Fblog\u002Ftaking-terraform-to-production":26,"\u002Fblog\u002Ftaking-terraform-to-production-surround":1049,"\u002Fblog\u002Ftaking-terraform-to-production-related":1056},[4,16],{"id":5,"avatar":6,"bio":7,"extension":8,"github":9,"meta":10,"name":11,"role":12,"stem":13,"website":14,"__hash__":15},"authors\u002Fauthors\u002Fsagar-kapoor.yml","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F69609200?v=4",null,"yml","sagar-kap",{},"Sagar Kapoor","Chief Executive Officer (CEO)","authors\u002Fsagar-kapoor","https:\u002F\u002Fsagarkapoor.eu","NsLNClfs_S-SNHFEnyR6Vq_7zEc1Xxl7QLoKrk_DB6s",{"id":17,"avatar":18,"bio":7,"extension":8,"github":19,"meta":20,"name":21,"role":22,"stem":23,"website":24,"__hash__":25},"authors\u002Fauthors\u002Fsomraj-saha.yml","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F31373860?v=4","jarmos-san",{},"Somraj Saha","Chief Technology Officer (CTO)","authors\u002Fsomraj-saha","https:\u002F\u002Fjarmos.dev","mDAq8GgPZv9H8en0XHAraq3aYi_v8t2ULWGqp4cp3zY",{"id":27,"title":28,"author":29,"body":30,"category":1039,"cover":1040,"date":1041,"description":1042,"extension":1043,"meta":1044,"navigation":218,"path":1045,"seo":1046,"stem":1047,"__hash__":1048},"blog\u002Fblog\u002Ftaking-terraform-to-production.md","Taking Terraform to Production: Automating IaC with CI\u002FCD","somraj-saha",{"type":31,"value":32,"toc":1031},"minimark",[33,54,57,65,74,79,82,94,109,115,130,137,141,152,155,165,174,177,377,390,396,400,403,439,442,452,455,493,504,508,519,536,539,566,569,891,902,906,913,916,974,977,985,989,992,995,1024,1027],[34,35,36,37,47,48,53],"p",{},"Infrastructure as Code (IaC) has revolutionized how modern engineering teams\nbuild, manage, and scale cloud environments. With tools like\n",[38,39,43],"a",{"href":40,"rel":41},"https:\u002F\u002Fdeveloper.hashicorp.com\u002Fterraform",[42],"nofollow",[44,45,46],"strong",{},"Terraform",", provisioning complex\narchitecture requiring a wide variety of cloud resources is as simple as\ndefining declarative\n",[38,49,52],{"href":50,"rel":51},"https:\u002F\u002Fdeveloper.hashicorp.com\u002Fterraform\u002Flanguage",[42],"HCL (HashiCorp Configuration Language)","\nfiles.",[34,55,56],{},"However, a tool is only as reliable as the workflow surrounding it.",[34,58,59,60,64],{},"In early-stage projects, running ",[61,62,63],"code",{},"terraform apply"," directly from a developer's\nlocal terminal is common. But as your team grows and your workloads move into\nproduction, local execution becomes a massive liability at scale. To achieve\ntrue stability, auditability, and speed, you must shift Terraform from local\nexecution into an automated, secure CI\u002FCD pipeline.",[34,66,67,68,73],{},"In this guide, we’ll explore how we (at ",[38,69,72],{"href":70,"rel":71},"https:\u002F\u002Fweburz.com",[42],"Weburz",") take\nTerraform to production by building a robust CI\u002FCD pipeline, securing our\nworkflows, and prevent infrastructure drift.",[75,76,78],"h2",{"id":77},"the-risk-of-local-execution","The Risk of Local Execution",[34,80,81],{},"For testing or development purposes, running Terraform on a developer's\nenvironment is acceptable and is actually the recommended approach. Following\nthe same approach in production is unacceptable and lead to disastrous\nconsequences.",[34,83,84,85,93],{},"For starters, a developer's environment is often messy and unpredictable with\nvarying CLI versions, uncommitted code branches and what not. Hence, relying\ncompletely on a local development environments to provision the infrastructure\nusing Terraform could lead to the\n\"",[38,86,89,90,89],{"href":87,"rel":88},"https:\u002F\u002Fi.programmerhumor.io\u002F2026\u002F10\u002F08ddf72225599f91b66bb0c59d5f1dc595918eca3b0281074facb01169780453.jpeg",[42]," ",[44,91,92],{},"...but this works on my machine...","\"\nproblem.",[34,95,96,97,99,100,104,105,108],{},"Using local development environments for infrastructure provisioning also leads\nto a lack of visibility and auditability when someone executes ",[61,98,63],{},"\nlocally. There's no mandatory peer review, no centralised log history and no\nclear audit trail linking the code changes to the cloud events. You could say,\n\"",[101,102,103],"em",{},"there's a log history provided by the cloud service provider","\" but I would say\n\"",[101,106,107],{},"prevention is better than cure","\". It's always better to be cautious and follow\nestablished Standard Operating Procedures (SOP) rather than frantically figuring\nout resolutions when disaster strikes.",[34,110,111,112,114],{},"There's also a risk of concurrency which is often overlooked (especially at\nsmaller scale organisations). These issues arise in rare cases where two\nengineers run ",[61,113,63],{}," simultaneously on local machines without strict\nremote locking. In situations like these, Terraform could potentially risk\ncorrupting the state file and thereby messing up the provisioned infrastructure\nas well.",[34,116,117,118,123,124,129],{},"By far the riskiest situation is having access to the unencrypted state file on\ndisk in the developer's local environment. When provisioning the production\nenvironment, Terraform creates\n",[38,119,122],{"href":120,"rel":121},"https:\u002F\u002Fdeveloper.hashicorp.com\u002Fterraform\u002Flanguage\u002Fstate",[42],"a state file"," which\nis stored on disk (by default) but can be configured to be stored in a secured\nlocation (such as\n",[38,125,128],{"href":126,"rel":127},"https:\u002F\u002Fazure.microsoft.com\u002Fen-us\u002Fproducts\u002Fstorage\u002Fblobs",[42],"Azure Blob Storage","\ncontainers). Leaving such high-privilege credentials stored on their\nworkstations increase the attack surface if the device is ever lost or\ncompromised.",[34,131,132,133,136],{},"We eliminate these risk by automating the Terraform IaC inside a CI\u002FCD platform\nby establishing a ",[44,134,135],{},"single source of truth"," and a standardized, repeatable\nexecution pipeline. The next few sections of the blog post will shed more\ninsight in how we do it.",[75,138,140],{"id":139},"prerequisites-remote-state-and-locking","Prerequisites: Remote State and Locking",[34,142,143,144,147,148,151],{},"Before building a pipeline, it's necessary to ensure the state file is stored\nsecurely outside of local repositories since it contains a mapping of the\ndeclarative code to real-world infrastructure resources, along with sensitive\noutput values. Terraform generates these state files with a ",[61,145,146],{},".tfstate"," file\nextension and they should ",[44,149,150],{},"NEVER"," be edited manually nor committed to a\nversion control system.",[34,153,154],{},"To store the state files safely, we recommend following this checklist below:",[156,157,158,162],"ol",{},[159,160,161],"li",{},"The state file with sensitive values are protected by encrypting it at rest\nand in transit.",[159,163,164],{},"Prevent simultaneous pipeline runs from mutating the state concurrently.",[34,166,167,168,173],{},"At Weburz, we secure our Terraform state by storing the file in an Azure Blob\nStorage container (with strict RBAC rules applied to it). Terraform can\nfetch\u002Fstore the state file to the remote container as long as it's configured to\ndo so using an appropriate\n",[38,169,172],{"href":170,"rel":171},"https:\u002F\u002Fdeveloper.hashicorp.com\u002Fterraform\u002Flanguage\u002Fbackend",[42],"\"backend\"",".",[34,175,176],{},"Here is an example backend configuration using Microsoft Azure and Azure Blob\nStorage we use at Weburz:",[178,179,184],"pre",{"className":180,"code":181,"language":182,"meta":183,"style":183},"language-hcl shiki shiki-themes github-dark","terraform {\n  required_version = \">= 1.17.0\"\n\n  required_providers {\n    azurerm = {\n      source  = \"hashicorp\u002Fazurerm\"\n      version = \"~>5.12\"\n    }\n  }\n\n  backend \"azurerm\" {\n      resource_group_name  = \"WeburzRG\"\n      storage_account_name = \"terraform-state\"\n      container_name       = \"tfstate-production\"\n      key                  = \"terraform.production.tfstate\"\n  }\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n","hcl","",[61,185,186,199,213,220,228,238,250,261,267,273,278,290,302,313,325,337,342,348,353,363,372],{"__ignoreMap":183},[187,188,191,195],"span",{"class":189,"line":190},"line",1,[187,192,194],{"class":193},"svObZ","terraform",[187,196,198],{"class":197},"s95oV"," {\n",[187,200,202,205,209],{"class":189,"line":201},2,[187,203,204],{"class":197},"  required_version",[187,206,208],{"class":207},"snl16"," =",[187,210,212],{"class":211},"sU2Wk"," \">= 1.17.0\"\n",[187,214,216],{"class":189,"line":215},3,[187,217,219],{"emptyLinePlaceholder":218},true,"\n",[187,221,223,226],{"class":189,"line":222},4,[187,224,225],{"class":193},"  required_providers",[187,227,198],{"class":197},[187,229,231,234,236],{"class":189,"line":230},5,[187,232,233],{"class":197},"    azurerm",[187,235,208],{"class":207},[187,237,198],{"class":197},[187,239,241,244,247],{"class":189,"line":240},6,[187,242,243],{"class":197},"      source  ",[187,245,246],{"class":207},"=",[187,248,249],{"class":211}," \"hashicorp\u002Fazurerm\"\n",[187,251,253,256,258],{"class":189,"line":252},7,[187,254,255],{"class":197},"      version ",[187,257,246],{"class":207},[187,259,260],{"class":211}," \"~>5.12\"\n",[187,262,264],{"class":189,"line":263},8,[187,265,266],{"class":197},"    }\n",[187,268,270],{"class":189,"line":269},9,[187,271,272],{"class":197},"  }\n",[187,274,276],{"class":189,"line":275},10,[187,277,219],{"emptyLinePlaceholder":218},[187,279,281,284,288],{"class":189,"line":280},11,[187,282,283],{"class":193},"  backend",[187,285,287],{"class":286},"sDLfK"," \"azurerm\"",[187,289,198],{"class":197},[187,291,293,296,299],{"class":189,"line":292},12,[187,294,295],{"class":197},"      resource_group_name",[187,297,298],{"class":207},"  =",[187,300,301],{"class":211}," \"WeburzRG\"\n",[187,303,305,308,310],{"class":189,"line":304},13,[187,306,307],{"class":197},"      storage_account_name",[187,309,208],{"class":207},[187,311,312],{"class":211}," \"terraform-state\"\n",[187,314,316,319,322],{"class":189,"line":315},14,[187,317,318],{"class":197},"      container_name",[187,320,321],{"class":207},"       =",[187,323,324],{"class":211}," \"tfstate-production\"\n",[187,326,328,331,334],{"class":189,"line":327},15,[187,329,330],{"class":197},"      key",[187,332,333],{"class":207},"                  =",[187,335,336],{"class":211}," \"terraform.production.tfstate\"\n",[187,338,340],{"class":189,"line":339},16,[187,341,272],{"class":197},[187,343,345],{"class":189,"line":344},17,[187,346,347],{"class":197},"}\n",[187,349,351],{"class":189,"line":350},18,[187,352,219],{"emptyLinePlaceholder":218},[187,354,356,359,361],{"class":189,"line":355},19,[187,357,358],{"class":193},"provider",[187,360,287],{"class":286},[187,362,198],{"class":197},[187,364,366,369],{"class":189,"line":365},20,[187,367,368],{"class":193},"  features",[187,370,371],{"class":197}," {}\n",[187,373,375],{"class":189,"line":374},21,[187,376,347],{"class":197},[34,378,379,380,385,386,389],{},"After authenticating yourself to Azure\n(",[38,381,384],{"href":382,"rel":383},"https:\u002F\u002Fregistry.terraform.io\u002Fproviders\u002Fhashicorp\u002Fazurerm\u002Flatest\u002Fdocs\u002Fguides\u002Fazure_cli",[42],"ideally using Azure CLI","),\ncreating the container resources and setting up the container access key, you\nshould run ",[61,387,388],{},"terraform init"," in your local environment. Terraform will now\ninstall all the necessary dependencies and then write the state to the remote\nlocation for further usage.",[34,391,392,393,395],{},"With the state file secured and usable, the developers can run ",[61,394,63],{},"\nany number of times and anywhere while safely managing the production\ninfrastructure. Now we can design the SOP for the pipeline from publishing the\nIaC from the commit to peer review and approval then followed by the actual\ninfrastructure provisioning in the next section.",[75,397,399],{"id":398},"designing-the-two-phase-pipeline-architecture","Designing the Two-Phase Pipeline Architecture",[34,401,402],{},"At Weburz our SOP for managing the infrastructure using Terraform is as follows:",[156,404,405,414,417,424],{},[159,406,407,408,413],{},"Developer creates a draft PR on ",[38,409,412],{"href":410,"rel":411},"https:\u002F\u002Fgithub.com",[42],"GitHub"," and requesting\nthe codeowner for review.",[159,415,416],{},"The PR goes through an automated static analysis and security scan before\nbeing reviewed by a codeowner.",[159,418,419,420,423],{},"The CI environment executes a ",[61,421,422],{},"terraform plan"," command and writes the output\nto a PR comment for the codeowner's review.",[159,425,426,427,430,431,434,435,438],{},"The codeowner requests for changes and\u002For fixes (",[101,428,429],{},"if necessary",") then merges\nthe PR to the default branch. This triggers another CI\u002FCD workflow to execute\nthe ",[61,432,433],{},"terraform plan ..."," and ",[61,436,437],{},"terraform apply ..."," commands to update the\nproduction environment safely.",[34,440,441],{},"For quick reference, here's a visual representation of our Terraform workflow.\nIt has worked out perfectly well for us and we hope it might help you in one way\nor the other as well;",[34,443,444],{},[38,445,448],{"href":446,"rel":447},"https:\u002F\u002Fmermaid.live\u002Fedit#pako:eNqNU02P2jAU_CuWD1UrQQosWSCHlWjYAocuCNpLlcN6k0ewmtjpi82SIv57n7N8rqjUHOLYnjczb-zseKwT4AEv4bcFFcNIihRFHmGkGD3CGq1s_gJ4WomNRjaCDROlGyDTxXm3EGhkLAuhDBtPHGQszcS-3NoPp26f3o9qI1GrHJS5VglnNYIc6ld1W2SOOnEgN9rYSK3e0x2ryGvz4WE8CViIIAywEYoVESzYB7Zw3ZeGxo2E12PFeEIF4TRg31GmKSAbUho5lSZsaQRZYEMlsqqUJVEsIbYoTcWWsThphtMDw-OWtknz2QCSrMacFZlQz1dA522uycZ7FNPWFNbUfS5YrPPLqN5czgL2pI1cVee4GBFcdHSKNqPo10KlULLP7Kvc0uj6lwjJEVNbmhEvhRac0jlUXYLOoc5tua613ygvQWQMmN6Qo_Gk4dJYQBOtKm-kOM_O4YFKLn3XfpzSsCiQ2Aj9DTAFF4nR5GQlLLX2BYWK1_88wnDE5rKATCr430NinuddH5S7bDfhoiiyqsazjz-KhK7K1c1cihVk1Sfe4CnKhAcGLTR4DpgLN-U7pxFxs4YcIh7QJ52J3TYTgb-asc40RjxSe6qnu_9T6_xIgdqmax6sRFbSzNbKhz_5tIqUJ2CorTI86NzXHDzY8S0P7jotrz3oDPx-t9fv3_v-XYNXPOh2vF6n5fcHnX6v3e72fH_f4H9q2ZbX7_n7v-FPYKI",[42],[449,450],"img",{"alt":183,"src":451},"https:\u002F\u002Fmermaid.ink\u002Fimg\u002Fpako:eNqNU02P2jAU_CuWD1UrAQ0LAZLDSjRsgUMXBO2l4rDe5BGsJnb6YrOkiP_e5-zytaJSfYjjeN7MvLGz57FOgIe8hN8WVAwjKVIU-QpXitEQ1mhl82fA05fYaGQj2DJRugkyXZx3C4FGxrIQyrDxxEHG0kzs8639aOr26fmgthK1ykGZa5VoViPIoX5Rt0XmqBMHcrONjdTqPd2xirw27-_Hk5BFCMIAG6FYE8GCfWAL131paN5KeDlWjCdUEE1D9h1lmgKyIaWRU2nClkaQBTZUIqtKWRLFEmKL0lRsGYuTZjR9Y3jY0TZpPhlAktWYsyIT6ukK6LzNNdl4j2LamsKaus8Fi3V-GdWry1nIHrWR6-ocFyOCi45O0WYU_UaoFEr2mX2VO5pd_xIhOWJqSzPipdDCUzpvVZegc6hzW25q7VfKSxAZA6a35Gg8abg0FtBEq8obKc6zc3igkkvftR-nNCwKJDZCfwNMwUViNDlZC0utfUGh4s0_jzAasbksIJMK_veQWKvVuj4od9luwkVRZFWNZx9_FAldlaubuRRryKpPvMFTlAkPDVpo8BwwF27J905jxc0GcljxkF7pTOyumQj81Yx1pnHFV-pA9XT3f2qdHylQ23TDw7XISlrZWvntTz59RcoTMNJWGR7eBTUHD_d8x8P2XdDqBp32oOcPun7H6_gNXvHQ77Z6ncDvdPzA9z2v1-4dGvxPLeu1Bv1uQKMf-G0v8Lz-4S_ZBmNT?type=png",[34,453,454],{},"When a developer submits a pull request (PR) against the default branch on\nGitHub, it triggers a CI workflow with these objectives:",[156,456,457,470,480],{},[159,458,459,462,463,466,467,173],{},[44,460,461],{},"Lint & Format Check:"," Enforce code styling using ",[61,464,465],{},"terraform fmt -check","\nand ",[61,468,469],{},"terraform validate",[159,471,472,475,476,479],{},[44,473,474],{},"Security Scanning:"," Run static analysis tools like ",[61,477,478],{},"checkov"," to flag\nmisconfigurations (for example, publicly accessible storage buckets or\nwildcard IAM permissions).",[159,481,482,485,486,488,489,492],{},[44,483,484],{},"Generate Plan:"," Execute ",[61,487,422],{}," against the target cloud provider\nand then automatically attach the plan output as a comment on the PR. This\ngives peer reviewers total visibility into added, modified, or destroyed\nresources ",[101,490,491],{},"before"," any code merges.",[34,494,495,496,499,500,503],{},"Once the PR is approved and merged into the main branch, an automated deployment\npipeline is triggered. This workflow checks out the updated default branch, runs\n",[61,497,498],{},"terraform plan \"main.tfplan\""," to regenerate the plan and then provisions the\ninfrastructure by running ",[61,501,502],{},"terraform apply \"main.tfplan\""," (if any changes\nexist).",[75,505,507],{"id":506},"securing-pipelines-with-oidc-openid-connect","Securing Pipelines with OIDC (OpenID Connect)",[34,509,510,511,514,515,518],{},"Historically, automated pipelines relied on long-lived API keys stored as secret\nenvironment variables (for example, ",[61,512,513],{},"AWS_ACCESS_KEY_ID"," and\n",[61,516,517],{},"AWS_SECRET_ACCESS_KEY","). These keys require periodic rotation and create severe\nvulnerability risks if leaked.",[34,520,521,522,527,528,535],{},"At Weburz, we use modern CI\u002FCD platforms (like\n",[38,523,526],{"href":524,"rel":525},"https:\u002F\u002Fgithub.com\u002Ffeatures\u002Factions",[42],"GitHub Actions",") which supports\n",[38,529,532],{"href":530,"rel":531},"https:\u002F\u002Fopenid.net",[42],[44,533,534],{},"OpenID Connect (OIDC)",", enabling secure and keyless\nauthentication in our CI\u002FCD environments.",[34,537,538],{},"While providing a detailed explanation of the inner workings of OIDC is\nout-of-scope of this article, here's a brief description of we use it to\nauthenticate to our cloud service providers.",[156,540,541,550,559],{},[159,542,543,544,549],{},"The CI runner requests a short-lived ",[38,545,548],{"href":546,"rel":547},"https:\u002F\u002Fjwt.io",[42],"JSON Web Token (JWT)","\nfrom the CI provider.",[159,551,552,553,558],{},"The runner presents this token to our cloud provider’s Identity Provider (for\nexample, ",[38,554,557],{"href":555,"rel":556},"https:\u002F\u002Fentra.microsoft.com",[42],"Microsoft Entra ID",").",[159,560,561,562,565],{},"The cloud provider verifies the identity token and exchanges it for\n",[44,563,564],{},"short-lived, temporary access credentials"," strictly scoped to the duration\nof the pipeline job.",[34,567,568],{},"For reference, here's a sample GitHub Actions workflow which uses OIDC to\nauthenticate to Microsoft Entra ID before executing the Terraform commands and\nprovisioning the infrastructure on Microsoft Azure.",[178,570,574],{"className":571,"code":572,"language":573,"meta":183,"style":183},"language-yaml shiki shiki-themes github-dark","name: \"Terraform Infrastructure Pipeline\"\n\non:\n  push:\n    branches: [\"main\"]\n\npermissions:\n  id-token: write\n  contents: read\n  pull-requests: write\n\njobs:\n  terraform:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions\u002Fcheckout@v4\n\n      - name: Configure Azure Credentials via OIDC\n        uses: https:\u002F\u002Fgithub.com\u002Fazure\u002Flogin@v3\n        with:\n          client-id: ${{ secrets.AZURE_CLIENT_ID }}\n          tenant-id: ${{ secrets.AZURE_TENANT_ID }}\n          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n\n      - name: Setup Terraform\n        uses: hashicorp\u002Fsetup-terraform@v3\n\n      - name: Terraform Init\n        run: terraform init\n\n      - name: Terraform Plan\n        run: terraform plan \"main.tfplan\" -no-color\n\n      - name: Terraform Apply\n        run: terraform apply \"main.tfplan\"\n","yaml",[61,575,576,588,592,600,607,621,625,632,642,652,661,665,672,679,689,696,708,718,722,733,742,749,760,771,782,787,799,809,814,826,837,842,854,864,869,881],{"__ignoreMap":183},[187,577,578,582,585],{"class":189,"line":190},[187,579,581],{"class":580},"s4JwU","name",[187,583,584],{"class":197},": ",[187,586,587],{"class":211},"\"Terraform Infrastructure Pipeline\"\n",[187,589,590],{"class":189,"line":201},[187,591,219],{"emptyLinePlaceholder":218},[187,593,594,597],{"class":189,"line":215},[187,595,596],{"class":286},"on",[187,598,599],{"class":197},":\n",[187,601,602,605],{"class":189,"line":222},[187,603,604],{"class":580},"  push",[187,606,599],{"class":197},[187,608,609,612,615,618],{"class":189,"line":230},[187,610,611],{"class":580},"    branches",[187,613,614],{"class":197},": [",[187,616,617],{"class":211},"\"main\"",[187,619,620],{"class":197},"]\n",[187,622,623],{"class":189,"line":240},[187,624,219],{"emptyLinePlaceholder":218},[187,626,627,630],{"class":189,"line":252},[187,628,629],{"class":580},"permissions",[187,631,599],{"class":197},[187,633,634,637,639],{"class":189,"line":263},[187,635,636],{"class":580},"  id-token",[187,638,584],{"class":197},[187,640,641],{"class":211},"write\n",[187,643,644,647,649],{"class":189,"line":269},[187,645,646],{"class":580},"  contents",[187,648,584],{"class":197},[187,650,651],{"class":211},"read\n",[187,653,654,657,659],{"class":189,"line":275},[187,655,656],{"class":580},"  pull-requests",[187,658,584],{"class":197},[187,660,641],{"class":211},[187,662,663],{"class":189,"line":280},[187,664,219],{"emptyLinePlaceholder":218},[187,666,667,670],{"class":189,"line":292},[187,668,669],{"class":580},"jobs",[187,671,599],{"class":197},[187,673,674,677],{"class":189,"line":304},[187,675,676],{"class":580},"  terraform",[187,678,599],{"class":197},[187,680,681,684,686],{"class":189,"line":315},[187,682,683],{"class":580},"    runs-on",[187,685,584],{"class":197},[187,687,688],{"class":211},"ubuntu-latest\n",[187,690,691,694],{"class":189,"line":327},[187,692,693],{"class":580},"    steps",[187,695,599],{"class":197},[187,697,698,701,703,705],{"class":189,"line":339},[187,699,700],{"class":197},"      - ",[187,702,581],{"class":580},[187,704,584],{"class":197},[187,706,707],{"class":211},"Checkout Code\n",[187,709,710,713,715],{"class":189,"line":344},[187,711,712],{"class":580},"        uses",[187,714,584],{"class":197},[187,716,717],{"class":211},"actions\u002Fcheckout@v4\n",[187,719,720],{"class":189,"line":350},[187,721,219],{"emptyLinePlaceholder":218},[187,723,724,726,728,730],{"class":189,"line":355},[187,725,700],{"class":197},[187,727,581],{"class":580},[187,729,584],{"class":197},[187,731,732],{"class":211},"Configure Azure Credentials via OIDC\n",[187,734,735,737,739],{"class":189,"line":365},[187,736,712],{"class":580},[187,738,584],{"class":197},[187,740,741],{"class":211},"https:\u002F\u002Fgithub.com\u002Fazure\u002Flogin@v3\n",[187,743,744,747],{"class":189,"line":374},[187,745,746],{"class":580},"        with",[187,748,599],{"class":197},[187,750,752,755,757],{"class":189,"line":751},22,[187,753,754],{"class":580},"          client-id",[187,756,584],{"class":197},[187,758,759],{"class":211},"${{ secrets.AZURE_CLIENT_ID }}\n",[187,761,763,766,768],{"class":189,"line":762},23,[187,764,765],{"class":580},"          tenant-id",[187,767,584],{"class":197},[187,769,770],{"class":211},"${{ secrets.AZURE_TENANT_ID }}\n",[187,772,774,777,779],{"class":189,"line":773},24,[187,775,776],{"class":580},"          subscription-id",[187,778,584],{"class":197},[187,780,781],{"class":211},"${{ secrets.AZURE_SUBSCRIPTION_ID }}\n",[187,783,785],{"class":189,"line":784},25,[187,786,219],{"emptyLinePlaceholder":218},[187,788,790,792,794,796],{"class":189,"line":789},26,[187,791,700],{"class":197},[187,793,581],{"class":580},[187,795,584],{"class":197},[187,797,798],{"class":211},"Setup Terraform\n",[187,800,802,804,806],{"class":189,"line":801},27,[187,803,712],{"class":580},[187,805,584],{"class":197},[187,807,808],{"class":211},"hashicorp\u002Fsetup-terraform@v3\n",[187,810,812],{"class":189,"line":811},28,[187,813,219],{"emptyLinePlaceholder":218},[187,815,817,819,821,823],{"class":189,"line":816},29,[187,818,700],{"class":197},[187,820,581],{"class":580},[187,822,584],{"class":197},[187,824,825],{"class":211},"Terraform Init\n",[187,827,829,832,834],{"class":189,"line":828},30,[187,830,831],{"class":580},"        run",[187,833,584],{"class":197},[187,835,836],{"class":211},"terraform init\n",[187,838,840],{"class":189,"line":839},31,[187,841,219],{"emptyLinePlaceholder":218},[187,843,845,847,849,851],{"class":189,"line":844},32,[187,846,700],{"class":197},[187,848,581],{"class":580},[187,850,584],{"class":197},[187,852,853],{"class":211},"Terraform Plan\n",[187,855,857,859,861],{"class":189,"line":856},33,[187,858,831],{"class":580},[187,860,584],{"class":197},[187,862,863],{"class":211},"terraform plan \"main.tfplan\" -no-color\n",[187,865,867],{"class":189,"line":866},34,[187,868,219],{"emptyLinePlaceholder":218},[187,870,872,874,876,878],{"class":189,"line":871},35,[187,873,700],{"class":197},[187,875,581],{"class":580},[187,877,584],{"class":197},[187,879,880],{"class":211},"Terraform Apply\n",[187,882,884,886,888],{"class":189,"line":883},36,[187,885,831],{"class":580},[187,887,584],{"class":197},[187,889,890],{"class":211},"terraform apply \"main.tfplan\"\n",[34,892,893,896,897,173],{},[44,894,895],{},"NOTE",": For Azure to trust GitHub's OIDC token, you will need to perform some\nmanual one-time configurations though. You will find the details instructions\nhere -\n",[38,898,901],{"href":899,"rel":900},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions\u002Fhow-tos\u002Fsecure-your-work\u002Fsecurity-harden-deployments\u002Foidc-in-azure",[42],"Configuring OpenID Connect in Azure ",[75,903,905],{"id":904},"continuous-drift-detection","Continuous Drift Detection",[34,907,908,909,912],{},"Even with automated pipelines, out-of-band changes can happen; for example, a\nteam member might modify a security group rule via the cloud console during an\noperational emergency. This creates ",[44,910,911],{},"infrastructure drift",", where real-world\nresources deviate from the defined codebase. Hence it's important to handle it\nas early in the pipeline as possible.",[34,914,915],{},"Although we've not set up early detection and warning systems for infrastructure\ndrift yet, we've it in our roadmap and here's how we plan on tackling it:",[156,917,918,936,968],{},[159,919,920,921,924,925,932,933,173],{},"Set up a ",[44,922,923],{},"scheduled nightly pipeline"," (for example, GitHub Action's\n",[38,926,929],{"href":927,"rel":928},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions\u002Freference\u002Fworkflows-and-actions\u002Fevents-that-trigger-workflows#schedule",[42],[61,930,931],{},"schedule","\nevent) which runs ",[61,934,935],{},"terraform plan -detailed-exitcode",[159,937,938,939,942,943],{},"The ",[61,940,941],{},"-detailed-exitcode"," flag returns:\n",[944,945,946,952,958],"ul",{},[159,947,948,951],{},[61,949,950],{},"0"," when the pipeline succeeded with no changes.",[159,953,954,957],{},[61,955,956],{},"1"," when an error occurred in the pipeline.",[159,959,960,963,964,967],{},[61,961,962],{},"2"," when the pipeline succeeded, but ",[44,965,966],{},"drift detected"," (pending changes\nexist).",[159,969,970,971,973],{},"If the job returns code ",[61,972,962],{},", we may send an alert via mail or create an issue\nthread in our incident tracking system.",[34,975,976],{},"We hope, this will ensure our infrastructure stays aligned with our code\nrepository without relying on manual audits from time to time.",[34,978,979,980,984],{},"We wrote about infrastructure drift in more details here -\n",[38,981,983],{"href":982},"\u002Fblog\u002Fmanaging-infrastructure-drift","Managing Infrastructure Drift: How Packer, Terraform, and Ansible Keep Enterprise Environments Compliant",",\nso give it a read if you want to learn more.",[75,986,988],{"id":987},"conclusion-next-steps","Conclusion & Next Steps",[34,990,991],{},"Automating Terraform through a CI\u002FCD pipeline transforms infrastructure\nmanagement from a high-risk manual task into a secure, predictable, and fully\nauditable process.",[34,993,994],{},"To recap the roadmap for production-ready IaC automation:",[944,996,997,1003,1012,1018],{},[159,998,999,1002],{},[44,1000,1001],{},"Centralize state storage"," with remote backends and state locking.",[159,1004,1005,1008,1009,1011],{},[44,1006,1007],{},"Enforce PR-driven reviews"," where ",[61,1010,422],{}," is transparently\ndisplayed before code merges.",[159,1013,1014,1017],{},[44,1015,1016],{},"Adopt keyless authentication"," using OIDC to eliminate long-lived cloud\ncredentials.",[159,1019,1020,1023],{},[44,1021,1022],{},"Implement automated drift detection"," to ensure code remains the definitive\nsource of truth.",[34,1025,1026],{},"By treating our infrastructure with the same rigor as software development, our\nteams deliver cloud resources faster while maintaining absolute confidence in\nproduction stability.",[1028,1029,1030],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":183,"searchDepth":201,"depth":201,"links":1032},[1033,1034,1035,1036,1037,1038],{"id":77,"depth":201,"text":78},{"id":139,"depth":201,"text":140},{"id":398,"depth":201,"text":399},{"id":506,"depth":201,"text":507},{"id":904,"depth":201,"text":905},{"id":987,"depth":201,"text":988},"Infrastructure","\u002Fblog\u002Ftaking-terraform-to-production.webp","2026-09-24","Learn how to automate Terraform with CI\u002FCD pipelines. Discover OIDC authentication, automated PR plans, state locking, and drift detection for production IaC.","md",{},"\u002Fblog\u002Ftaking-terraform-to-production",{"title":28,"description":1042},"blog\u002Ftaking-terraform-to-production","PtUPDmCnHboqEpXnn1Lxt5wqpyJSsXYdtPs1r4U2XAQ",[1050,7],{"title":1051,"path":1052,"stem":1053,"description":1054,"date":1055,"children":-1},"Securely Self-Hosting PostgreSQL: Configuration, Backups, and Best Practices","\u002Fblog\u002Fsecurely-self-hosting-postgresql","blog\u002Fsecurely-self-hosting-postgresql","Take full control of your data by setting up your own self-hosted PostgreSQL database. Discover how to configure, secure, and manage production-ready instances efficiently.","2026-08-10",[1057,1061,1065,1067,1068],{"path":1058,"title":1059,"date":1060,"category":1039},"\u002Fblog\u002Fautomated-restic-backups","How We Automated Secure, Deduplicated Backups with Restic","2026-09-29",{"path":1062,"title":1063,"date":1064,"category":1039},"\u002Fblog\u002Fgolden-images-with-packer","Golden Images at Scale: Building Secure, Multi-Cloud VMs with Packer","2026-09-11",{"path":982,"title":983,"date":1066,"category":1039},"2026-09-03",{"path":1052,"title":1051,"date":1055,"category":1039},{"path":1069,"title":1070,"date":1071,"category":1072},"\u002Fblog\u002Fhello-world","Hello, world","2026-05-22","People",1793622283112]