Skip to content

CI/CD

raya’s repository is hosted on GitHub, and leverages GitHub Actions for continuous integration and deployment.

Workflows

docs.yml

This is the workflow generated by zensical new. It ensures this documentation is re-generated and published every time a new commit lands on the main branch. It needs no repository secret: it authenticates to GitHub Pages through OIDC, thanks to the id-token: write permission.

provision-checks.yml

This workflow runs on Pull Requests. It

  • Runs various Packer built-in checks to ensure the correctness of the CoreOS image Packer template.
  • Runs various Terraform built-in checks to ensure the correctness of the committed files.
  • Runs terraform plan to show the impact of the change on production, so it is reviewed before it lands.

This is a read-only workflow. No deployment happens when it runs.

provision.yml

This workflow runs on every push to the main branch. It

  • Builds the Fedora CoreOS image used pervasively in raya.
  • Runs terraform apply to actually provision raya. This job needs the previous one to have finished to gracefully handle a change in the Fedora CoreOS image.

Additionally, this workflow can be run manually from the GitHub interface. It can be run against any branch, not only main. If it is run against a branch different than main, only the first job (building the provisioning image) is run.

This is useful when the image needs to be reconstructed following a dependency bump (e.g., upgrading to a new k3s version). In that case, terraform plan will fail because the selectors of data.hcloud_image.fcos won’t resolve (the image does not exist yet). Building a new image does not affect the production, and unblock running terraform apply.

Note

Images are snapshots stored in Hetzner Cloud, and they are never deleted by this IaC. Bookkeeping remains manual for now.

Terraform Backend

Terraform state is hosted on HCP Terraform, under the raya workspace belonging to the lthms organization. Its execution mode is set to Local.

Variable Management

Terraform inputs are split in two, depending on whether or not their values can be committed to the repository.

Non-sensitive inputs (sizing, locations, the subdomain the cluster manages, the list of authorized GitHub handles) are declared in variables.tf, and their production values live in prod.tfvars. Both provisioning workflows pass that file explicitly, so terraform plan and terraform apply see the same configuration.

terraform plan -var-file prod.tfvars
terraform apply -auto-approve -var-file prod.tfvars

Sensitive inputs are declared in secrets.tf. They never appear in prod.tfvars, but instead come from repository secrets.

Secret Management

To let the GitHub runners connect to the various services involved in raya’s deployment, several repository secrets were created:

Secret Role
TF_API_TOKEN Authentication to HCP Terraform
HCLOUD_TOKEN Authentication to Hetzner Cloud
HCLOUD_CLUSTER_TOKEN Authentication to Hetzner Cloud, for the components running inside the cluster
BETTERSTACK_TOKEN Authentication to BetterStack
GCP_TERRAFORM_CREDENTIALS Authentication to Google Cloud, for Cloud DNS

These secrets are exposed to both provisioning workflows (.github/workflows/provision-checks.yml and .github/workflows/provision.yml) as environment variables.

TF_API_TOKEN is fed to Terraform via the TF_TOKEN_app_terraform_io environment variable (since we are using app.terraform.io)1.

The other ones hold values intended for sensitive Terraform variables declared in secrets.tf. Such values are fed to Terraform using the TF_VAR_ prefix convention. For instance, HCLOUD_TOKEN initializes the hcloud_token variable by setting TF_VAR_hcloud_token2.

TF_VAR_hcloud_token: ${{ secrets.HCLOUD_TOKEN }}

HCLOUD_TOKEN is also passed under its own name to the Packer jobs, where image/build.sh queries the Hetzner API with it and Packer authenticates the build. That use has nothing to do with Terraform variables, which is why the same secret appears under two different names depending on the job.

HCLOUD_CLUSTER_TOKEN is a second, distinct Hetzner token, created by hand in the console. It is never used by Terraform itself: it is handed to the cluster, for the components running there that need to talk to the Hetzner API. Today that is the CSI driver, which authenticates with it to create and attach volumes. Hetzner tokens cannot be scoped, so keeping the two apart does not reduce what the cluster can do — it means the cluster's token can be revoked without locking the deployment pipeline out.

Warning

Because the workspace uses Local execution, terraform plan runs on the GitHub runner and its output is written to a public Actions log. Any sensitive Terraform variable must therefore be declared as such (sensitive = true).

Note that this only redacts variables: resource attributes (addresses, user_data, key fingerprints) may still appear in cleartext by default. Wrap any expression carrying a secret in sensitive() when its value does not already derive from a sensitive variable. For instance, user_data = sensitive(data.ct_config.control_plane.rendered).


  1. See Terraform documentation on the subject 

  2. TF_VAR_-prefixed environment variables are consumed by Terraform because the raya workspace uses the Local execution mode.