> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/installation/kubernetes/terraform-aks.md).

# Azure AKS with Terraform

Provision an AKS cluster, a managed database and an OpenIAM deployment on Azure with Terraform — the recommended route for a new Azure environment.

**This is the recommended route when you are creating a new Azure environment.** Terraform provisions the virtual network, the AKS cluster, the managed database and every OpenIAM release in one operation.

If the cluster already exists, use Helm instead — [*Kubernetes overview*](/installation/kubernetes.md).

## What this builds, and how long it takes

|                |                                                                                                               |
| -------------- | ------------------------------------------------------------------------------------------------------------- |
| **You get**    | A virtual network, a private AKS cluster, a managed database and a complete OpenIAM deployment, in one region |
| **Time**       | 2–3 hours, most of it waiting on Azure                                                                        |
| **Hosts**      | A deployment virtual machine inside your Azure network — see below                                            |
| **Downtime**   | None — this creates new infrastructure                                                                        |
| **Reversible** | Mostly. Confirm in the portal that nothing remains after a destroy                                            |

**Scope.** One OpenIAM deployment, with one instance of each component, in a single zone. See [*Scope and availability*](/installation/scope-and-availability.md).

## Before you start

### The deployment machine is a prerequisite, not a convenience

**Terraform creates the AKS cluster as a private cluster**, so its API server is not reachable from the internet. The deployment also runs database client commands against a private endpoint. Both mean the procedure must run from inside your Azure network.

**Create these manually before you begin:**

| Resource                                        | Notes                                                          |
| ----------------------------------------------- | -------------------------------------------------------------- |
| A **resource group**                            | Terraform can create one, but most organisations pre-create it |
| A **deployment virtual network** and **subnet** | Terraform peers the cluster's network to this one              |
| A **deployment virtual machine** in that subnet | Where you run everything below. A small Linux VM is enough     |

**On the deployment machine**

| Tool              | Version                                                                                                                |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Terraform         | **1.13.4 or later.** The modules declare `>= 1.13.4`                                                                   |
| Helm              | 3                                                                                                                      |
| `kubectl`         | Matching the cluster version you deploy                                                                                |
| Azure CLI         | Current, signed in with `az login`                                                                                     |
| A database client | `psql` for PostgreSQL, or `sqlcmd` for SQL Server. **Terraform calls it directly**, so the deployment fails without it |
| `git`, `envsubst` | `envsubst` comes from `gettext`                                                                                        |

**Some Azure material still specifies Terraform 0.12.21. That version cannot parse these modules** — use 1.13.4 or later.

**From OpenIAM**

* The deployment materials repository, and the exact revision issued for this release.
* Registry credentials for `registry.openiam.com`.

## Set these first

{% code overflow="wrap" %}

```bash
export NS=<the namespace you will deploy into>
```

{% endcode %}

***

## Build it

### 1. Get the deployment materials, on the deployment machine

{% code overflow="wrap" %}

```bash
git clone <the repository URL from your account team>
cd kubernetes-docker-configuration
git checkout <the revision issued for this release>
```

{% endcode %}

### 2. Sign in and set your registry credentials

{% code overflow="wrap" %}

```bash
az login
az account list-locations -o table      # to confirm the region name to use

export DOCKER_REGISTRY=registry.openiam.com
export DOCKERHUB_USERNAME=<your registry username>
export DOCKERHUB_PASSWORD=<your registry password>
```

{% endcode %}

### 3. Select Azure in `main.tf`

{% code overflow="wrap" %}

```hcl
module "deployment" {
  source = "./modules/core/azure"
```

{% endcode %}

**Do this before running anything else.**

### 4. Set your values

**`env.sh`** — set `APP_NAME`, `OPENIAM_VERSION_NUMBER`, `BUILD_ENVIRONMENT` and `CLOUD_PROVIDER=azure`.

**`terraform.tfvars`** — `app_name` **must equal** `APP_NAME` in `env.sh`.

| Setting                                                                                      | Set it to                                                                                                                     |
| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `app_name`, `app_version`                                                                    | Your deployment name and the release                                                                                          |
| `region`                                                                                     | Your Azure region, as `az account list-locations` reports it                                                                  |
| `resource_group_name`                                                                        | **Your resource group.** The shipped value is a sample                                                                        |
| `create_resource_group`                                                                      | `false` if it already exists                                                                                                  |
| `replica_count`                                                                              | **The node count, not a pod count.** `2` is a reasonable minimum                                                              |
| `namespace`                                                                                  | A namespace of your own rather than `default`                                                                                 |
| `infra.azure.subscription_id`                                                                | **Your subscription.** A sample GUID ships in this field                                                                      |
| `infra.azure.virtual_network_cidr`, `aks_subnet_cidr`, `db_subnet_cidr`, `redis_subnet_cidr` | Ranges that do not overlap your deployment network                                                                            |
| `infra.azure.deploy_vnet_name`, `deploy_subnet_name`, `deploy_resource_group_name`           | **The network your deployment machine is in.** Terraform peers to it; wrong values mean the cluster is unreachable            |
| `kubernetes.aks.version`, `machine_type`                                                     | The version you intend to run, and a size meeting [*Prepare the cluster*](/installation/kubernetes/prepare-the-cluster.md)    |
| `database.type` and `database.azure.type`                                                    | **Make these agree.** `azure.type` selects which managed service is created; `database.type` drives the platform's own wiring |
| `database.azure.backup_retention_days`                                                       | Your retention requirement                                                                                                    |

**Every credential in `terraform.tfvars` ships as a placeholder and must be changed before you apply** — the database root account and all eight schema accounts, the cache, the search tier, the secret store's key and keystore passwords, the message broker's password and cluster cookie, the graph storage password, and the dashboard password if you enable monitoring.

**`iam.auto.tfvars`** must be present even on Azure — the configuration requires the variable — but its contents are AWS-specific and can stay as shipped.

### 5. Generate the configuration

{% code overflow="wrap" %}

```bash
./setup.sh
```

{% endcode %}

**This step is required and comes before `terraform init`.** Terraform reads the rendered files at plan time.

{% code overflow="wrap" %}

```bash
ls .deploy/ | head
```

{% endcode %}

### 6. Initialise and review the plan

{% code overflow="wrap" %}

```bash
terraform init
terraform plan
```

{% endcode %}

### 7. Apply

{% code overflow="wrap" %}

```bash
terraform apply
```

{% endcode %}

**How long:** 2–3 hours.

### 8. Point `kubectl` at the cluster

{% code overflow="wrap" %}

```bash
kubectl get nodes
```

{% endcode %}

**Expected:** your nodes, `Ready`. If this fails, you are almost certainly running from outside the peered network — the cluster is private by design.

### 9. Expose the deployment and set DNS

{% code overflow="wrap" %}

```bash
kubectl get svc -n "$NS" | grep rproxy
```

{% endcode %}

Expose the reverse proxy's service with a Load Balancer service, or with an Application Gateway through the ingress controller if you want TLS termination or a web application firewall at the edge. Then create a DNS record pointing at it.

| Setting                       | Value                                  |
| ----------------------------- | -------------------------------------- |
| **Health probe path**         | `/idp/login`                           |
| **Backend protocol and port** | HTTP, the reverse proxy service's port |

### 10. Verify

[*Verify a Kubernetes deployment*](/installation/kubernetes/verify.md), then [*First sign-in*](/installation/rpm/rpm-single-node/first-sign-in.md) using the DNS name from step 9.

***

## Verify it

Before moving on, confirm the apply produced what it should:

{% code overflow="wrap" %}

```bash
terraform output
kubectl get nodes
kubectl get pods -n "$NS" --no-headers | awk '{print $3}' | sort | uniq -c
kubectl get pvc -n "$NS"
```

{% endcode %}

**Expected:** outputs present, nodes `Ready`, pods `Running` or `Completed`, every claim `Bound`.

Then work through [*Verify a Kubernetes deployment*](/installation/kubernetes/verify.md) in full — it checks each tier rather than each object.

***

## TLS

**Terminate TLS at an Application Gateway** with your certificate, and forward to the reverse proxy on HTTP. Set `X-Forwarded-Proto` so the platform builds `https://` links.

An Azure Load Balancer is layer 4 and cannot hold a certificate — with that in front, terminate TLS in the cluster by setting `rproxy.https.disabled = "0"` and supplying your certificate and key in `.ssl/` before running `setup.sh`.

**The AWS certificate setting has no effect on Azure.** Ignore `rproxy.aws.certificateManagerARN` here.

## State and secrets

**State is local.** There is no remote backend configured, so `terraform.tfstate` is a file on the deployment machine.

* **The state file contains the credentials from `terraform.tfvars`.** Treat it as a secret. Keep it out of version control and restrict access to the deployment machine accordingly.
* **Losing it means Terraform can no longer manage the deployment.** Back it up together with `terraform.tfvars`, `env.sh`, `main.tf` and `.deploy/` — a later apply or destroy needs all of them.

The deployment machine holding both the state and the only network path to the cluster makes it a component worth protecting and rebuilding deliberately, not a scratch VM.

## Re-running

`terraform apply` is safe to re-run. Run `terraform plan` first and read it — a plan that proposes to replace the database or the cluster is telling you a value was changed that cannot change in place.

## Remove it

{% code overflow="wrap" %}

```bash
terraform destroy
```

{% endcode %}

Confirm in the portal that the resource group, the peering and the private DNS zone links are gone. If you let Terraform create the resource group, destroying removes it and everything in it — check first that nothing else was placed there.

## If it goes wrong

| What you see                                                                    | What it means                                               | What to do                                           |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------- |
| `terraform plan` fails on a missing file under `.deploy/`                       | `setup.sh` has not been run                                 | Set your values, run `./setup.sh`, plan again        |
| `kubectl` times out against the API server                                      | You are outside the peered network                          | Run from the deployment machine                      |
| Terraform fails calling a database client                                       | `psql` or `sqlcmd` is not installed                         | Install the client for the engine you chose          |
| The database is unreachable from pods but reachable from the deployment machine | The private DNS zone is not linked to the cluster's network | Link it and re-test from a pod                       |
| Volume claims stay `Pending`                                                    | Storage class, or the storage account's network rules       | [*Azure AKS*](/installation/kubernetes/azure-aks.md) |
| The search tier restarts repeatedly                                             | `vm.max_map_count` on the node pool                         | [*Azure AKS*](/installation/kubernetes/azure-aks.md) |
| Pods cannot pull images                                                         | Registry credentials were not exported before `setup.sh`    | Export them, re-run `setup.sh`, apply                |

## What this procedure does not cover

Bringing your own AKS cluster — that is the Helm route. Multi-zone placement, scaled replica counts and disaster recovery are out of scope for this release — [*Scope and availability*](/installation/scope-and-availability.md).

## Related

* [*Prepare the cluster*](/installation/kubernetes/prepare-the-cluster.md)
* [*Azure AKS*](/installation/kubernetes/azure-aks.md) — node settings and storage detail
* [*Verify a Kubernetes deployment*](/installation/kubernetes/verify.md) — the next page
* [*Managed databases*](/installation/rpm/cloud/managed-databases.md)

*Applies to OpenIAM 2026.8.1.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/installation/kubernetes/terraform-aks.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
