Managing Cloud Resource Costs with Infracost and Open Policy Agent in a Jenkins Pipeline for Deployment

Managing Cloud Resource Costs with Infracost and Open Policy Agent in a Jenkins Pipeline for Deployment

In your deployment phase, a crucial question to address is the cost estimation of the resources required for deploying your amazing application within the allocated or recommended budget. Failing to address this question could result in exceeding the allocated or budgeted amount due to resource creation costs.

One tool that was developed to help with putting a check on the cost of cloud resources is Infracost. Infracost is a valuable tool designed to monitor and control the cost of cloud resources. Unlike tools like the AWS calculator, Infracost stands out with its automated approach. By simply parsing the JSON format of your Terraform-created resources as an argument to the Infracost CLI command, it automatically provides you with the monthly cost estimation for the resources to be deployed, whereas the AWS calculator requires manual input of each resource to be created.

This article aims to guide you, the reader, on obtaining cost estimates for your cloud resources using Infracost. It covers creating an Open Policy Agent (OPA) policy to enforce monthly budget constraints and integrating the infracost command into your Jenkins pipeline. To set up Infracost, refer to the official documentation for detailed instructions on getting started.

The terraform files for the resources to be created are stored in a terrafom folder which is in the same location as the Jenkinsfile. This is the file location format used in this article.

As we execute the Infracost command within a Jenkins pipeline, we utilize the official Infracost docker image as the Jenkins agent, alongside configuring essential environment variables. Here is an example of how the configuration should be set up:

agent {
  docker {
    image 'infracost/infracost:ci-latest'
    args "--user=root --entrypoint=''"
          }
       }
environment {
   INFRACOST_API_KEY = credentials("INFRACOST_API_KEY")
   INFRACOST_VCS_PROVIDER = 'github'
   INFRACOST_VCS_REPOSITORY_URL = 'https://github.com/Okeybukks/devops-automation'
    }

To get your INFRACOST_API_KEY you can run this command in your CLI.

infracost configure get api_key

Alternatively, you can access the Infracost dashboard, login with your credentials, navigate to "Org settings," and click on the "Copy" button next to the API Key. This key needs to be securely stored as secret text in Jenkins credentials, with an ID of INFRACOST_API_KEY. Considering that our Git repository is hosted on GitHub, set the value of INFRACOST_VCS_PROVIDER to "GitHub." Additionally, specify the INFRACOST_VCS_REPOSITORY_URL as the URL of the repository containing your Jenkinsfile and Terraform folder.

After configuring Infracost for Jenkins, the subsequent step involves executing the infracost breakdown command. This command analyzes the Terraform plan and provides a cost estimate for the cloud resources intended for deployment. The resulting output is saved in a JSON file format, which will be utilized by our OPA policy.

 stage("Check Financial Expense of Infrastructures Job with Infracost"){
   agent {
     docker {
       image 'infracost/infracost:ci-latest'
       args "--user=root --entrypoint=''"
             }
         }
   environment {
       INFRACOST_API_KEY = credentials("INFRACOST_API_KEY")
       INFRACOST_VCS_PROVIDER = 'github'
       INFRACOST_VCS_REPOSITORY_URL = 'https://github.com/Okeybukks/devops-automation'
            }
   steps{
       dir("./terraform") {
          sh 'echo "This is the financial check job"'     
          sh 'infracost breakdown --path . --format json --out-file infracost.json'
          archiveArtifacts artifacts: 'infracost.json'
           } 
        }
   }

The terraform files are located in the terraform folder, so to change into the terraform folder, we will be using the dir block.

The infracost breakdown command takes a couple of arguments with values passed to it.

--path takes the path where the terraform files are located. Since we are already in the terraform folder, we use . to reference the path.

--format takes the format of the output of the infracost breakdown command.

--out-file takes the name of the file you wish to save the infracost breakdown output.

To enable the utilization of the output file generated by the infracost breakdown command in subsequent stages, the file is parsed to the archiveArtifacts command, converting it into an artifact. For a more comprehensive understanding of artifact creation and copying, I invite you to explore the article I have authored on Jenkins artifacts.

To establish a cost constraint on the creation of resources, an OPA policy is employed. In this article, we are imposing a limit of $100 on the allocated resources. OPA serves as a comprehensive toolset and framework for policy implementation throughout the cloud-native ecosystem. To delve deeper into OPA, you can find more information by clicking here

OPA policies are written in the rego language, and as such, the policy file is suffixed with .rego. The documentation for rego serves as a comprehensive resource to comprehend policy formulation. In this article, a straightforward policy will be presented, which verifies whether the cumulative monthly estimate of your cloud resources surpasses the predefined maximum monthly estimate that must not be exceeded.

package infracost

deny[out] {

    # define a variable
    maxMonthlyCost = 100.0

    msg := sprintf(
        "Total monthly cost must be less than $%.2f (Current monthly cost is $%.2f)",
        [maxMonthlyCost, to_number(input.totalMonthlyCost)],
    )

      out := {
        "msg": msg,
        "failed": to_number(input.totalMonthlyCost) >= maxMonthlyCost
      }
}

The package infracost line is the infracost package or module utilized by OPA to understand the content of the output file from the infracost breakdown command. OPA policies are written in a deny[out] block. Any code written outside this block will return a rego_parse_error error.

maxMonthlyCost is the defined variable which is the maximum amount budgeted for our resources.

The message to be output in the Jenkins pipeline and GitHub commit message if the set condition fails is defined in the msg block.

The condition logic is set using the out block. The total estimate our resources will cost us is saved in input.totalMonthlyCost variable. This variable is a default variable in the rego language. The input.totalMonthlyCost is the total monthly cost estimated by infracost.

From our policy, if to_number(input.totalMonthlyCost) >= maxMonthlyCost is true it stops the pipeline and prevents moving to the next stage of the pipeline. The message in the msg block explains to us that we cant go forward.

The content of this policy is stored with infracost-policy.rego file name. This filename can be changed though, but it must have the .rego format appended to it.

With the policy written, it is then parsed to the infracost comment github command. This is what the stage looks like.

stage("Post Infracost comment"){
  agent {
     docker {
        image 'infracost/infracost:ci-latest'
        args "--user=root --entrypoint=''"
             }
        }
  environment {
     INFRACOST_API_KEY = credentials("INFRACOST_API_KEY")
     INFRACOST_VCS_PROVIDER = 'github'
     INFRACOST_VCS_REPOSITORY_URL = 'https://github.com/Okeybukks/devops-automation'
     INFRACOST_VCS_BASE_BRANCH = 'main'
     GITHUB_TOKEN = credentials("GITHUB_TOKEN")
     GITHUB_REPO = "Okeybukks/devops-automation"
         }
  steps{
     dir('./terraform'){
         sh 'echo "This is the financial check job"'
         copyArtifacts filter: 'infracost.json', fingerprintArtifacts: true, projectName: 'test', selector: specific ('${BUILD_NUMBER}')    

         sh 'infracost comment github --path infracost.json --policy-path infracost-policy.rego \
                    --github-token $GITHUB_TOKEN --repo $GITHUB_REPO --commit $GIT_COMMIT'
         }
     }
 }

Upon observation, you will notice the addition of new environment variables required for this stage. Since the infracost command will be responsible for posting the commit message to our repository, it will necessitate specific GitHub access permissions to fulfil this task effectively.

Generate a classic token with repo permission ticked. If you can read how to generate a classic GitHub token using this article. Once generated, save it in your Jenkins credential using secret text and save it with the ID GITHUB_TOKEN .

The GitHub repo name of this project is also needed. Store this value in the GITHUB_REPO ID.

The output of the infracost breakdown command is saved in the infracost.json file. To access the file at this stage, the copyArtifacts is utilized.

Now we have all the values to the arguments needed by the infracost comment github command to utilize our policy in creating a check for resource creation.

--path value is the file created with the infracost breakdown command, i.e infracost.json

--policy value is the policy file we created i.e infracost-policy.rego .

--github-token value is our classic GitHub token stored in GITHUB_TOKEN .

--repo is the project repo name stored in GITHUB_REPO

--commit is required to post on a pull request's commit. Its value is stored in $GIT_COMMIT variable, one of Jenkins default variables.

When you run your pipeline, you should have something similar to this if your resources estimate exceeds the set mark.

+ infracost comment github --path infracost.json --policy-path infracost-policy.rego --github-token **** --repo Okeybukks/devops-automation --commit a0e8fa201d81ba9d2a508e4adc55e2b8f1ffca6d
time="2023-06-12T04:29:15Z" level=info msg="Finding matching comments for tag infracost-comment"
time="2023-06-12T04:29:21Z" level=info msg="Found 0 matching comments"
time="2023-06-12T04:29:21Z" level=info msg="Creating new comment"
time="2023-06-12T04:29:21Z" level=info msg="Created new comment https://github.com/Okeybukks/devops-automation/commit/a0e8fa201d81ba9d2a508e4adc55e2b8f1ffca6d#commitcomment-117572438"
Comment posted to GitHub

Error: Policy check failed:

 - Total monthly cost must be less than $100.00 (Current monthly cost is $168.72)

This concludes the article. Implementing this cost monitoring approach will not only lead to cost savings in resource usage but also prevent unintended deployments of unplanned cloud resources. Thank you for reading.