Terraform as a Scripting Language

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
    }
  }
}

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!
  
        $ 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"
      + name        = "everything in a pr"
      + repository  = "T-101"
      + 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
            }
        }
    }

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

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#

  • 43-line-long script, including blank spaces and comments.
  • Review your changes before they happen.
  • Sense of superiority over your coworkers.
  • Ease of understanding and use (vs. regular scripts)

Caveats#

  • You'll get all Terraform powers with this, so be careful, you could as easily nuke your entire organization.
  • You get a state file to take care of (though you can just delete it when you're done).
  • People will not stop throwing tasks at you to test your superpowers.

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.