OpenAI shipped the official Terraform Provider 1.0.0 on 29 July 2026. It uses the Administration API so projects, members, roles, service accounts, certificates, and rate limits live in config instead of console clicks.
Registry source: openai/openai. GitHub repo: openai/terraform-provider-openai. Don’t mix the two names.
This is a verified rewrite of KnightLi’s guide. Resource behavior was checked against official docs. Permission strings, role IDs, and model IDs still need a live check against the current API and data sources before you merge.
What it manages—and what it doesn’t
Control plane only: projects and members, invites and groups, service accounts, certificates, model and hosted-tool permissions, data retention, spend alerts, project rate limits. Data sources look up existing IDs so you don’t hard-code everything.
It does not call model APIs. An Admin API key cannot hit Chat Completions / Responses. A normal project key cannot hit the Administration API.
Don’t just bump the version to 1.0.0
The stable changelog has a breaking change: the deprecated aggregate project rate-limit resource is gone. Use one openai_project_rate_limit per existing rate_limit_id. Request resilience and telemetry also changed; production still means plan first, then apply.
Prerequisites and init
You need Terraform CLI ≥ 1.0 (import blocks need ≥ 1.5), org permission to create an Admin key, and an encrypted remote state backend.
export OPENAI_ADMIN_KEY="<your-admin-api-key>"
# optional: OPENAI_ORG_ID, OPENAI_PROJECT_ID
PowerShell: $env:OPENAI_ADMIN_KEY = "<your-admin-api-key>"
Do not put the admin key in .tf files, variable defaults, or git. Inject it from a secret store in CI, and lock down who can read logs and state.
terraform {
required_version = ">= 1.0"
required_providers {
openai = {
source = "openai/openai"
version = "~> 1.0"
}
}
}
provider "openai" {
# reads OPENAI_ADMIN_KEY by default
}
Commit .terraform.lock.hcl after terraform init.
Projects, members, invites
A project can be just a name:
resource "openai_project" "production" {
name = "production-api"
}
output "production_project_id" {
value = openai_project.production.project_id
}
geography is optional—don’t change a production region because an example had it. fmt, validate, plan -out=tfplan, then apply.
Add an existing org user:
resource "openai_project_user" "operator" {
project_id = openai_project.production.project_id
user_id = var.operator_user_id
role = "member"
}
Built-in project roles can go through openai_project_user_role with a role_id. Safer: discover roles with the openai_project_roles data source. Custom roles need project_id, role_name, and permissions from the real permission set.
People not yet in the org use openai_invite, optionally with project membership. After they accept, plan again and check remote state against config.
Service accounts do not emit API keys
resource "openai_project_service_account" "deploy" {
project_id = openai_project.production.project_id
name = "production-deploy"
}
The provider docs are explicit: this resource sets create_service_account_only=true. It creates the account only—no roles, no API key. Assign roles with other Terraform resources; create keys outside Terraform via the public API. Don’t assume a usable secret in module outputs.
Certificates, model access, rate limits
Org certificates can be read from a PEM file. sensitive on certificate does not keep it out of state. Encrypt remote state. On rotation, inspect the plan for replace and overlap validity windows.
Model allow-lists use IDs that change with the product. Check currently available models before merge; don’t treat an example model name as stable.
openai_project_rate_limit manages an existing limit. You must pass project_id and rate_limit_id:
resource "openai_project_rate_limit" "primary_model" {
project_id = openai_project.production.project_id
rate_limit_id = "rate_limit_123"
max_requests_per_1_minute = 500
max_tokens_per_1_minute = 200000
}
You can also set daily requests, batch daily input tokens, images per minute, and audio MB per minute. Terraform can only write values the API accepts; bigger numbers do not bypass org quotas. That is why you cannot “create a new rate-limit object” in 1.0: the resource reconciles an existing ID.
Import first; use plan for drift
Don’t apply a second copy of a console-created project. Terraform 1.5+ import blocks use resource-specific IDs (bare ID, project_id/resource_id, or longer composites)—see each resource’s Import section:
import {
to = openai_project.production
id = "proj_123"
}
import {
to = openai_project_service_account.deploy
id = "proj_123/service_account_123"
}
import {
to = openai_project_rate_limit.primary_model
id = "proj_123/rate_limit_123"
}
Then terraform plan. A huge update list means local config does not match remote—fix config, don’t overlay production.
In CI, terraform plan -detailed-exitcode:
0— no changes1— command failed2— changes or drift; a human should look
Do not auto-apply on 2. Drift might be an emergency console fix, a revoked grant, or a change that should land back in git.
State and review
Treat the Admin key as high privilege: short-lived local injection, dedicated CI identity, encrypted locked state, split plan/apply approval, key rotation. Watch destroy / replace before deleting projects, members, roles, or certificates. prevent_destroy is a seatbelt, not a review process.
The valuable part of adopting this provider is not one apply. It is using the Admin key only where it belongs, importing what already exists, and making plan the gate for permission and quota changes.