{"id":10971,"date":"2022-06-28T18:50:10","date_gmt":"2022-06-28T13:20:10","guid":{"rendered":"https:\/\/opstree.com\/blog\/\/?p=10971"},"modified":"2022-07-12T18:15:23","modified_gmt":"2022-07-12T12:45:23","slug":"terraform-ci-cd-with-azure-devops","status":"publish","type":"post","link":"https:\/\/opstree.com\/blog\/2022\/06\/28\/terraform-ci-cd-with-azure-devops\/","title":{"rendered":"Terraform CI-CD With Azure DevOps"},"content":{"rendered":"\n<p class=\"has-text-align-justify\">Let&#8217;s consider a scenario in which you are deploying your infrastructure using a Terraform code (infrastructure-as-code) which is stored in a remote git repository. Now working in an organization you need to make sure that all your deployments are always tracked without an exception, an add-on to that whether your Terraform code is following your security and compliance policies or not. Or maybe what is the monthly cost that you can expect with that infra and whether it lies under your budget or not. You may also want to take note that all your resources are being created in the same region&#8230; etc&#8230; etc.<\/p>\n\n\n\n<p class=\"has-text-align-justify\">Sounds magical right !!! We all know that these concerns are very important when you&#8217;re looking for a highly consistent, fully tracked, and automated approach. That&#8217;s why in this article we are going to look for a simple step-by-step way to automate and streamline our Terraform code using Azure DevOps (ADO).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Soo&#8230; Let&#8217;s Get Started !!!<\/h2>\n\n\n\n<!--more-->\n\n\n\n<p>First of all, we need to know what is Terraform &amp; Azure DevOps.<\/p>\n\n\n\n<p class=\"has-text-align-justify\"><strong>Talking About Terraform<\/strong>: HashiCorp Terraform is an infrastructure as code tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share. You can then use a consistent workflow to provision and manage all of your infrastructure throughout its lifecycle. Terraform can manage low-level components like compute, storage, and networking resources, as well as high-level components like DNS entries and SaaS features.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-2.png?w=1024\" alt=\"\" class=\"wp-image-10975\" width=\"629\" height=\"552\" \/><figcaption class=\"wp-element-caption\">Terraform Workflow<\/figcaption><\/figure>\n\n\n\n<p>If you want to learn more about terraform you can click <a rel=\"noreferrer noopener\" href=\"https:\/\/www.terraform.io\/intro\" target=\"_blank\">here<\/a>.<\/p>\n\n\n\n<p class=\"has-text-align-justify\"><strong>Talking about Azure DevOps<\/strong>: Azure DevOps provides developer services for allowing teams to plan work, collaborate on code development, and build and deploy applications. Azure DevOps supports a collaborative culture and set of processes that bring together developers, project managers, and contributors to develop software. It allows organizations to create and improve products at a faster pace than they can with traditional software development approaches.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img decoding=\"async\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-3.png?w=1024\" alt=\"\" class=\"wp-image-10978\" width=\"800\" \/><figcaption class=\"wp-element-caption\">DevOps lifecycle in Azure Devops<\/figcaption><\/figure>\n\n\n\n<p>If you want to learn more about Azure DevOps click <a rel=\"noreferrer noopener\" href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/user-guide\/what-is-azure-devops?view=azure-devops\" target=\"_blank\">here<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Azure Pipeline For Terraform<\/h2>\n\n\n\n<p class=\"has-text-align-justify\">Okay !!! I know that&#8217;s a lot of information. Now let&#8217;s get back to the point&#8230;  the  question we all started with, &#8220;How the hell are we going to achieve all this and that too in a single pipeline?&#8221;  Well, the answer is very simple, by using different tools dedicated to a particular task.<\/p>\n\n\n\n<p class=\"has-text-align-center\"><img decoding=\"async\" src=\"https:\/\/lh6.googleusercontent.com\/1llIZz_ka-zdnmK9gA82mczGrsMpMclXPcUFKjHupnXgdZPJ6gn3XilaOuEA68z-OmiciuAiVyA6UZ6vZWSaBXX8eLIHHYBdvA1RIvlkhu7pqQc6MmL9xTrAvFHM_gtWfBYUlI388P8pqiNUGg4v\" width=\"928px;\" height=\"354px;\"><\/p>\n\n\n\n<p>This is the broad architecture of the pipeline that we are going to create.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pre-requisites:<\/h3>\n\n\n\n<p class=\"has-text-align-justify\">No matter whether we are deploying our infrastructure into Azure Cloud Services or Amazon Web Services (AWS). All we need are the following checklist:<\/p>\n\n\n\n<ul><li>Active Cloud Service (Azure\/AWS)<\/li><li>Azure DevOps Account<\/li><li>Terraform Code to deploy<\/li><li>A Linux machine (VM or EC2) for agent pool<\/li><li>Docker<\/li><li>Storage Account (Azure Blob Container or AWS S3)<\/li><\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Tools used:<\/h3>\n\n\n\n<ol><li><strong>TFsec<\/strong><\/li><\/ol>\n\n\n\n<p><strong><em>TFsec <\/em><\/strong>is a static analysis security scanner for your Terraform code.<\/p>\n\n\n\n<p><strong><em>TFsec <\/em><\/strong>takes a developer-first approach to scan your Terraform templates; using static analysis and deep integration with the official HCL parser ensures that security issues can be detected before your infrastructure changes take effect.<\/p>\n\n\n\n<ol start=\"2\"><li><strong>TFlint<\/strong><\/li><\/ol>\n\n\n\n<p><strong><em>TFlint <\/em><\/strong>is a framework and each feature is provided by plugins, the key features are as follows:<\/p>\n\n\n\n<ul><li>Find possible errors (like illegal instance types) for Major Cloud providers (AWS\/Azure\/GCP).<\/li><li>Warn about deprecated syntax and unused declarations.<\/li><li>Enforce best practices and naming conventions.<\/li><\/ul>\n\n\n\n<ol start=\"3\"><li><strong>InfraCost<\/strong><\/li><\/ol>\n\n\n\n<p class=\"has-text-align-justify\">Infracost shows cloud cost estimates for Terraform. It lets DevOps, SRE, and engineers see a cost breakdown and understand costs before making changes, either in the terminal or in pull requests. It can also show us the difference between our present state and desired state.<\/p>\n\n\n\n<p>Rest all the requirements we can fulfill within our pipeline itself<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Build Pipeline steps<\/h2>\n\n\n\n<p class=\"has-text-align-justify\">Assuming that we have already configured an agent in the agent pool which is going to assist us in executing all the commands to achieve our pipeline goals. Also, try importing your terraform code into your Azure Repos as it&#8217;ll benefit you in a very unique way that we&#8217;ll find out further in this article in the bonus section.<\/p>\n\n\n\n<p>If you what to know how to configure an agent you can click <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-02:-Configuring-Your-First-Self-Hosted-Agent\" target=\"_blank\">here<\/a> <\/p>\n\n\n\n<p>You can click <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-01:-Create-A-New-Project-in-Azure-DevOps-&amp;-Import-a-New-Repository\" target=\"_blank\">here<\/a> to follow the steps to import a repo.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Part 1: Installing Dependencies<\/h4>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"386\" height=\"352\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-4.png?w=386\" alt=\"\" class=\"wp-image-10981\" \/><figcaption class=\"wp-element-caption\">Steps to install dependencies<\/figcaption><\/figure>\n\n\n\n<p>All we need to do in this part is to download and install all the dependencies required in your pipeline.<\/p>\n\n\n\n<p>Here we will use the docker images for different tasks, eg:<\/p>\n\n\n\n<ul><li>Terraform security compliance: <strong><a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/aquasecurity\/tfsec\" target=\"_blank\">Tfsec<\/a><\/strong><\/li><li>Terraform Linting: <strong><a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/terraform-linters\/tflint\" target=\"_blank\">Tflint<\/a><\/strong><\/li><li>Infrastructure Cost Estimation &amp; Cost Difference: <strong><a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/infracost\/infracost\" target=\"_blank\">Infracost<\/a><\/strong><\/li><\/ul>\n\n\n\n<p>Alongside you can try this YAML format of the pipeline.<\/p>\n\n\n\n<pre class=\"wp-block-verse has-light-gray-color has-dark-gray-background-color has-text-color has-background\">  - task: Bash@3\n    displayName: Install Docker\n    enabled: False\n    inputs:\n      targetType: inline\n      script: &gt;-\n        sudo apt update\n        sudo apt install apt-transport-https ca-certificates curl software-                           properties-common -y\n        curl -fsSL https:\/\/download.docker.com\/linux\/ubuntu\/gpg | sudo apt-key add -\n        sudo add-apt-repository \"deb [arch=amd64] https:\/\/download.docker.com\/linux\/ubuntu bionic stable\n        sudo add-apt-repository \"deb [arch=amd64] https:\/\/download.docker.com\/linux\/ubuntu bionic stable\"\n        sudo apt update\n        apt-cache policy docker-ce\n        sudo apt install docker-ce -y\n        sudo systemctl start docker\n\n  - task: Bash@3\n    displayName: Install Azure CLI\n    enabled: False\n    inputs:\n      targetType: inline\n      script: \"sudo apt-get update \\nsudo apt-get install ca-certificates curl apt-transport-https lsb-release gnupg -y\\ncurl -sL https:\/\/packages.microsoft.com\/keys\/microsoft.asc |\\n    gpg --dearmor |\\n    sudo tee \/etc\/apt\/trusted.gpg.d\/microsoft.gpg &gt; \/dev\/null\\nAZ_REPO=$(lsb_release -cs)\\necho \\\"deb [arch=amd64] https:\/\/packages.microsoft.com\/repos\/azure-cli\/ $AZ_REPO main\\\" |\\n    sudo tee \/etc\/apt\/sources.list.d\/azure-cli.list\\nsudo apt-get update\\nsudo apt-get install azure-cli -y\\nsudo apt install unzip -y\"\n\n  - task: TerraformInstaller@0\n    displayName: Install Terraform 1.1.8\n    inputs:\n      terraformVersion: 1.1.8\n\n  - task: Bash@3\n    displayName: Pulling Required Docker Images\n    enabled: False\n    inputs:\n      targetType: inline\n      script: &gt;\n        # TFSEC\n        sudo docker pull tfsec\/tfsec:v1.13.2-arm64v8\n        # TFLINT\n        sudo docker pull ghcr.io\/terraform-linters\/tflint:v0.35.0\n        # InfraCost\n        sudo docker pull infracost\/infracost:0.<\/pre>\n\n\n\n<p>For more detailed info click <a href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-04:-Installing-Dependencies\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Part 2: Terraform Initializing &amp; Planning<\/h4>\n\n\n\n<p>This is one of the most simple and well-known step of the whole pipeline<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"470\" height=\"203\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-5.png?w=470\" alt=\"\" class=\"wp-image-10986\" \/><figcaption class=\"wp-element-caption\">Steps for Terraform init, validate &amp; plan<\/figcaption><\/figure>\n\n\n\n<p>In this part, we&#8217;ll initialize, validate and plan our terraform code and store the output into a file using <code>-out=plan.out<\/code> flag.<\/p>\n\n\n\n<pre class=\"wp-block-verse has-light-gray-color has-dark-gray-background-color has-text-color has-background\">  - task: TerraformTaskV2@2\n    displayName: 'Terraform : INIT'\n    inputs:\n      backendServiceArm: a575**********************4bcc71\n      backendAzureRmResourceGroupName: ADOagent_rg\n      backendAzureRmStorageAccountName: terrastoragestatesreport\n      backendAzureRmContainerName: statefile\n      backendAzureRmKey: terraform.tfstate\n  - task: TerraformTaskV2@2\n    displayName: 'Terraform : VALIDATE'\n    inputs:\n      command: validate\n  - task: TerraformTaskV2@2\n    displayName: 'Terraform : PLAN ( For Cost Optimization )'\n    inputs:\n      command: plan\n      commandOptions: -lock=false -out=plan.out\n      environmentServiceNameAzureRM: a575**********************4bcc71<\/pre>\n\n\n\n<p>For more detailed steps click <a href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-05:-Terraform-Initializing,-Validating-&amp;-Planning\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Part 3: Heart of Our Pipeline: <strong>Terraform Security Compliance, Linting, Cost Estimation &amp; Cost Difference<\/strong><\/h4>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"335\" height=\"280\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-6.png?w=335\" alt=\"\" class=\"wp-image-10988\" \/><figcaption class=\"wp-element-caption\">Steps to secure, lint and estimate the cost of terraform code<\/figcaption><\/figure>\n\n\n\n<p>Now using the above-mentioned tools we can achieve these tasks <strong>Terraform Security Compliance, Linting, Cost Estimation &amp; Cost Difference<\/strong> in a bash task. <\/p>\n\n\n\n<p>Though you don&#8217;t need custom settings while linting or cost calculating but you can definitely use a custom checks file for Terraform Compliance step.<\/p>\n\n\n\n<p>A custom check file of Tfsec will look like this:<\/p>\n\n\n\n<pre class=\"wp-block-verse has-light-gray-color has-dark-gray-background-color has-text-color has-background\">---\nchecks:\n  - code: CUS001\n    description: Custom check to ensure the Name tag is applied to Resources Group Module \n    impact:  By not having Name Tag we can't keep track of our Resources\n    requiredTypes:\n      - module\n    requiredLabels:\n      - resource_group\n    severity: MEDIUM\n    matchSpec:\n     name: tag_map\n     action: contains\n     value: Name\n    errorMessage: The required Name tag was missing\n\n  - code: CUS002\n    description: Custom check to ensure the Name tag is applied to Resources Group Module\n    impact:  By not having Environment Tag we can't keep track of our Resources\n    requiredTypes:\n      - module\n    requiredLabels:\n      - resource_group\n    severity: CRITICAL\n    matchSpec:\n      name: tag_map\n      action: contains\n      value: Environment\n    errorMessage: The required Environment tag was missing\n\n  - code: CUS003\n    description: Custom check to ensure Resource Group is going to be created in Australia East region\n    impact:  By not having our resource in Australia East we might get some latency\n    requiredTypes:\n      - module\n    requiredLabels:\n      - resource_group\n    severity: MEDIUM\n    matchSpec:\n     name: resource_group_location\n     action: equals\n     value: \"Australia East\"\n    errorMessage: The required \"Australia East\" location was missing<\/pre>\n\n\n\n<p>YAML Pipeline for the task:<\/p>\n\n\n\n<pre class=\"wp-block-verse has-light-gray-color has-dark-gray-background-color has-text-color has-background\">  - task: Bash@3\n    displayName: 'Terraform : TFSEC'\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      targetType: inline\n      script: sudo docker run --rm -v \"$(pwd):\/src\" aquasec\/tfsec \/src --tfvars-file \/src\/terraform.tfvars\n  - task: Bash@3\n    displayName: 'Terraform : Linting'\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      targetType: inline\n      script: &gt;\n        sudo docker run --rm -v $(pwd):\/data -t ghcr.io\/terraform-linters\/tflint\n  - task: Bash@3\n    displayName: 'Terraform : Cost Estimation'\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      targetType: inline\n      script: \"terraform show -json plan.out &gt; plan.json\\n\\nsudo docker run --rm   -e INFRACOST_API_KEY=$(INFRACOST_API_KEY)   -v \\\"$(pwd):\/src\\\" infracost\/infracost breakdown --path  \/src\/plan.json --show-skipped \\n\"\n  - task: Bash@3\n    displayName: 'Terraform : Cost Difference'\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      targetType: inline\n      script: &gt;\n        sudo docker run --rm   -e INFRACOST_API_KEY=$(INFRACOST_API_KEY)   -v \"$(pwd):\/src\" infracost\/infracost diff --path  \/src\/plan.json --show-skipped &gt; $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber)<\/pre>\n\n\n\n<p>Here we also need to generate an API Key for the Infracost app, to learn how to do it you can click <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-06:-Terraform-Security-Compliance,-Linting,-Cost-Estimation-&amp;-Cost-Difference\" target=\"_blank\">here<\/a>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Part 4: Generating &amp; Uploading Logs<\/h4>\n\n\n\n<p>The need for this step is to store our logs generated in previous steps into a storage account and make sure that we are not going to lose them.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"325\" height=\"280\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-7.png?w=325\" alt=\"\" class=\"wp-image-10994\" \/><figcaption class=\"wp-element-caption\">Steps to generate &amp; upload the logs file<\/figcaption><\/figure>\n\n\n\n<pre class=\"wp-block-verse has-light-gray-color has-dark-gray-background-color has-text-color has-background\">  - task: Bash@3\n    displayName: Generating Logs\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      targetType: inline\n      script: &gt;\n        # Creating Logs Of Tfsec\n\n        sudo docker run --rm -v \"$(pwd):\/src\" aquasec\/tfsec \/src --tfvars-file \/src\/terraform.tfvars &gt; $(Build.DefinitionName)-tfsec-$(Build.BuildNumber)\n\n        cat $(Build.DefinitionName)-tfsec-$(Build.BuildNumber)\n\n\n        #Creating Logs Of Cost Estimation\n\n        sudo docker run --rm   -e INFRACOST_API_KEY=$(INFRACOST_API_KEY)   -v \"$(pwd):\/src\" infracost\/infracost breakdown --path  \/src\/plan.json --show-skipped --format html &gt; $(Build.DefinitionName)-cost-$(Build.BuildNumber).html\n\n        cat $(Build.DefinitionName)-cost-$(Build.BuildNumber).html\n\n\n        #Creating Logs Of Cost Diffrence\n\n        sudo docker run --rm   -e INFRACOST_API_KEY=$(INFRACOST_API_KEY)   -v \"$(pwd):\/src\" infracost\/infracost diff --path  \/src\/plan.json --show-skipped &gt; $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber)\n\n        cat $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber)\n  - task: AzureCLI@2\n    displayName: 'Upload tfsec file '\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      connectedServiceNameARM: a575*********************c71\n      scriptType: bash\n      scriptLocation: inlineScript\n      inlineScript: &gt;\n        az storage blob upload --file $(Build.DefinitionName)-tfsec-$(Build.BuildNumber) --name $(Build.DefinitionName)-tfsec-$(Build.BuildNumber) --account-name terrastoragestatesreport --container-name report\n      cwd: $(Pipeline.Workspace)\/s\n  - task: AzureCLI@2\n    displayName: 'Upload cost file '\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      connectedServiceNameARM: a575*********************c71\n      scriptType: bash\n      scriptLocation: inlineScript\n      inlineScript: az storage blob upload --file $(Build.DefinitionName)-cost-$(Build.BuildNumber).html --name $(Build.DefinitionName)-cost-$(Build.BuildNumber).html --account-name terrastoragestatesreport --container-name report\n      cwd: $(Pipeline.Workspace)\/s\n  - task: AzureCLI@2\n    displayName: Upload cost diff file\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      connectedServiceNameARM: a575*********************c71\n      scriptType: bash\n      scriptLocation: inlineScript\n      inlineScript: az storage blob upload --file $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber) --name $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber) --account-name terrastoragestatesreport --container-name report\n      cwd: $(Pipeline.Workspace)\/s<\/pre>\n\n\n\n<p>For a detailed description of this part, you can click <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-07:-Generating-&amp;-Uploading-Logs\" target=\"_blank\">here<\/a>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Part 5: Generating Artifacts<\/h4>\n\n\n\n<p>In this step, we&#8217;ll generate two artifacts, one named Release which will trigger the Release pipeline, and the other one named <strong><em>Repot<\/em><\/strong> which will publish the reports for our output files of Compliance, Cost Estimation &amp; Cost Difference.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"312\" height=\"277\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-8.png?w=312\" alt=\"\" class=\"wp-image-10997\" \/><figcaption class=\"wp-element-caption\">Steps to generate the artifacts<\/figcaption><\/figure>\n\n\n\n<pre class=\"wp-block-verse has-light-gray-color has-dark-gray-background-color has-text-color has-background\">  - task: Bash@3\n    displayName: Generating Logs\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      targetType: inline\n      script: &gt;\n        # Creating Logs Of Tfsec\n\n        sudo docker run --rm -v \"$(pwd):\/src\" aquasec\/tfsec \/src --tfvars-file \/src\/terraform.tfvars &gt; $(Build.DefinitionName)-tfsec-$(Build.BuildNumber)\n\n        cat $(Build.DefinitionName)-tfsec-$(Build.BuildNumber)\n\n\n        #Creating Logs Of Cost Estimation\n\n        sudo docker run --rm   -e INFRACOST_API_KEY=$(INFRACOST_API_KEY)   -v \"$(pwd):\/src\" infracost\/infracost breakdown --path  \/src\/plan.json --show-skipped --format html &gt; $(Build.DefinitionName)-cost-$(Build.BuildNumber).html\n\n        cat $(Build.DefinitionName)-cost-$(Build.BuildNumber).html\n\n\n        #Creating Logs Of Cost Diffrence\n\n        sudo docker run --rm   -e INFRACOST_API_KEY=$(INFRACOST_API_KEY)   -v \"$(pwd):\/src\" infracost\/infracost diff --path  \/src\/plan.json --show-skipped &gt; $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber)\n\n        cat $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber)\n  - task: AzureCLI@2\n    displayName: 'Upload tfsec file '\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      connectedServiceNameARM: a575*********************c71\n      scriptType: bash\n      scriptLocation: inlineScript\n      inlineScript: &gt;\n        az storage blob upload --file $(Build.DefinitionName)-tfsec-$(Build.BuildNumber) --name $(Build.DefinitionName)-tfsec-$(Build.BuildNumber) --account-name terrastoragestatesreport --container-name report\n      cwd: $(Pipeline.Workspace)\/s\n  - task: AzureCLI@2\n    displayName: 'Upload cost file '\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      connectedServiceNameARM: a575*********************c71\n      scriptType: bash\n      scriptLocation: inlineScript\n      inlineScript: az storage blob upload --file $(Build.DefinitionName)-cost-$(Build.BuildNumber).html --name $(Build.DefinitionName)-cost-$(Build.BuildNumber).html --account-name terrastoragestatesreport --container-name report\n      cwd: $(Pipeline.Workspace)\/s\n  - task: AzureCLI@2\n    displayName: Upload cost diff file\n    condition: succeededOrFailed()\n    enabled: False\n    inputs:\n      connectedServiceNameARM: a575*********************c71\n      scriptType: bash\n      scriptLocation: inlineScript\n      inlineScript: az storage blob upload --file $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber) --name $(Build.DefinitionName)-cost-diff-$(Build.BuildNumber) --account-name terrastoragestatesreport --container-name report\n      cwd: $(Pipeline.Workspace)\/s<\/pre>\n\n\n\n<p>For a detailed description of this part, you can click <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-08:-Generating-Artifacts\" target=\"_blank\">here<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Release Pipeline Steps<\/h2>\n\n\n\n<p>Our Build Step will generate an artifact named <strong><em>Release<\/em><\/strong> which will contain all the terraform files required to apply our desired configuration. Also after all the checks and validations, we&#8217;ve found out that there is no error in our code and it is exactly what we have desired, so we will allow the Continuous Deployment for our Release Pipeline.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"792\" height=\"616\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-10.png?w=792\" alt=\"\" class=\"wp-image-11001\" \/><figcaption class=\"wp-element-caption\">Release pipeline workflow<\/figcaption><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Part 1: Auto Approval For Terraform Apply<\/h4>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"523\" height=\"373\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-9.png?w=523\" alt=\"\" class=\"wp-image-11000\" \/><figcaption class=\"wp-element-caption\">Steps to apply to terraform code<\/figcaption><\/figure>\n\n\n\n<p>In this step, we will simply apply our terraform code and keep this stage as Auto-Approved.<\/p>\n\n\n\n<pre class=\"wp-block-verse has-light-gray-color has-dark-gray-background-color has-text-color has-background\">steps:\n- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0\n  displayName: 'Install Terraform 1.1.8'\n  inputs:\n    terraformVersion: 1.1.8\n\n- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV2@2\n  displayName: 'Terraform : INIT'\n  inputs:\n    workingDirectory: '$(System.DefaultWorkingDirectory)\/_IAC-CI\/release'\n    backendServiceArm: 'Opstree-PoCs (4c9***************************f3c)'\n    backendAzureRmResourceGroupName: 'ADOagent_rg'\n    backendAzureRmStorageAccountName: terrastoragestatesreport\n    backendAzureRmContainerName: statefile\n    backendAzureRmKey: terraform.tfstate\n\n- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV2@2\n  displayName: 'Terraform : APPLY'\n  inputs:\n    command: apply\n    workingDirectory: '$(System.DefaultWorkingDirectory)\/_IAC-CI\/release'\n    commandOptions: '--auto-approve'\n    environmentServiceNameAzureRM: 'Opstree-PoCs (4c9***************************f3c)'<\/pre>\n\n\n\n<p>To know more about this stage click <a href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-10:-Auto-Approval-For-Terraform-Apply\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Part 2: Manual Approval For Terraform Destroy<\/h4>\n\n\n\n<p class=\"has-text-align-justify\">Here in our Terraform Destroy pipeline, we will configure it for manual approval as it is going to be very sensitive &amp; secured. An unnecessarily or unwanted destroyed infrastructure can cause a huge loss of time, money, resources, backup &amp; data. So we&#8217;ll keep it highly secured and limit the access to reliable users only. <\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"382\" height=\"388\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-11.png?w=382\" alt=\"\" class=\"wp-image-11006\" \/><figcaption class=\"wp-element-caption\">Steps to destroy terraform code<\/figcaption><\/figure>\n\n\n\n<pre class=\"wp-block-verse has-light-gray-color has-dark-gray-background-color has-text-color has-background\">steps:\n- task: ms-devlabs.custom-terraform-tasks.custom-terraform-installer-task.TerraformInstaller@0\n  displayName: 'Install Terraform 1.1.8'\n  inputs:\n    terraformVersion: 1.1.8\n\n- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV2@2\n  displayName: 'Terraform : INIT'\n  inputs:\n    workingDirectory: '$(System.DefaultWorkingDirectory)\/_IAC-CI\/release'\n    backendServiceArm: 'Opstree-PoCs (4c9***************************f3c)'\n    backendAzureRmResourceGroupName: 'ADOagent_rg'\n    backendAzureRmStorageAccountName: terrastoragestatesreport\n    backendAzureRmContainerName: statefile\n    backendAzureRmKey: terraform.tfstate\n\n- task: ms-devlabs.custom-terraform-tasks.custom-terraform-release-task.TerraformTaskV2@2\n  displayName: 'Terraform : APPLY'\n  inputs:\n    command: apply\n    workingDirectory: '$(System.DefaultWorkingDirectory)\/_IAC-CI\/release'\n    commandOptions: '--auto-approve'\n    environmentServiceNameAzureRM: 'Opstree-PoCs (4c9***************************f3c)'<\/pre>\n\n\n\n<p>To know more about this stage click <a href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-11:-Manual-Approval-For-Terraform-Destroy\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Bonus: Branching Policy<\/h2>\n\n\n\n<p class=\"has-text-align-justify\">We need branching policies in order to save our main\/master branch from unwanted commits. To make any changes to our main\/master branch we need to Merge a branch, after committing changes, into the main\/master branch using Pull Request.  Also, remember that our Terraform-CD or Release branch will only be triggered if Terraform-CI or Build branch was triggered from the main branch.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img decoding=\"async\" src=\"https:\/\/opstree.com\/blog\/\/wp-content\/uploads\/2022\/06\/image-12.png?w=1024\" alt=\"\" class=\"wp-image-11011\" width=\"800\" \/><figcaption class=\"wp-element-caption\">Branching policy in Azure Repos<\/figcaption><\/figure>\n\n\n\n<p>To learn how to configure the branching policy in Azure DevOps click <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/himanshumudgal08\/terraform-CICD\/wiki\/Lab-12:-Configuring-Branching-Policy-in-Azure-Repos\" target=\"_blank\">here<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"has-text-align-justify\">So in this article, we get to learn about how to automate and streamline our terraform code deployment using Azure DevOps and use tools like Tfsec for security and compliance, Tflint for linting terraform code and Infracost for Cost Estimation &amp; Cost Difference. Along with that we also learned how to upload our logs into Blob Containers, publish artifacts and make a release pipeline using it which will have different stages for Terraform apply and terraform destroy.<\/p>\n\n\n\n<p><strong>Content References <\/strong>&#8211; <a href=\"https:\/\/www.terraform.io\/intro\" target=\"_blank\" rel=\"noopener\">Reference 1<\/a>, <a href=\"https:\/\/softobiz.com\/implement-azure-devops-in-your-organization-for-better-outcomes\/\" target=\"_blank\" rel=\"noopener\">Reference 2<\/a><br><strong>Image References<\/strong> &#8211; <a href=\"https:\/\/2eeqxr3onqxh2bnq421rwcx7-wpengine.netdna-ssl.com\/wp-content\/uploads\/2019\/11\/devops-2-1024x576.jpg\" target=\"_blank\" rel=\"noopener\">Image 1<\/a>, <a href=\"https:\/\/content.hashicorp.com\/api\/assets?product=terraform&amp;version=refs%2Fheads%2Fstable-website&amp;asset=website%2Fimg%2Fdocs%2Fintro-terraform-workflow.png&amp;width=2048&amp;height=1798\" target=\"_blank\" rel=\"noopener\">Image 2<\/a><\/p>\n\n\n\n<p><br><strong style=\"font-weight:bold;\">Blog Pundit:<\/strong>  <a href=\"https:\/\/opstree.com\/blog\/\/author\/bhupendersinghb5dca0b393\/\"><strong>Bhupender Rawat<\/strong><\/a> and <a rel=\"noreferrer noopener\" href=\"https:\/\/opstree.com\/blog\/\/author\/sandeep7c51ad81ba\/\" target=\"_blank\"><strong>Sandeep Rawat<\/strong><\/a><\/p>\n\n\n\n<p><strong><a href=\"https:\/\/www.opstree.com\/contact-us?utm_source=wordpress&amp;utm_campaign=Terraform-CI-CD-With-Azure-DevOps&amp;utm_id=Blog\">Opstree<\/a><\/strong><a href=\"https:\/\/www.opstree.com\/contact-us?utm_source=wordpress&amp;utm_campaign=What-is-a-Bare-Git-Repository%3F&amp;utm_id=Blog\"> <\/a>is an End to End DevOps solution provider<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button is-style-fill\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/www.opstree.com\/contact-us\" target=\"_blank\" rel=\"noreferrer noopener\">CONTACT US<\/a><\/div>\n<\/div>\n\n\n\n<p class=\"has-text-align-center\"><strong>Connect Us <\/strong><\/p>\n\n\n\n<ul class=\"wp-block-social-links aligncenter is-content-justification-right is-layout-flex wp-container-core-social-links-is-layout-1 wp-block-social-links-is-layout-flex\"><li class=\"wp-social-link wp-social-link-linkedin  wp-block-social-link\"><a href=\"https:\/\/www.linkedin.com\/company\/opstree-solutions\" class=\"wp-block-social-link-anchor\" target=\"_blank\" rel=\"noopener\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M19.7,3H4.3C3.582,3,3,3.582,3,4.3v15.4C3,20.418,3.582,21,4.3,21h15.4c0.718,0,1.3-0.582,1.3-1.3V4.3 C21,3.582,20.418,3,19.7,3z M8.339,18.338H5.667v-8.59h2.672V18.338z M7.004,8.574c-0.857,0-1.549-0.694-1.549-1.548 c0-0.855,0.691-1.548,1.549-1.548c0.854,0,1.547,0.694,1.547,1.548C8.551,7.881,7.858,8.574,7.004,8.574z M18.339,18.338h-2.669 v-4.177c0-0.996-0.017-2.278-1.387-2.278c-1.389,0-1.601,1.086-1.601,2.206v4.249h-2.667v-8.59h2.559v1.174h0.037 c0.356-0.675,1.227-1.387,2.526-1.387c2.703,0,3.203,1.779,3.203,4.092V18.338z\"><\/path><\/svg><span class=\"wp-block-social-link-label screen-reader-text\">LinkedIn<\/span><\/a><\/li>\n\n<li class=\"wp-social-link wp-social-link-youtube  wp-block-social-link\"><a href=\"https:\/\/www.youtube.com\/channel\/UCeLma6SpNYH7jjYKSBNSexw\" class=\"wp-block-social-link-anchor\" target=\"_blank\" rel=\"noopener\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M21.8,8.001c0,0-0.195-1.378-0.795-1.985c-0.76-0.797-1.613-0.801-2.004-0.847c-2.799-0.202-6.997-0.202-6.997-0.202 h-0.009c0,0-4.198,0-6.997,0.202C4.608,5.216,3.756,5.22,2.995,6.016C2.395,6.623,2.2,8.001,2.2,8.001S2,9.62,2,11.238v1.517 c0,1.618,0.2,3.237,0.2,3.237s0.195,1.378,0.795,1.985c0.761,0.797,1.76,0.771,2.205,0.855c1.6,0.153,6.8,0.201,6.8,0.201 s4.203-0.006,7.001-0.209c0.391-0.047,1.243-0.051,2.004-0.847c0.6-0.607,0.795-1.985,0.795-1.985s0.2-1.618,0.2-3.237v-1.517 C22,9.62,21.8,8.001,21.8,8.001z M9.935,14.594l-0.001-5.62l5.404,2.82L9.935,14.594z\"><\/path><\/svg><span class=\"wp-block-social-link-label screen-reader-text\">YouTube<\/span><\/a><\/li>\n\n<li class=\"wp-social-link wp-social-link-github  wp-block-social-link\"><a href=\"https:\/\/github.com\/OpsTree\" class=\"wp-block-social-link-anchor\" target=\"_blank\" rel=\"noopener\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M12,2C6.477,2,2,6.477,2,12c0,4.419,2.865,8.166,6.839,9.489c0.5,0.09,0.682-0.218,0.682-0.484 c0-0.236-0.009-0.866-0.014-1.699c-2.782,0.602-3.369-1.34-3.369-1.34c-0.455-1.157-1.11-1.465-1.11-1.465 c-0.909-0.62,0.069-0.608,0.069-0.608c1.004,0.071,1.532,1.03,1.532,1.03c0.891,1.529,2.341,1.089,2.91,0.833 c0.091-0.647,0.349-1.086,0.635-1.337c-2.22-0.251-4.555-1.111-4.555-4.943c0-1.091,0.39-1.984,1.03-2.682 C6.546,8.54,6.202,7.524,6.746,6.148c0,0,0.84-0.269,2.75,1.025C10.295,6.95,11.15,6.84,12,6.836 c0.85,0.004,1.705,0.114,2.504,0.336c1.909-1.294,2.748-1.025,2.748-1.025c0.546,1.376,0.202,2.394,0.1,2.646 c0.64,0.699,1.026,1.591,1.026,2.682c0,3.841-2.337,4.687-4.565,4.935c0.359,0.307,0.679,0.917,0.679,1.852 c0,1.335-0.012,2.415-0.012,2.741c0,0.269,0.18,0.579,0.688,0.481C19.138,20.161,22,16.416,22,12C22,6.477,17.523,2,12,2z\"><\/path><\/svg><span class=\"wp-block-social-link-label screen-reader-text\">GitHub<\/span><\/a><\/li>\n\n<li class=\"wp-social-link wp-social-link-facebook  wp-block-social-link\"><a href=\"https:\/\/www.facebook.com\/opstree\" class=\"wp-block-social-link-anchor\" target=\"_blank\" rel=\"noopener\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M12 2C6.5 2 2 6.5 2 12c0 5 3.7 9.1 8.4 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.3c-1.2 0-1.6.8-1.6 1.6V12h2.8l-.4 2.9h-2.3v7C18.3 21.1 22 17 22 12c0-5.5-4.5-10-10-10z\"><\/path><\/svg><span class=\"wp-block-social-link-label screen-reader-text\">Facebook<\/span><\/a><\/li>\n\n<li class=\"wp-social-link wp-social-link-medium  wp-block-social-link\"><a href=\"https:\/\/medium.com\/buildpiper\" class=\"wp-block-social-link-anchor\" target=\"_blank\" rel=\"noopener\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" version=\"1.1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M20.962,7.257l-5.457,8.867l-3.923-6.375l3.126-5.08c0.112-0.182,0.319-0.286,0.527-0.286c0.05,0,0.1,0.008,0.149,0.02 c0.039,0.01,0.078,0.023,0.114,0.041l5.43,2.715l0.006,0.003c0.004,0.002,0.007,0.006,0.011,0.008 C20.971,7.191,20.98,7.227,20.962,7.257z M9.86,8.592v5.783l5.14,2.57L9.86,8.592z M15.772,17.331l4.231,2.115 C20.554,19.721,21,19.529,21,19.016V8.835L15.772,17.331z M8.968,7.178L3.665,4.527C3.569,4.479,3.478,4.456,3.395,4.456 C3.163,4.456,3,4.636,3,4.938v11.45c0,0.306,0.224,0.669,0.498,0.806l4.671,2.335c0.12,0.06,0.234,0.088,0.337,0.088 c0.29,0,0.494-0.225,0.494-0.602V7.231C9,7.208,8.988,7.188,8.968,7.178z\"><\/path><\/svg><span class=\"wp-block-social-link-label screen-reader-text\">Medium<\/span><\/a><\/li><\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Let&#8217;s consider a scenario in which you are deploying your infrastructure using a Terraform code (infrastructure-as-code) which is stored in a remote git repository. Now working in an organization you need to make sure that all your deployments are always tracked without an exception, an add-on to that whether your Terraform code is following your &hellip; <a href=\"https:\/\/opstree.com\/blog\/2022\/06\/28\/terraform-ci-cd-with-azure-devops\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Terraform CI-CD With Azure DevOps&#8221;<\/span><\/a><\/p>\n","protected":false},"author":222974219,"featured_media":29900,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false},"version":2}},"categories":[28070474],"tags":[335778,460,4605929,717131695,768739308,768739286,4996032,3021235],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/opstree.com\/blog\/wp-content\/uploads\/2025\/11\/DevSecOps-1.jpg","jetpack_likes_enabled":true,"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/pfDBOm-2QX","jetpack-related-posts":[],"_links":{"self":[{"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/posts\/10971"}],"collection":[{"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/users\/222974219"}],"replies":[{"embeddable":true,"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/comments?post=10971"}],"version-history":[{"count":24,"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/posts\/10971\/revisions"}],"predecessor-version":[{"id":11270,"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/posts\/10971\/revisions\/11270"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/media\/29900"}],"wp:attachment":[{"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/media?parent=10971"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/categories?post=10971"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/opstree.com\/blog\/wp-json\/wp\/v2\/tags?post=10971"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}