How to integrate OpenTofu with CI/CD
How to integrate OpenTofu with CI/CD
Running OpenTofu in a CI/CD pipeline eliminates manual applies, ensures consistent infrastructure changes, and creates an audit trail. This guide shows GitHub Actions and GitLab CI configurations for Quake AI.
Prerequisites#
- An OpenTofu project with remote state configured (see state management guide)
- Quake AI OpenStack credentials and S3 credentials
- A GitHub or GitLab repository
Secrets configuration#
Both CI platforms need your Quake AI credentials stored as secrets. Use application credentials (the same credentials produced by Generate app credentials) so CI pipelines authenticate without a user password. Add these as CI/CD variables or repository secrets:
| Variable | Purpose |
|---|---|
OS_AUTH_URL | Quake AI Keystone endpoint |
OS_AUTH_TYPE | Always v3applicationcredential |
OS_APPLICATION_CREDENTIAL_ID | Application credential ID |
OS_APPLICATION_CREDENTIAL_SECRET | Application credential secret |
OS_REGION_NAME | Quake AI region |
AWS_ACCESS_KEY_ID | Quake AI S3 access key (for remote state) |
AWS_SECRET_ACCESS_KEY | Quake AI S3 secret key (for remote state) |
GitHub actions#
Plan on pull request, apply on merge#
This workflow runs tofu plan on every pull request and tofu apply when changes merge to main:
name: Infrastructure
on:
pull_request:
paths:
- 'infrastructure/**'
push:
branches:
- main
paths:
- 'infrastructure/**'
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
defaults:
run:
working-directory: infrastructure
env:
OS_AUTH_URL: ${{ secrets.OS_AUTH_URL }}
OS_AUTH_TYPE: v3applicationcredential
OS_APPLICATION_CREDENTIAL_ID: ${{ secrets.OS_APPLICATION_CREDENTIAL_ID }}
OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
OS_REGION_NAME: ${{ secrets.OS_REGION_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
- run: tofu init
- run: tofu validate
- run: tofu plan -no-color -out=plan.out
- name: Post plan to PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
const plan = execSync('cd infrastructure && tofu show -no-color plan.out').toString();
const body = `### OpenTofu Plan\n\n\`\`\`\n${plan.slice(0, 60000)}\n\`\`\``;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
apply:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
defaults:
run:
working-directory: infrastructure
env:
OS_AUTH_URL: ${{ secrets.OS_AUTH_URL }}
OS_AUTH_TYPE: v3applicationcredential
OS_APPLICATION_CREDENTIAL_ID: ${{ secrets.OS_APPLICATION_CREDENTIAL_ID }}
OS_APPLICATION_CREDENTIAL_SECRET: ${{ secrets.OS_APPLICATION_CREDENTIAL_SECRET }}
OS_REGION_NAME: ${{ secrets.OS_REGION_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
- run: tofu init
- run: tofu apply -auto-approveMulti-environment workflow#
For projects with separate environment variable files, extend the workflow with a matrix:
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, production]
max-parallel: 1
defaults:
run:
working-directory: infrastructure
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
- run: tofu init -backend-config="key=${{ matrix.environment }}/terraform.tfstate"
- run: tofu apply -auto-approve -var-file=envs/${{ matrix.environment }}.tfvarsThe max-parallel: 1 setting ensures environments deploy sequentially: dev before staging before production.
GitLab CI#
Plan on merge request, apply on merge#
stages:
- validate
- plan
- apply
variables:
TF_ROOT: infrastructure
.tofu_base:
image: ghcr.io/opentofu/opentofu:latest
before_script:
- cd $TF_ROOT
- tofu init
validate:
extends: .tofu_base
stage: validate
script:
- tofu validate
plan:
extends: .tofu_base
stage: plan
script:
- tofu plan -out=plan.cache
artifacts:
paths:
- $TF_ROOT/plan.cache
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
apply:
extends: .tofu_base
stage: apply
script:
- tofu apply plan.cache
dependencies:
- plan
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
environment:
name: productionThe when: manual gate on the apply stage requires a human click in the GitLab UI before changes apply to production.
Store secrets in GitLab CI/CD variables#
Navigate to Settings > CI/CD > Variables in your GitLab project. Add each environment variable listed in the secrets table above. Mark them as Masked and Protected (restrict to protected branches only).
Validation-only pipeline#
If you are not ready for automated applies, start with a validation-only pipeline that checks template syntax on every commit:
name: Validate Templates
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: opentofu/setup-opentofu@v1
- name: Validate all templates
run: |
for dir in iac/templates/*/; do
if ls "$dir"*.tf &>/dev/null; then
echo "Validating $dir..."
cd "$dir"
tofu init -backend=false
tofu validate
cd - > /dev/null
fi
doneThis is the approach the Quake AI developer platform uses for its own template library.
Best practices#
- Plan on PR, apply on merge. Never auto-apply on pull requests. Review the plan output before merging.
- Serialize applies. Use
max-parallel: 1orresource_groupto prevent concurrent state writes. - Pin provider versions. Use
version = "~> 2.0"constraints inrequired_providersto avoid unexpected upgrades. - Cache provider downloads. The OpenStack and AWS providers are large. Cache the
.terraform/providersdirectory between runs to speed uptofu init. - Separate state per environment. Use different state keys or backend configs to isolate environments (see multi-environment guide).
See also#
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
Last validated: 25.05.2026
See Also
Terraform and OpenTofu on Quake AI
Prerequisite
How to add a block volume to a template
Shares: IaC, Terraform
How to deploy an application to a Quake AI VM from CI
Shares: IaC, Terraform
How to emulate a branchable Postgres workflow
Shares: IaC, Terraform
How to customize a template's image and flavor
Shares: IaC, Terraform