Manage controllers¶
See also: Juju | Controller
Bootstrap a controller¶
To bootstrap a controller:
Configure the provider with
controller_mode = true. For example:
main.tf¶terraform {
required_providers {
juju = {
source = "juju/juju"
version = "~> 1.0.0"
}
}
}
provider "juju" {
controller_mode = true
}
This enables bootstrapping and restricts resource creation to controllers only.
Gather the necessary cloud credentials for your target cloud (e.g., LXD, AWS, Kubernetes).
juju show-credentials --client localhost localhost --show-secrets
From the output, you will need the values client-cert, client-key, and server-cert. Keep them out of version control (for example, pass them via TF_VAR_... environment variables, a secrets manager, or a .tfvars file you do not commit).
Create a
juju_controllerresource with your controller name, cloud configuration, and credentials. You can also includecontroller_configandcontroller_model_configto configure the controller during bootstrap. For example, for a LXD cloud:
main.tf¶resource "juju_controller" "this" {
name = "test-controller"
# Use /snap/juju/current/bin/juju if Juju is installed as a snap (avoids confinement issues)
juju_binary = "/snap/juju/current/bin/juju"
cloud = {
name = "localhost"
type = "lxd"
auth_types = ["certificate"]
}
cloud_credential = {
name = "localhost"
auth_type = "certificate"
attributes = {
"client-cert" = var.lxd_client_cert
"client-key" = var.lxd_client_key
"server-cert" = var.lxd_server_cert
}
}
# Bootstrap is a good time to set configurations, constraints, etc.
# Settings here map to flags/config used by `juju controller-config`.
controller_config = {
"audit-log-max-backups" = "10"
"query-tracing-enabled" = "true"
}
# Settings here map to flags/config used by `juju model-config`.
controller_model_config = {
"juju-http-proxy" = "http://my-proxy.internal"
}
}
After terraform apply, the resource will expose useful read-only attributes such as the controller api_addresses, ca_cert, username, and password.
See more:
juju_controller(resource)
Configure a controller¶
A Juju controller can be configured with various settings that control its behavior. There are two types of configuration:
Controller configuration (
controller_config): Settings specific to the controller itself.Controller model configuration (
controller_model_config): Settings for the controller model.
You can configure these settings either during bootstrap or after the controller is created. However, keep in mind that some settings cannot be changed after bootstrap.
During bootstrap¶
To configure a controller during bootstrap, in your juju_controller resource specify the controller_config and/or controller_model_config attributes. For example:
main.tf¶resource "juju_controller" "this" {
name = "configured-controller"
juju_binary = "/snap/juju/current/bin/juju"
cloud = {
# cloud configuration...
}
cloud_credential = {
# credential configuration...
}
# Controller-specific configuration
controller_config = {
"audit-log-max-backups" = "10"
"query-tracing-enabled" = "true"
}
# Controller model configuration
controller_model_config = {
"juju-http-proxy" = "http://my-proxy.internal"
"update-status-hook-interval" = "1m"
}
}
Post-bootstrap¶
To configure a controller post-bootstrap, modify the controller_config or controller_model_config attributes in your Terraform configuration and run terraform apply:
main.tf¶resource "juju_controller" "this" {
# ... existing configuration ...
controller_config = {
"audit-log-max-backups" = "15" # Updated from 10
"query-tracing-enabled" = "true"
"audit-log-capture-args" = "true" # Newly added
}
}
Important
Configuration update behaviors:
Removing a key from
controller_configdoes not unset it on the controller – it remains at its previous valueSome settings cannot be changed after bootstrap. Attempting to change them will result in an error, requiring you to destroy and recreate the controller
Boolean values must be specified as strings:
"true"or"false", not bare boolean values
To restore a setting to its default value, you must explicitly set it to the default value rather than removing it from the configuration.
To discover valid configuration keys and values, use juju bootstrap --help or consult the Juju documentation. Many juju_controller resource attributes correspond directly to the flags and config options used by the juju bootstrap command.
Enable controller high availability¶
Note
Enabling HA relies on Terraform actions, which require Terraform 1.14 or later. For more information, see Terraform actions .
High availability (HA) for a Juju controller ensures that multiple controller units are running so the controller remains available if individual units fail. You can enable HA either during bootstrap or post-bootstrap, and in the latter case you can scale out as well as in.
During bootstrap¶
To enable controller high availability during bootstrap, in your juju_controller resource, in the lifecycle block, define the action_trigger field. For example:
main.tf¶resource "juju_controller" "this" {
name = "my-controller"
juju_binary = "/snap/juju/current/bin/juju"
cloud = {
name = "localhost"
type = "lxd"
auth_types = ["certificate"]
}
cloud_credential = {
name = "localhost"
auth_type = "certificate"
attributes = {
"client-cert" = var.lxd_client_cert
"client-key" = var.lxd_client_key
"server-cert" = var.lxd_server_cert
}
}
lifecycle {
ignore_changes = [
cloud_credential.attributes["client-cert"],
cloud_credential.attributes["client-key"],
]
action_trigger {
events = [after_create]
actions = [action.juju_enable_ha.this]
}
}
}
action "juju_enable_ha" "this" {
config {
api_addresses = juju_controller.this.api_addresses
ca_cert = juju_controller.this.ca_cert
username = juju_controller.this.username
password = juju_controller.this.password
units = 3
}
}
Post-bootstrap¶
To enable controller high availability post-bootstrap, define a Terraform juju_enable_ha action block:
main.tf¶action "juju_enable_ha" "this" {
config {
api_addresses = juju_controller.this.api_addresses
ca_cert = juju_controller.this.ca_cert
username = juju_controller.this.username
password = juju_controller.this.password
units = 5
}
}
Then run:
terraform apply -invoke=action.juju_enable_ha.this
Terraform will execute the juju_enable_ha action and ensure the controller has the requested number of units.
To scale out the number of units after HA is enabled, update the units value in your juju_enable_ha action and run the terraform apply -invoke=action.juju_enable_ha.this command again. The number of units must always be an odd number.
Note
As with the juju CLI, constraints set while scaling post-bootstrap always apply only to the new units being created.
To scale in, remove backing machines manually via the juju CLI juju remove-machine .
Note
While it is possible to control the number of units or remove machines directly through Terraform, that is currently supported only for regular applications.
Import an existing controller¶
Note
This operation imports the controller as a resource that Terraform will manage. Controllers cannot be referenced as data sources (read-only). Once imported, Terraform will track the controller’s state and can make changes to its configuration.
To import an existing controller:
Gather the controller’s connection details. You can obtain these by running:
juju show-controller --show-password
From the output, you will need:
Controller name
API addresses
CA certificate
Admin username (typically admin)
Admin password
Controller UUID
Credential name
Create an
importblock with the identity schema containing the controller’s connection information. For example:
main.tf¶import {
to = juju_controller.imported
identity = {
name = "my-existing-controller"
api_addresses = ["<ip>:17070"]
username = "admin"
password = "<password>"
ca_cert = <<-EOT
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
EOT
controller_uuid = "<controller-uuid>"
credential_name = "<credential-name>"
}
}
Define the corresponding
juju_controllerresource with the cloud and credential information. Then runterraform plan. Terraform will detect the import block and import the controller during the nextterraform apply. For example:
main.tf¶resource "juju_controller" "imported" {
name = "my-existing-controller"
cloud = {
...
}
cloud_credential = {
...
}
}
Example: LXD controller resource definition for import
main.tf¶provider "juju" {
controller_mode = true
}
resource "juju_controller" "imported" {
name = "my-lxd-controller"
juju_binary = "/snap/juju/current/bin/juju"
cloud = {
name = "localhost"
type = "lxd"
auth_types = ["certificate"]
}
cloud_credential = {
name = "localhost"
auth_type = "certificate"
attributes = {
<attrs>
}
}
lifecycle {
ignore_changes = [
cloud.endpoint,
cloud.region,
cloud_credential.attributes["client-cert"],
cloud_credential.attributes["client-key"],
]
}
}
import {
to = juju_controller.imported
identity = {
name = "my-lxd-controller"
api_addresses = ["<ip>:17070"]
username = "admin"
password = "<password>"
ca_cert = <<-EOT
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
EOT
controller_uuid = "<controller-uuid>"
credential_name = "<credential-name>"
}
}
Note
The cloud_credential.attributes["client-cert"] and cloud_credential.attributes["client-key"] are not required to bootstrap an LXD controller, but they are populated in the state during import because they are fetched from the controller. The same applies to cloud.endpoint and cloud.region, which may be set by Juju during bootstrap even if not explicitly specified.
Example: MicroK8s controller resource definition for import
main.tf¶provider "juju" {
controller_mode = true
}
resource "juju_controller" "imported" {
name = "my-k8s-controller"
juju_binary = "/snap/juju/current/bin/juju"
cloud = {
name = "test-k8s"
type = "kubernetes"
auth_types = ["clientcertificate"]
endpoint = var.k8s_endpoint
ca_certificates = [var.k8s_ca_cert]
host_cloud_region = "localhost"
}
cloud_credential = {
name = "test-credential"
auth_type = "clientcertificate"
attributes = {
"ClientCertificateData" = var.k8s_client_cert
"ClientKeyData" = var.k8s_client_key
}
}
lifecycle {
ignore_changes = [
cloud.region,
cloud.host_cloud_region
]
}
}
import {
to = juju_controller.imported
identity = {
name = "my-k8s-controller"
api_addresses = ["<ip>:17070"]
username = "admin"
password = "<password>"
ca_cert = <<-EOT
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
EOT
controller_uuid = "<controller-uuid>"
credential_name = "<credential-name>"
}
}
Note
The cloud.region is not required during bootstrap but may be set by Juju and needs to be ignored. The cloud.host_cloud_region cannot be fetched from the controller after bootstrap, so it must be ignored to prevent Terraform from attempting to replace the controller.
See more:
juju_controller(resource)
Verify the import:
Run
terraform planto see which attributes Terraform cannot determine or that differ from your configuration. These differences are expected after an import.Add any fields showing unexpected changes to the
lifecycle.ignore_changesblock. Common fields to ignore include:Credential attributes that may differ between your plan and the ones fetched from the controller
Cloud region and endpoint fields, which may be set by Juju during bootstrap even if not explicitly specified
Bootstrap-time configuration that cannot be changed and can’t be fetched from the controller
For example:
main.tf¶resource "juju_controller" "imported" { # ... existing configuration ... lifecycle { ignore_changes = [ cloud.endpoint, cloud.region, cloud_credential.attributes["client-cert"], cloud_credential.attributes["client-key"], ] } }
Run
terraform planagain. You should see either no changes or only expected configuration updates.
Tip
If you see controller_config or controller_model_config showing changes to set default values, you can either apply them (they will update the controller configuration which is idempotent) or add these blocks to ignore_changes to prevent the updates.
Add a cloud to a controller¶
See more: Add a machine cloud, Add a Kubernetes cloud
Add a credential to a controller¶
By virtue of being bootstrapped into a cloud, your controller already has a credential for that cloud. However, if you want to use a different credential, or if you’re adding a further cloud to the controller and would like to also add a credential for that cloud, you will need to add those credentials to the controller too. You can do that in the usual way by creating a resource of the juju_credential type.
See more: Add a credential
Manage access to a controller¶
Note
At present the Terraform Provider for Juju supports controller access management only for Juju controllers added to JAAS.
When using Juju with JAAS, to grant access to a Juju controller added to JAAS, in your Terraform plan add a resource type juju_jaas_access_controller. Access can be granted to one or more users, service accounts, roles, and/or groups. You must specify the model UUID, the JAAS controller access level, and the desired list of users, service accounts, roles, and/or groups. For example:
main.tf¶resource "juju_jaas_access_controller" "development" {
access = "administrator"
users = ["foo@domain.com"]
service_accounts = ["Client-ID-1", "Client-ID-2"]
roles = [juju_jaas_role.development.uuid]
groups = [juju_jaas_group.development.uuid]
}
Remove a controller¶
See also: Juju | Removing things
To remove a controller, remove its resource definition from your Terraform plan.
See more:
juju_controller(resource)