Blog · 2026-10-19 · Gardience Engineering

Terraform: configure servers on first boot, Windows and Linux

Terraform builds the machine; something has to configure it. How to make every instance join configuration management, get its baseline in seconds, and leave the fleet on destroy, with the Gardience provider.

The common pattern is Terraform to provision and Ansible (or Salt, Chef, Puppet) to configure. It works, but the handoff is the weak spot: a provisioner block that SSHes in, a pipeline step that runs a playbook after apply, an inventory that has to know about new machines, and nothing that cleans up when a machine is destroyed.

A cleaner handoff: the machine enrolls itself on first boot, and the configuration system reacts to its arrival.

The idea

  1. Terraform creates an enrollment in the configuration system and gets a first-boot script.
  2. The script goes in the instance's user_data (cloud-init on Linux, PowerShell on Windows).
  3. On first boot the machine installs the management agent, joins, and lands in its groups.
  4. A join rule applies the machine's state the moment it arrives. No inventory, no SSH from the pipeline.
  5. On terraform destroy, the machine is removed from the fleet.

With the Gardience provider

Gardience is a supported console on open-source Salt for Windows and Linux, and its provider for Terraform and OpenTofu works this way:

terraform {
  required_providers {
    gardience = { source = "gardience/gardience" }
  }
}

provider "gardience" {} # GARDIENCE_URL and GARDIENCE_API_KEY

resource "gardience_enrollment" "web" {
  label  = "web tier"
  hours  = 2160 # open for 90 days
  groups = ["web"]
}

resource "gardience_state" "web_base" {
  name    = "web_base"
  content = <<-YAML
    nginx:
      pkg.installed: []
      service.running:
        - enable: True
  YAML
}

resource "gardience_event_rule" "web_join" {
  id    = "web-join"
  kind  = "join"
  scope = "group"
  group = "web"
  state = gardience_state.web_base.name
}

data "gardience_bootstrap_script" "web1" {
  enrollment_id = gardience_enrollment.web.id
  os            = "linux"
  node          = "web-1"
}

resource "aws_instance" "web1" {
  ami           = var.ami
  instance_type = "t3.small"
  user_data     = data.gardience_bootstrap_script.web1.content
}

resource "gardience_machine" "web1" {
  name       = "web-1"
  depends_on = [aws_instance.web1]
}

What happens on terraform apply:

On terraform destroy, gardience_machine removes the machine from the fleet, so dead machines never pile up.

For Windows, use os = "windows" and wrap the script for your platform; on AWS: user_data = "<powershell>${data.gardience_bootstrap_script.win1.content}</powershell>".

Why this beats a provisioner block

Provisioner or pipeline stepFirst-boot enrollment plus join rule
NetworkPipeline needs SSH or WinRM into the new machineMachine dials out; nothing inbound
CredentialsPipeline holds host credentialsPipeline holds one scoped API key
Autoscaling groupsPipeline does not see new instancesEvery instance enrolls itself
CleanupManualRemoved on destroy
AuditPipeline logsEvery change in a tamper-evident audit log

Guardrails worth knowing

Questions

Does this work with OpenTofu?

Yes. The provider supports both Terraform and OpenTofu.

Can I keep using Ansible playbooks for configuration?

Yes. Gardience runs existing Ansible playbooks over the Salt connection, on Windows and Linux, without SSH keys or WinRM.

What about autoscaling groups?

Put the enrollment's generic script (linux_script or windows_script) in the launch template. Every instance the group creates enrolls itself and gets its state from the join rule.

Try Gardience on your own machines

One supported console for Windows and Linux on open-source Salt: patching, a scored STIG/CIS-aligned baseline, drift repair, event-driven automation, two-person approval and a tamper-evident audit log. Flat yearly pricing, not per node.

Free 90-day trial (25 nodes)See it work Compare

Salt is a trademark of its owner, Ansible is a trademark of Red Hat, Inc., and other names are trademarks of their owners. Gardience is independent and not affiliated with any of them.