I’ve spent the last eight years working with Terraform on an almost constant basis. Sometimes I think in HCL, and dream of waves of resource creation messages flying past my eyes. But for most people, that is not the case. While I’ve cultivated an expertise in Terraform, I cannot expect others to have done the same. That includes Ops folks, but also developers, networking teams, SREs, and security peeps. They all have responsibilities and skills that aren’t thinking about Terraform 24/7.
When organizations get started on their IaC journey, it’s usually an outside consultant or an internal platform team that introduces and implements Terraform. Initial projects are successful because of the deep expertise and knowledge those folks bring to the table. But how do you generalize that knowledge? How do you spread the wealth of expertise around to other teams that are less familiar with Terraform and infrastructure in general?
You could try and make everyone learn Terraform. I’m sure there’s nothing a bunch of harried developers would like more than to learn yet another DSL (YADSL?). Learning Terraform isn’t easy. I’ve been working with it since 2016, and I’m still learning about new features and functionality. And that’s just the IaC, developers also need to learn the intricacies of the cloud platforms you’ve been using, including applying proper security and sane defaults.
What about the network, security, and identity teams? They all have their own fires to put out. Asking everyone in the organization to adopt and integrate Terraform into their daily practices is just not realistic. So what’s the solution? Usually it’s to build a platform team that crafts well-defined standards, golden paths, and templatized environments for other teams to use. They can lean on specialized teams for their expertise and translate that expertise into practical implementations, like codifying infrastructure in Terraform modules and applying policy as code to ensure adherence to best practices.
Those Terraform modules are still using Terraform though. One problem platform teams are trying to solve is how to help others interact with Terraform modules without having an in-depth knowledge of the language. If you’ve ever tried to use a sufficiently complex one (AKS I’m looking at you), you realize how quickly the abstraction starts to break down. Additionally, combining modules in a coherent composition still requires an understanding of the Terraform language itself and how modules interact with each other. Platform teams can try and put a pretty UI in front of that, but it doesn’t solve the underlying challenge of root module authorship.
This is a problem that managed solutions are trying to solve. One such solution, and sponsor of this post is Resourcely. They are a configuration platform for defining and deploying cloud infrastructure at scale in an efficient and secure manner.
Resourcely offers Blueprints for deployment and Guardrails for enforcing policy and compliance, both of which can be authored by your platform team. Developers can consume these artifacts from behind a UI that abstracts the underlying Terraform code, while still leaving it accessible for break-glass scenarios. Your developers can build the infrastructure they need quickly and without that deep working knowledge of Terraform.
In this post, I’ll run through how Resourcely is set up and how you can author and utilize Blueprints and Guardrails to deploy infrastructure.
As I just mentioned, Resourcely is a platform to aid in defining and deploying infrastructure. It integrates with source control to manage your infrastructure as code, and it leverages Terraform under the covers to perform the actual deployment and management of your environments.
The two main concepts to understand in Resourcely are Blueprints and Guardrails. Blueprints are an abstracted form of a Terraform module, providing an interpretation layer between a developer-facing form and a rendered Terraform configuration. Guardrails are a way to define policy as code using the Really domain specific language. Guardrails are applied to Blueprints and verified during the planning stage to ensure that proposed deployments are in compliance. That is all a bit esoteric, so let’s walk through a demonstration to see it all in action.
For this demo, let’s suppose I’ve been charged with creating a self-service option for developers to provision a Google Cloud Storage bucket. The module should create a basic storage bucket and ensure that public access is not enabled. Additionally, if the bucket is used for production it should be limited to regions in the US.
We can use a combination of Blueprints and Guardrails to expose the right fields to the developers while ensuring that we’re in compliance with policies. First, we’ll create a Blueprint using a starter template. Next we’ll create a Guardrail to prevent public access for the bucket. Then, we’ll deploy a storage bucket using GitHub integration. Finally, we’ll add a second Guardrail to restrict the region selection and verify it works.
Once you’re signed into Resourcely, you’ll see the Foundry, Blueprints, and Guardrails options in the side menu.
Foundry is where you can author Blueprints and Guardrails from scratch using Resourcely’s built-in IDE. You can also start the creation process from the Blueprints or Guardrails view.
Blueprints are defined using the file type .tft
, which I can only assume stands for TerraForm Template. When someone wants to consume a Blueprint, they are presented with a form to fill out based on the contents of the Blueprint file. The values from the form are combined with the Terraform template in the Blueprint to create valid Terraform code that can be checked into source control.
When you author a Blueprint, you get to decide what fields are available to the consumer and what values are allowed for that field. Later, we’ll see how we can use Guardrails to further enforce settings based on context and policy.
In the video below, I am creating a Blueprint from a starter supplied by Resourcely. You can also opt to import a Blueprint from a source control repository or from the modules in the Terraform public registry.
The content for the Blueprint looks very similar to Terraform code, except there are some variables and syntax that need to be interpreted. The Blueprint file can start with YAML-based front-matter that includes the definition of constants and variables to use inside the template.
---
constants:
__name: "{{ name }}_{{ __guid }}"
This code defines a constant called __name
that is a combination of the variable name
and a GUID created by the special expression __guid
. We can now use the __name
constant throughout the rest of the template as a unique value.
You may have already noticed that variables and other special expressions start and end with double curly braces {{ }}
. If you’re familiar with Go’s templating language, you’ll feel right at home here.
Below the constant, we define a variable called location
and its properties.
variables:
location:
type: string
desc: "GCP Location to use for the bucket."
required: true
suggest: "US-CENTRAL1"
---
Resourcely calls the properties tag parameters. In addition to setting the type and description, we are also providing a suggestion for its initial value.
You might notice that the name
variable used in our constant hasn’t been defined yet. That’s because you can define variables in-line as well.
Below the front matter is the Terraform code to create our bucket. It uses the {{ }}
to signify that some interpolation needs to occur when this code is rendered.
resource "google_storage_bucket" "{{ __name }}" {
name = "{{ name | desc: "Name of bucket" }}"
location = "{{ location }}"
force_destroy = true
public_access_prevention = "enforced"
uniform_bucket_level_access = true
lifecycle_rule {
condition {
age = 7
}
action {
type = "AbortIncompleteMultipartUpload"
}
}
}
The resource block name label is "{{ __name }}"
, which will use the value stored in the constant __name
for the resource name in the configuration. This helps with uniqueness of names once the template is rendered.
The location
and name
arguments both use template variables. Variables can be defined in-line or as part of the front-matter. The name
variable is defined in-line using the syntax {{ name | desc: "Name of bucket" }}
. The |
character after the variable’s name is used to set tag parameters, with a pipe between each tag parameter.
The template variables are turned into a form the consumer can fill out, which is shown on the Developer Experience tab.
In our form, both the Name and Location appear along with the description we included for each variable. The location value has also been pre-populated with US-CENTRAL1
.
Resourcely is smart enough to understand the context of the location value, and so it creates a drop-down list of GCP regions to select instead of a free-form text field.
Nothing like preventing invalid input!
The official documentation for Blueprints includes several other supported tag parameters, and other useful interpolation syntax.
Beyond the content of the template, there are three other sections that go into a complete Blueprint: Guardrail customization, metadata definition, and context attachment. We don’t have any Guardrails yet, so we can skip that section.
The Define Metadata section allows us to specify information about the Blueprint itself.
The fields include the Name of the Blueprint, a Description, which Provider is being used, and metadata tags to associate with the Blueprint to aid in discovery.
The settings shown in the Attach Context section come from the Global Context defined at the top-level of the organization.
Each context value will be added to the form presented to the consumer, and the value can be referenced by any Guardrails being applied. We’ll touch on this later when we use the context to control which regions are allowed for a bucket.
In addition to configuring the Blueprint settings and viewing the developer experience, the Terraform tab shows the resulting Terraform configuration based on the form input.
You can toggle between the Settings, Developer Experience, and Terraform tabs to refine and test the Blueprint until it meets your requirements.
Once all the sections are complete, we can create our and publish our Blueprint. Now it will appear in the list of available Blueprints for use.
Blueprints can be set to an Unpublished status, hiding them from consumers.
Based on our requirements, we need to prevent the Google Cloud Storage bucket from being publicly accessible. We will achieve this by creating a Guardrail.
The argument that prevents public access in our code is public_access_prevention
. We set that argument to enforced
, which prevents public access. But what if someone alters the configuration after it’s rendered? We can detect and potentially block deployment of the altered configuration using Guardrails.
Guardrails are verified both when the form is being filled out and when the Blueprint is queued for deployment. Resourcely can check the contents of the Terraform plan and verify it is in compliance with the attached Guardrails.
We’ll start over in the Foundry to create a Guardrail.
In the video, I select a starter Guardrail that already has the settings I’m interested in. The resulting code is expressed using Really, which reads a lot like a SQL query.
GUARDRAIL "[Storage] Bucket Public Access Prevention Enabled"
WHEN google_storage_bucket
REQUIRE public_access_prevention = "enforced"
OVERRIDE WITH APPROVAL @ned1313
Essentially, the Guardrail looks for any instances of the google_storage_bucket
resource type and checks to make sure the public_access_prevention
argument is set to "enforced"
. If it’s not, then the Guardrail fails, but I can override it because I’m super special.
Similar to the Blueprint creation, the Guardrail authoring environment has a Define Metadata section where we can specify things like the Guardrail’s name and description.
All of the fields are pre-filled based on the starter Guardrail.
The Set Activation Policy section is where the rubber meets the road. It determines where the Guardrail is applied and how it should be evaluated.
A Status of Inactive means the Guardrail isn’t applied or evaluated. The Evaluate Only status will include an informational message about the status of the Guardrail evaluation, but it won’t block the process from moving forward. When a Guardrail is Active, it will block deployment on failure, unless someone with permission overrides it.
For the Repository setting, you can specify a single repository or set of repositories using wildcards. We left the field blank, which means the Guardrail will apply to all repositories in the organization.
Now that we have our Blueprint and Guardrail, we’re almost ready to deploy some infrastructure. First we’ve got to get our GitHub organization connected, and set up GitHub Actions to integrate with Resourcely.
There are two steps to connecting Resourcely to GitHub and integrating it with your GitHub Actions. First you need to grant Resourcely access to your GitHub organization and configure a webhook that fires on pull requests and pull request reviews. The video below demonstrates the process:
You can also follow the instructions documented here.
Once Resourcely is connected to GitHub, you can update your GitHub Actions to incorporate Resourcely into the CI process. There is an example GitHub Action workflow provided by Resourcely you can leverage. Let’s look at some of the key portions of my configuration.
The Resourcely CI process needs a copy of the plan output in JSON format. In the terraform-plan
job section of my workflow, I save the plan to a file, convert it to JSON, and save it as an artifact:
- name: Terraform Plan
run: terraform plan -var "project_id=${{ secrets.PROJECT_ID }}" -out=plan.tfplan
- name: Convert plan to JSON
id: convert_plan
run: terraform show -json plan.tfplan > plan.json
- name: Upload Terraform Plan JSON Output
uses: actions/upload-artifact@v4
with:
name: plan-file-json
path: plan.json
The resourcely-ci
job only fires when the terraform-plan
job is complete and if the event is a pull request:
resourcely-ci:
needs: terraform-plan
if: github.event_name == 'pull_request'
The job checks out the source code, retrieves the plan file, and kicks off the Resourcely action:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download Terraform Plan Output
uses: actions/download-artifact@v4
with:
name: plan-file-json
path: tf-plan-files/
- name: Resourcely CI
uses: Resourcely-Inc/resourcely-action@v1
with:
resourcely_api_token: ${{ secrets.RESOURCELY_API_TOKEN }}
resourcely_api_host: "https://api.resourcely.io"
tf_plan_directory: "tf-plan-files"
You might notice the secrets.RESOURCELY_API_TOKEN
in the above code. You’ll need to generate a Resourcely API token and save it in the GitHub Actions Secrets for the repository.
You can check out the full workflow file, as well as the rest of the configuration by looking at this repository. Now it’s time to create a resource!
To create a resource with a Blueprint, we are essentially going to add Terraform code to an existing repository through a pull request. Resourcely will take the inputs for the Blueprint, render a Terraform configuration, and then create a pull request targeting the repository we specify.
The pull request will kick off the GitHub Action workflow we just reviewed. Once the change is approved and merged, the actual resource will be deployed through the same workflow.
Let’s start by going to the Resources section of the Resourcely page and use the Google Cloud Storage Blueprint.
I hope you noticed that I went in and manually changed the public_access_prevention
argument to inherited
. That is still a valid value for the argument, but it doesn’t match our Guardrail. Let’s see what happens during the pull request review.
Once the pull request has been created, the Resourcely CI process will kick off as part of the workflow. We can check out the results by clicking on the link to the pull request:
The Resourcely Guardrails check doesn’t pass because our public access Guardrail failed! Digging into the details, we can see which Guardrail failed and why. Since our Guardrail is set to evaluate only, we can still move forward with the deployment, but I’d rather be in compliance.
Back in the Resourcely portal, I’ll update my pull request and switch the public_access_prevention
argument back to enforced
.
Now all the checks in the PR pass, and we can move forward with the apply.
When the deployment is complete, we now have our compliant resource deployed in Google Cloud. But that was just for enforcing the public access piece, what about limiting the region based on environment?
Before we add a new Guardrail and apply it to the Blueprint, I need to introduce a new concept, the Global Context.
In the Global Context area, you can create questions that will be included in the form filled out by the consumer. The questions can be multiple choice, single choice, or free-form text.
We want to limit production workloads to the US, so we need to know what type of environment is being targeted. I’ve created a single choice Global Context called deploy_environment
and added some values to it.
These options will appear as a drop-down menu in the Blueprints we apply it to.
With our new Global Context applied to our existing Blueprint, we can create a Guardrail that leverages it.
I want a Guardrail that requires my bucket location to be in the US if the deploy_environment
is Production.
The code for the Guardrail looks like this:
GUARDRAIL "GCP Storage Allowed Regions"
WHEN google_storage_bucket AND CONTEXT deploy_environment MATCHES ANY ["production"]
REQUIRE location MATCHES "US-*"
OVERRIDE WITH APPROVAL @default
The WHEN statement is looking for resources of type google_storage_bucket
and checking if the deploy_environment
context is in the list ["production"]
. If I want to add the same restriction for another environment later, I can simply add it to the list. The REQUIRE statement limits the location to regions that match the string "US-*"
.
Like our previous Guardrail, this Guardrail is being applied to all repositories, but this time we have it set to an Active status. Let’s take it for a test drive on our Blueprint.
Over in the Blueprint, we get to see a different way that Guardrails are applied. I’ll jump over to the Developer Experience tab, and test things out.
There is now a Global Context section to the Blueprint. If I set the environment to development and check the location drop-down, it shows me all the regions. Once I change it to production, now the location selection is limited to only US regions. I can override the Guardrail by unlocking it, however during the pull request review it will require an approver to allow the exception.
Guardrails are not just checked during plan, they also dynamically impact what the consumer sees in the Blueprint form. That shortens the feedback loop for creating compliant infrastructure.
One of the big challenges of driving IaC adoption is making it easy to consume for others. Sure your platform team knows Terraform inside and out, but how do you get your application teams on board? Give them a simple interface for vending infrastructure and tie it into the source control systems and developer platforms they’re already using. Make it easy to compose infrastructure from Blueprints and enforce best practices through the use of Guardrails. At the same time, include an escape hatch for exceptions, so you don’t block developer productivity unnecessarily.
I’ve only spent a few days working through what Resourcely has to offer and I know that the product is still evolving and growing. It’s great to see that they already have integrations with developer portals, CI/CD platforms, and ITSM products. Resourcely is not a product that lives on an island, but rather a way to integrate your IaC management into other tools and workflows.
If you’d like to take Resourcely for a spin yourself, you can use my referral link. I don’t get any kickbacks beyond this sponsored post, but it does let them know you read it and I appreciate that.
Sponsored Note: This blog post was sponsored by Resourcely. While I did receive compensation, the opinions and content of the post are entirely created and written by me.
Resourcely Guardrails and Blueprints
November 15, 2024
Deploying Azure Landing Zones with Terraform
November 12, 2024
October 18, 2024
What's New in the AzureRM Provider Version 4?
August 27, 2024