Terraform has become the default way teams stand up TLS certificates across AWS Certificate Manager, GCP Certificate Manager, and Azure Key Vault, but treating certificate resources like any other piece of infrastructure code introduces failure modes that don’t show up with EC2 instances or storage buckets. This article covers how to declare, validate, and rotate SSL certificates through Terraform without creating gaps that plain infrastructure-as-code workflows tend to miss.
Why certificate resources behave differently in Terraform state
A Terraform resource for a compute instance is idempotent in a way a certificate resource is not. An aws_acm_certificate resource with DNS validation depends on a separate aws_acm_certificate_validation resource, a Route 53 record, and a CA’s willingness to issue within its rate limits. If any of those three steps times out during apply, Terraform can leave the certificate in a PENDING_VALIDATION state indefinitely while the state file thinks the resource exists.
This matters because a certificate stuck in that state doesn’t serve traffic. Teams running `terraform apply` in CI, without checking the actual issuance status afterward, have shipped deploys where the certificate resource “applied successfully” per Terraform’s exit code, but the load balancer listener attached to it was serving the old cert or none at all.
The lesson: Terraform apply succeeding is not the same as a certificate being live and trusted. Verification has to happen outside the Terraform run itself.
Declaring a certificate resource correctly
A typical AWS ACM setup with DNS validation looks like this:
resource “aws_acm_certificate” “main” {
domain_name = “app.example.com”
validation_method = “DNS”
subject_alternative_names = [“www.app.example.com”]
lifecycle {
create_before_destroy = true
}
}
The `create_before_destroy` lifecycle block is not optional decoration — without it, a certificate renewal or SAN change destroys the old certificate before the new one is validated and attached, causing a window where the listener has nothing to present. This is one of the most common mistakes practitioners make when writing their first ACM Terraform module: they copy an example without the lifecycle block, and the first renewal cycle takes the site down for the two to five minutes DNS validation takes to propagate.
For GCP, the equivalent resource is `google_certificate_manager_certificate`, and for Azure it’s typically a `azurerm_key_vault_certificate` combined with an issuer policy for auto-rotation. Each cloud has different default renewal windows — ACM renews automatically 60 days before expiration if DNS validation records remain in place, but only if those records were never removed after the initial validation. A surprising number of teams delete the validation CNAME once the certificate first issues, thinking it’s a one-time setup step, which silently breaks all future auto-renewals.
Handling multi-cloud and hybrid certificate declarations
Organizations running workloads across AWS and GCP, or using Cloudflare in front of an origin certificate, end up managing certificate lifecycle across providers with different validation semantics inside the same Terraform state. The pattern that holds up under review is to separate certificate provisioning into its own state file or workspace from the compute and networking resources that consume the certificate ARN or ID, referenced via a `terraform_remote_state` data source or output variable.
Mixing certificate and compute resources in the same state file means a `terraform destroy` scoped to tear down a broken deployment can inadvertently revoke a certificate that other listeners still reference. Keeping certificate state separate limits blast radius when something in the apply needs to be rolled back.
What CI/CD pipelines get wrong about certificate validation
Terraform plan and apply logs only tell you what changed in the API calls, not what a browser or `openssl s_client` sees when connecting to the endpoint. Three mistakes show up repeatedly in production pipelines:
– Treating a successful `terraform apply` as proof the certificate is trusted by browsers, when a chain issue (missing intermediate) can still produce an untrusted connection despite ACM reporting ISSUED.
– Never checking that the SAN list in the applied certificate actually matches what the load balancer listener expects, especially after someone edits the `subject_alternative_names` list and forgets a subdomain used by an internal service.
– Running certificate provisioning and DNS record creation in the same apply without a `depends_on` or explicit wait, causing race conditions where validation fails on first apply and succeeds silently on the automatic retry, masking a real DNS delegation problem.
A seasoned SRE treats a Terraform-provisioned certificate the same way they’d treat any external dependency: verify it’s actually doing what the state file claims, using something outside Terraform’s own view of the world. That’s a fundamentally different check than a green pipeline. More detail on wiring certificate checks into CI/CD is covered in SSL Certificate Management in CI/CD Pipelines.
Busting the myth that Terraform makes certificates “set and forget”
A common misconception is that once a certificate is defined as code and applied, expiration risk disappears because Terraform will “keep it in sync.” That’s not how most providers behave. ACM auto-renews only under specific conditions (DNS validation records present, certificate in use by an attached resource, and issued via ACM itself rather than imported). An imported third-party certificate, common when a company already owns a wildcard cert from DigiCert or Sectigo, does not auto-renew in Terraform at all — someone has to manually rotate the PEM file and re-run apply before the fixed expiration date, typically 13 months out for OV/EV certs issued after the CA/Browser Forum’s 2020 shortening of max validity.
Terraform code guarantees the desired state is declared correctly. It does not guarantee that state stays true over time, especially for imported certificates or across a Route 53 delegation change that breaks silent renewal without triggering any Terraform diff.
Monitoring what Terraform can’t see
Because Terraform’s view stops at “did the API call succeed,” continuous external monitoring is the part that catches drift between declared and actual state — expired imported certs, broken chains, DNS validation records someone deleted six months after go-live, or a SAN mismatch introduced by a manual console change outside of Terraform entirely. Running periodic external checks against the live endpoint, independent of the apply pipeline, with 30/14/7/1-day expiration warnings, closes the gap that infrastructure-as-code alone leaves open. This is particularly relevant for teams running certificate provisioning across AWS, GCP, and Azure Key Vault simultaneously, where keeping track of renewal windows and issuer policies per cloud by hand becomes unmanageable past a handful of domains — see How to Monitor SSL Certificates Across Multi-Cloud Environments for the multi-cloud specific patterns.
Frequently asked questions
Does Terraform automatically renew SSL certificates?
Only indirectly, and only for cloud-native certificates like AWS ACM or GCP Certificate Manager where the underlying service handles renewal itself, provided the validation records remain in place and the certificate stays attached to an active resource. Terraform doesn’t run a renewal process on its own; it just needs to not have destroyed the validation infrastructure that the cloud provider’s renewal job depends on.
Should certificate provisioning live in the same Terraform state as the application infrastructure?
Generally no, past a small team or single-service setup. Separating certificate state from compute and networking state limits the blast radius of a bad apply or destroy, and lets certificate rotation happen on its own cadence without touching unrelated resources.
What happens if a Terraform apply fails partway through certificate validation?
The certificate resource can be left in a pending state in the Terraform state file while not actually serving traffic anywhere. Re-running apply usually resolves it if the DNS records are correct, but if the failure was caused by a rate limit or a broken DNS delegation, it needs manual investigation before rerunning — blindly retrying just repeats the same failure.
Terraform is good at declaring what a certificate setup should look like and keeping that declaration under version control, but it was never designed to answer the question “is this certificate actually valid and trusted right now.” That’s a separate, continuous check that belongs outside the apply pipeline, running against the live endpoint on its own schedule regardless of what the last `terraform plan` reported.
