Environments
A TerraDart project declares its environments in Dart: an enum of your own, one member per environment, each carrying that environment’s values. runEnvironments builds one Stack per member, and terradart apply --env <name> runs one of them and writes that environment’s define file for the client. Nothing is configured in pubspec.yaml.
Declare the environments
Section titled “Declare the environments”Any members, any fields. The Stack takes one member and derives everything from it — the project, the sizes, and where its state lives, so one environment can keep a local state file while another uses a GCS bucket:
enum Env { dev(projectId: 'acme-dev', stateBucket: null), stg(projectId: 'acme-stg', stateBucket: 'acme-stg-tfstate'), prod(projectId: 'acme-prod', stateBucket: 'acme-prod-tfstate');
const Env({required this.projectId, required this.stateBucket});
final String projectId;
/// The GCS bucket of the state; `null` keeps a local file. final String? stateBucket;}import 'package:my_app/env.dart';import 'package:terradart_google/provider.dart';
final class AppStack extends Stack { AppStack({required Env env}) : super( providers: [GoogleProvider(project: env.projectId)], backend: switch (env.stateBucket) { final bucket? => GcsBackend(bucket: bucket, prefix: 'app'), null => LocalBackend(path: 'state/${env.name}.tfstate'), }, ) { addOutput('api_url', .literal('https://api.${env.projectId}.example.com')); addDartDefineOutput(); }}runEnvironments takes the members and builds the Stack for the one the command names:
import 'package:my_app/app_stack.dart';import 'package:my_app/env.dart';import 'package:terradart_core/terradart_core.dart';
Future<void> main(List<String> args) => runEnvironments(args, Env.values, (env) => AppStack(env: env));Run one environment
Section titled “Run one environment”terradart synth without --env writes every environment. validate, plan, apply, destroy and outputs run against one, the first of:
--env <name>(-e), a member’s name;- the
TERRADART_ENVenvironment variable — ignored by an entry point that callsrunStack; - the
defaultEnvthe entry point givesrunEnvironments; - the only environment, when the enum has one member.
With none of these, the command stops and lists the names. It prints which one it chose and why — env: dev (--env), env: dev (TERRADART_ENV), env: dev (default), env: dev (only environment):
import 'package:my_app/app_stack.dart';import 'package:my_app/env.dart';import 'package:terradart_core/terradart_core.dart';
Future<void> main(List<String> args) => runEnvironments( args, Env.values, (env) => AppStack(env: env), defaultEnv: Env.dev,);When TERRADART_ENV or defaultEnv chose the environment, apply and destroy ask first, and run only on yes:
$ terradart apply> dart run bin/infra.dart...env: dev (default)Apply environment "dev" (default)? Only "yes" is accepted:--auto-approve skips the question, as it skips the engine’s. Without an answer to read (no terminal, or input closed), the command stops before init; pass --env or --auto-approve in CI.
terradart validate --env stgterradart plan --env stgterradart apply --env stgThe names are the enum’s, not a fixed set: qa, sandbox, euWest work the same. A name that is not a member stops before anything runs, listing the ones that are:
$ terradart plan --env staging> dart run bin/infra.dart --env staginginfra: unknown environment "staging"; known envs: dev, stg, prod.terradart: synth failed: bin/infra.dart exited 64.Build each client with its environment
Section titled “Build each client with its environment”Each environment gets its own Terraform directory and its own define file, named after it. After apply, terradart writes the define file and prints the line that builds the client with it:
| Command | Terraform directory | Define file | Client build |
|---|---|---|---|
terradart apply --env dev | tf-out/dev | .terradart/dart_defines.dev.json | flutter run --dart-define-from-file=.terradart/dart_defines.dev.json |
terradart apply --env stg | tf-out/stg | .terradart/dart_defines.stg.json | flutter build web --dart-define-from-file=.terradart/dart_defines.stg.json |
terradart apply --env prod | tf-out/prod | .terradart/dart_defines.prod.json | flutter build ipa --dart-define-from-file=.terradart/dart_defines.prod.json |
A client’s build job does not apply anything: terradart outputs --env prod writes the same file from the applied state, and the build compiles it in.
terradart outputs --env prodflutter build web --dart-define-from-file=.terradart/dart_defines.prod.jsonThe app reads every value with its type through the generated <Stack>Outputs reader, whichever environment it was built for. Outputs in client apps covers the reader, the values a define file may carry, and why a secret never goes in one.
One directory, one backend, several states
Section titled “One directory, one backend, several states”When every environment keeps its state in the same kind of backend and only its settings differ, the environments can share one directory and tell their states apart when terradart initializes it. runEnvironments says how, in Dart:
-
Partial backend configuration. The Stack’s backend leaves the settings out (
const GcsBackend());backendConfignames a-backend-configfile (relative to the package) orkey=valuepairs per environment.terradartrunsinit -reconfigurewith them on every command, so the directory never keeps another environment’s backend.import 'package:my_app/app_stack.dart';import 'package:my_app/env.dart';import 'package:terradart_core/terradart_core.dart';Future<void> main(List<String> args) => runEnvironments(args,Env.values,(env) => AppStack(env: env),dir: (_) => 'tf-out',backendConfig: (env) => ['backend/${env.name}.gcs.tfbackend'],); -
Workspaces.
workspacenames the Terraform workspaceterradartselects afterinit, creating it on the firstplanorapply.import 'package:my_app/app_stack.dart';import 'package:my_app/env.dart';import 'package:terradart_core/terradart_core.dart';Future<void> main(List<String> args) => runEnvironments(args,Env.values,(env) => AppStack(env: env),dir: (_) => 'tf-out',workspace: (env) => env.name,);
Two environments may share a directory only when one of these tells them apart; runEnvironments throws otherwise. --workspace and --backend-config on the command line override and extend them for one run.
Moving an environment’s state
Section titled “Moving an environment’s state”An environment’s backend is Dart too, so moving its state starts with a change to the Stack — giving stg a stateBucket, say, where it kept a local file. The state then follows once:
terradart state migrate --env stgIt synthesizes stg, names the full source and target configuration (the bucket and prefix, not only the backend type), asks, and runs the engine’s init -migrate-state in stg’s directory with its backendConfig. --auto-approve skips the question, and is required when it cannot ask (no terminal, --no-input, CI or an agent). Run it before the next plan or apply of stg: those reconfigure the directory onto the new backend without copying. When environments share that directory, the copy is the state of the environment that last initialized it. If that was another environment, or TerraDart has no record of which one, it stops and tells you to run terradart plan --env stg first — it does not copy, and --auto-approve does not skip the stop. The other environments are not touched.
Migrated environments
Section titled “Migrated environments”terradart migrate --merge-envs writes an Env enum whose members carry the directory each environment came from (path), and a bin/infra.dart that calls runEnvironments with dir: (env) => 'tf-out/${env.path}'. terradart plan --env prodEu plans the matching environment. A single-module migration calls runStack instead, so terradart plan with no --env is enough. See Migrating from HCL.