Terraform as a Scripting Language

Terraform as a Scripting Language

Give me a lever long enough...

What if I told you that you can abuse Terraform to create one-off scripts with ease that are self-documenting, you also get to see what the script will do before it does anything, and you’ll get to look like someone with superpowers while doing so.

What is this??

I’m not a lawyer, so don’t blame me if someone sues you…

There’s no law against running Terraform without a VCS, or outside a CI pipeline, or against writing one-off scripts that have the potential to alter reality in just a couple of lines.

Well, let’s talk about a mid-sized organization that has, idk, 300 repositories all hosted on GitHub and they didn’t know about rulesets for some reason and now they know they should use them. While they’re simple to configure, there’s no way to configure them against a batch of repositories (that I’m aware of; if there is, I don’t care).

So what would the hypothetical engineer do in this situation? Script it, obviously. How would you start? Maybe create a main.py and import PyGithub?

Ugh, now you have to read the documentation and search how to paginate the repositories, you have to create tests to ensure that what you’re doing is what you want, you need to handle edge cases, maybe three repositories already have a ruleset that will conflict with yours…

Depending on your Python proficiency

This is not going to be a gigantic task to handle, but it’s also not something you would be comfortable running for a couple of hours at least…

Maybe don’t use Python?

You already have Terraform, you know how to use it, the documentation is easy to follow, the GitHub provider works well, you don’t even have to care about their weird API versioning. Terraform is almost just pure configuration, so if you name your things well it will be self-documenting. You also get a statefile, a way to review your changes before applying them (avoid writing tests with this one simple trick), and you can also self-correct if someone goes in the future and breaks a ruleset for any repository within your scope.

Get to it

data "github_repositories" "this" {
  # everything you can do in GitHub's search you can do here
  query            = "org:cyberdyne-systems"
  results_per_page = 50
}

resource "github_repository_ruleset" "this" {
  for_each = toset(data.github_repositories.this.names)

  enforcement = "active"
  name        = "everything in a pr"
  repository  = each.key
  target      = "branch"

  conditions {
    ref_name {
      exclude = []
      include = ["~DEFAULT_BRANCH"]
    }
  }

  rules {
    pull_request {
      dismiss_stale_reviews_on_push     = true
      require_code_owner_review         = true
      require_last_push_approval        = true
      required_approving_review_count   = 1
      required_review_thread_resolution = true
    }
  }
}

# If you're logged in to the gh cli tool, that's it.
provider "github" {}

terraform {
  required_providers {
    github = {
      source  = "integrations/github"
      version = "~> 6.7.0"
    }
  }
}

Now, simply do what you already know how to do:

$ terraform init

Initializing the backend...
Initializing provider plugins...
- Reusing previous version of integrations/github from the dependency lock file
- Installing integrations/github v6.7.3...
- Installed integrations/github v6.7.3 (signed by a HashiCorp partner, key ID 38027F80D7FD5FB2)
Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://developer.hashicorp.com/terraform/cli/plugins/signing

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other

$ terraform plan -out planfile

data.github_repositories.this: Reading...
data.github_repositories.this: Read complete after 1s [id=org:cyberdyne-systems]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # github_repository_ruleset.this["T-101"] will be created
  + resource "github_repository_ruleset" "this" {
      + enforcement = "active"
      + etag        = (known after apply)
      + id          = (known after apply)
      + name        = "everything in a pr"
      + node_id     = (known after apply)
      + repository  = "T-101"
      + ruleset_id  = (known after apply)
      + target      = "branch"

      + conditions {
          + ref_name {
              + exclude = []
              + include = [
                  + "~DEFAULT_BRANCH",
                ]
            }
        }

      + rules {
          + update_allows_fetch_and_merge = false

          + pull_request {
              + dismiss_stale_reviews_on_push     = true
              + require_code_owner_review         = true
              + require_last_push_approval        = true
              + required_approving_review_count   = 1
              + required_review_thread_resolution = true
            }
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: planfile

To perform exactly these actions, run the following command to apply:
    terraform apply "planfile"
This is not going to work if you don’t have permissions to do so… It’s not that easy to break into other users’ repos, I hope?

What! That looks awesome, terraform apply planfile and you’re done!

In this example, there’s only one repository for cyberdyne-systems, but it would also work for any repository you actually have enough permissions for to begin with.

Benefits

Caveats

Fin

Be careful with this. REVIEW YOUR PLANS before applying.

You could also do this with your standard Terraform flow and bring all those resources into IaC management as they should have been in the first place.

That would be a nice bonus: instead of “hey, I was able to do this in 30 seconds and it would have taken you three work days to do so,” you could actually maintain this in GitHub Actions with a cronjob that runs apply on its own to remediate drift (to some degree).

You’d get audit logs and approvers for any changes, and you won’t be the only one to blame if something goes wrong.

Spider Man Uncle Ben GIFfrom Spider Man GIFs