End-to-end integration tests for the Materialize self-managed Terraform modules. The test harness deploys real infrastructure on AWS, Azure, or GCP, verifies that Materialize is running, and tears it down. It can also run entirely locally against a kind cluster -- see Local kind runs.
- Rust (edition 2024)
- Terraform >= 1.10
psql(PostgreSQL client)- Cloud CLI for your provider:
- AWS:
awsCLI, configured profile - GCP:
gcloudCLI, authenticated - Azure:
azCLI, authenticated
- AWS:
Build the test binary:
cd test
cargo buildRun the full lifecycle (init, apply, verify, destroy) in one command:
cargo run -- run aws \
--owner "Your Name" \
--license-key-file /path/to/license.key \
--aws-region us-east-1 \
--aws-profile my-profileThe kind provider runs the non-cloud-specific part of the test surface on your laptop, with no cloud credentials. It deploys kubernetes/examples/simple -- the customer-facing example for existing (non-cloud-managed) clusters -- onto a local kind cluster, with in-cluster PostgreSQL (metadata) and RustFS (persist) test backends from test/kind/backends.yaml standing in for the backends a customer would bring. This exercises the kubernetes/ modules (cert-manager, self-signed-cluster-issuer, materialize-instance); the cloud-only modules (networking, managed clusters, load balancers, IAM, monitoring) are out of scope by construction and remain covered by the cloud providers.
Additional prerequisites: Docker, kind, kubectl.
cargo run -- run kind --owner "Your Name" --license-key-file /path/to/license.keyinit creates a kind cluster named after the test run ID (so kind runs coexist with any other kind clusters and with each other), writes its kubeconfig into the test run directory, and deploys the test backends; apply deploys the example into it with terraform; verify runs the same checks as the cloud providers, except that the node-local-dns/CoreDNS checks are skipped (kind keeps its stock DNS) and SQL connectivity goes through a kubectl port-forward to balancerd instead of a load balancer; destroy deletes the kind cluster.
The staged workflow works as on the clouds, and is the fast path for iterating: keep the cluster up and re-run verify (or sync + apply after changing module code) without recreating anything:
cargo run -- init kind --owner "Your Name" --license-key-file key.txt
# => Test run initialized successfully: t260319-a4bc2f
cargo run -- apply --test-run t260319-a4bc2f
cargo run -- verify --test-run t260319-a4bc2f # repeatable
cargo run -- destroy --test-run t260319-a4bc2f --rmThe dev overrides (--local-chart-path, --orchestratord-version, --environmentd-version) work with kind too, which makes it the cheapest way to smoke-test a local chart or new image version.
Runs init, apply, verify, and destroy in sequence. On success the test run directory is cleaned up automatically.
cargo run -- run <provider> [OPTIONS]Copies the example Terraform files, generates a terraform.tfvars.json, and runs terraform init. Prints a test run ID for use with subsequent commands.
cargo run -- init aws \
--owner "Your Name" \
--license-key-file /path/to/license.key \
--aws-region us-east-1 \
--aws-profile my-profileRuns terraform apply for an initialized test run.
cargo run -- apply --test-run t260319-a4bc2fVerifies the deployment by:
- Configuring kubectl for the cluster
- Waiting for the Materialize custom resource to be UpToDate
- Waiting for all expected pods (environmentd, console, balancerd, clusterd) to be Running
- Connecting to Materialize via SQL and running
SELECT 1
cargo run -- verify --test-run t260319-a4bc2fRuns terraform destroy, up to three attempts. For AWS, it deletes the run's detached ENIs before each retry; left behind, they hold the EKS node security group and fail every attempt with DependencyViolation.
cargo run -- destroy --test-run t260319-a4bc2f
cargo run -- destroy --test-run t260319-a4bc2f --rm # also delete the test run directoryLists all test runs sorted by creation date.
cargo run -- list
cargo run -- list --latest # print only the most recentA last-resort cleanup for when terraform destroy fails and leaks resources. AWS
has no single-container delete, so purge finds a run's resources by the unique
per-run id: most carry a TestRun=<id> tag (or, for controller-created resources,
the cluster tag, which embeds the id), while a few are matched by the <id> name
prefix (IAM instance profiles and policies, the Karpenter SQS queue and EventBridge
rules) or by membership in the run's tagged VPC (security groups, ENIs). It only
ever acts on resources tied to that long, unique run id, independent of terraform
state.
cargo run -- purge --test-run t260319-a4bc2fA single invocation deletes resources in dependency order, waiting on the slow
deletions (EKS node groups and cluster, RDS instance, NAT gateways, load
balancers) before tearing down what they block, and repeats the whole sweep up to
five times (pausing 15s between passes) until nothing remains or no further
progress is made. So a normal run is fully reclaimed by one purge; only if
resources are still draining after five passes does it exit non-zero.
One exception: KMS keys cannot be deleted immediately, so the EKS encryption key
is scheduled for deletion with the 7-day minimum pending window rather than removed
outright. Some resource types are out of scope (see the module docs in
src/commands/purge.rs).
The AWS test workflow runs this automatically after every job.
The staged approach is useful for development -- you can init and apply once, then iterate on verify, and destroy when done:
cargo run -- init aws --owner "Your Name" --license-key-file key.txt --aws-region us-east-1 --aws-profile my-profile
# => Test run initialized successfully: t260319-a4bc2f
cargo run -- apply --test-run t260319-a4bc2f
cargo run -- verify --test-run t260319-a4bc2f
cargo run -- destroy --test-run t260319-a4bc2f --rmThe Materialize license key can be provided in three ways (in order of precedence):
--license-key <value>-- inline on the command line--license-key-file <path>-- read from a fileMATERIALIZE_LICENSE_KEYenvironment variable
| Argument | Description |
|---|---|
--aws-region |
AWS region (e.g. us-east-1) |
--aws-profile |
AWS CLI profile for authentication |
| Argument | Description |
|---|---|
--project-id |
GCP project ID |
--region |
GCP region (e.g. us-east1) |
| Argument | Description |
|---|---|
--subscription-id |
Azure subscription ID |
--resource-group-name |
Azure resource group name |
--location |
Azure location (e.g. westus2) |
No provider-specific arguments. Requires Docker and kind locally; the S3 backend options are accepted but unnecessary (state is small and local).
| Argument | Description | Default |
|---|---|---|
--owner |
Value for the Owner tag/label | (required) |
--purpose |
Value for the Purpose tag/label | Integration test |
--helm-chart |
Path to operator Helm chart | (optional) |
--use-local-chart |
Use local Helm chart | false |
--orchestratord-version |
Orchestratord image version | (optional) |
--environmentd-version |
Environmentd image version | (optional) |
--backend-s3-bucket |
S3 bucket for remote terraform state | (optional) |
--backend-s3-region |
S3 bucket region | us-east-1 |
--backend-s3-profile |
AWS profile for S3 backend auth | (optional) |
By default, terraform state is stored locally in the test run directory. To store state remotely in S3, pass the --backend-s3-bucket option (works with any provider):
cargo run -- run aws \
--owner "Your Name" \
--license-key-file key.txt \
--aws-region us-east-1 \
--aws-profile my-profile \
--backend-s3-bucket my-terraform-state-bucket \
--backend-s3-region us-east-1 \
--backend-s3-profile my-profileIf --backend-s3-profile is omitted, Terraform will use ambient AWS credentials (e.g. environment variables from OIDC).
The state key/prefix is automatically set to {test-run-id}/terraform.tfstate, keeping each test run's state isolated within the bucket.
Test runs are stored under test/runs/<id>/. Each directory contains:
- Copied
.tffiles (with rewritten module paths) terraform.tfvars.json-- generated variables.lifecycle-- tracks the current phase and status (e.g.apply completed)kubeconfig-- generated during verify- Terraform state and lock files
CI is split into six workflow files under .github/workflows/:
Triggered on every pull request to main. Runs the lint workflow and gates the PR on a ci-success job that verifies all lint checks passed.
Triggered when a PR enters GitHub's merge queue. Runs lint and the three provider test workflows (AWS, GCP, Azure) in parallel. The ci-success gate currently requires only lint to pass (provider tests are invoked but not yet blocking).
A reusable workflow (workflow_call) consumed by both pr.yml and merge_queue.yml. It runs three jobs:
- Terraform Lint --
terraform fmt -check -recursiveandtflint --recursive - Validate Simple Examples --
terraform init -backend=false && terraform validatefor each example directory (aws/examples/simple,azure/examples/simple,gcp/examples/simple) - Rust Tests Lint --
cargo fmt --check,cargo clippy -- -D warnings, andcargo deny checkon the test harness
Reusable workflow, also manually triggerable (workflow_dispatch). Authenticates via OIDC to assume an IAM role, then runs the full test lifecycle (cargo run -- run --destroy-on-failure aws ...) with remote S3 state. Smart path filtering skips the run if only GCP/Azure files changed. An always() step then runs cargo run -- purge for any run whose destroy did not complete, guaranteeing tag-scoped cleanup of leaked resources regardless of job outcome.
Same structure as AWS. Authenticates to GCP via Workload Identity Federation and to AWS via OIDC (for the S3 state backend). Skips if only AWS/Azure files changed.
Same structure as AWS. Authenticates to Azure via OIDC and to AWS via OIDC (for the S3 state backend). Skips if only AWS/GCP files changed.
Runs the self-managed kind lifecycle (cargo run -- run --destroy-on-failure kind) on a larger (ubuntu-24.04-16core) GitHub-hosted runner -- the stack's default resource requests do not fit the standard 4-CPU runner. Needs no cloud credentials, only the MATERIALIZE_LICENSE_KEY secret. Currently workflow_dispatch/workflow_call only -- not wired into the merge queue until it has proven reliable there.
All four test workflows use --destroy-on-failure to ensure infrastructure is torn down even on test failure; the three cloud workflows store Terraform state remotely in S3, while kind keeps state locally on the runner.
test/
Cargo.toml
README.md
runs/ # test run directories (gitignored)
kind/
cluster.yaml # kind cluster config (node labels for Materialize workloads)
backends.yaml # in-cluster PostgreSQL/RustFS test backends
src/
main.rs # CLI dispatch
cli.rs # argument definitions
types.rs # CloudProvider, TfVars, TerraformOutputs
helpers.rs # command execution, retry, lifecycle, ID generation
commands/
mod.rs # re-exports
init.rs # phase_init
apply.rs # phase_apply
verify.rs # phase_verify
destroy.rs # phase_destroy
list.rs # list command