Get started with the Terraform Provider for Juju¶
The Terraform Provider for Juju brings infrastructure-as-code capabilities to Juju .
In this tutorial you will define and deploy a chat service (Mattermost backed by PostgreSQL) using declarative configuration files managed with Terraform.
What you’ll need:
A workstation, e.g., a laptop, that has sufficient resources to launch a virtual machine with 4 CPUs, 8 GB RAM, and 50 GB disk space.
Familiarity with a terminal.
Basic familiarity with Juju concepts (controllers, models, charms, applications).
Basic familiarity with Terraform (providers, resources, state).
What you’ll do:
Set things up: launch a Juju-ready VM using Multipass, install Terraform, and bootstrap a Juju controller.
See how to manage users and access control as code.
Deploy infrastructure and applications with Terraform configuration files.
Clean up resources.
Set things up¶
To work with the Terraform Provider for Juju, you’ll need:
A cloud (MicroK8s for this tutorial)
The
jujuCLI (to extract cloud credentials and to bootstrap controllers – the Terraform provider callsjujucommands in the background)The
terraformCLI (to run your infrastructure-as-code definitions)A Juju controller (which you’ll bootstrap with Terraform)
You’ll get most of these automatically by launching a Juju-ready Ubuntu VM with Multipass using the charm-dev cloud-init configuration, then install Terraform manually.
First, install Multipass . For example, on Linux with snapd:
~$ Important
If on Windows: Note that Multipass can only be installed on Windows 10 Pro or Enterprise. If you are using a different version, you’ll need to manually set up MicroK8s and the juju CLI outside of a Multipass VM.
Now, use Multipass to launch a Juju-ready VM using the charm-dev cloud-init configuration:
Note
This step may take a few minutes to complete (e.g., 10 mins).
This is because the command downloads, installs, (updates,) and configures a number of packages (including MicroK8s, the juju CLI, Terraform, and development tools), and the speed will be affected by network bandwidth.
You’ll have everything you need in an isolated environment.
~$ Tips for troubleshooting
If the VM launch fails, run multipass delete --purge my-juju-vm to clean up, then try the launch command again.
Open a shell into the VM:
~$ Anything you type after the VM shell prompt (ubuntu@my-juju-vm:~$) will run on the VM.
Tips for usage
At any point:
To exit the shell, press Ctrl + D or type
exit.To stop the VM after exiting the VM shell, run
multipass stop my-juju-vm.To restart the VM and re-open a shell into it, type
multipass shell my-juju-vm.
Congratulations! Your cloud is ready, and thanks to the charm-dev cloud-init, you already have:
MicroK8s configured and running
The
jujuCLI installedA MicroK8s cloud registered with Juju
Verify this in your VM:
ubuntu@my-juju-vm:~$ ubuntu@my-juju-vm:~$ ubuntu@my-juju-vm:~$ Now install Terraform in your VM:
ubuntu@my-juju-vm:~$ Verify the installation:
ubuntu@my-juju-vm:~$ The Terraform Provider for Juju works by calling the juju CLI in the background. When you run terraform apply, Terraform will call juju bootstrap, and Juju needs MicroK8s credentials to connect to your cluster. Copy the credentials to where Juju expects to find them when called by Terraform:
ubuntu@my-juju-vm:~$ ubuntu@my-juju-vm:~$ ubuntu@my-juju-vm:~$ Why is this necessary?
When the Terraform Provider bootstraps the controller, it needs access to the MicroK8s credentials. This command copies the credentials to a location where they can be found during the bootstrap process.
Now, on your local workstation (not the VM), create a directory for your Terraform configuration:
~$ ~$ Mount it to your VM:
~$ This lets you create files locally and run Terraform on them inside the VM, while using your IDE to view and edit the files.
Tip
Recommended workflow setup:
You’ll be working across two contexts: your local workstation and the VM. To work efficiently:
Two terminal windows (or one split terminal):
One terminal for your local workstation (where you’ll create files, run
multipasscommands, and rungitcommands)One terminal for the VM shell (where you’ll run
terraformandjujucommands)
Your favorite text editor on your local workstation to view and edit
.tffiles. Changes you make locally will be automatically visible in the VM via the mounted directory.
Initialize version control. On your local workstation, in your terraform-juju directory:
~$ Create two subdirectories to organize your infrastructure:
~$ Note
Why two directories?
The Terraform Juju provider has a controller_mode setting that determines which resources you can manage:
When
controller_mode = true: You can ONLY managejuju_controllerresources (to bootstrap controllers)When
controller_mode = false(or omitted): You can manage everything EXCEPTjuju_controllerresources (models, applications, integrations, etc.)
This design requires separating controller bootstrap from application deployment into two distinct Terraform configurations. This tutorial uses 1-bootstrap/ for the controller and 2-deploy/ for your applications.
Now bootstrap a Juju controller. A Juju controller is your Juju control plane – the entity that holds the Juju API server and Juju’s database. With the Terraform Provider, you can bootstrap a controller by defining it in your Terraform configuration.
On your VM, view the MicroK8s credentials that you’ll need for your Terraform configuration:
ubuntu@my-juju-vm:~$ microk8s:
auth-type: oauth2
Token: eyJhbGciOiJSUzI1NiIsImtpZCI6IldBbERh...
From the output, copy the full Token value (it will be much longer than shown here). You’ll also need the MicroK8s endpoint and CA certificate, which you can get from the kubeconfig:
ubuntu@my-juju-vm:~$ apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTi...
server: https://10.x.x.x:16443
name: microk8s-cluster
...
From the output, copy the full certificate-authority-data value and the server (endpoint) URL.
On your local workstation, in your terraform-juju directory, create your controller bootstrap configuration.
First, create 1-bootstrap/terraform.tf to configure Terraform to use the Juju provider in controller mode:
~/terraform-juju$ 1-bootstrap/terraform.tf¶terraform {
required_providers {
juju = {
version = "~> 1.4"
source = "juju/juju"
}
local = {
source = "hashicorp/local"
}
}
}
provider "juju" {
controller_mode = true
}
Important
Notice controller_mode = true. This setting restricts the provider to only managing juju_controller resources. You cannot define models, applications, or integrations in this configuration.
Next, create 1-bootstrap/variables.tf to define variables for your sensitive credentials:
~/terraform-juju$ 1-bootstrap/variables.tf¶variable "k8s_endpoint" {
description = "MicroK8s API endpoint"
type = string
}
variable "k8s_ca_cert" {
description = "MicroK8s CA certificate"
type = string
sensitive = true
}
variable "k8s_token" {
description = "MicroK8s authentication token"
type = string
sensitive = true
}
Create 1-bootstrap/terraform.tfvars with your actual credential values (from the commands above):
~/terraform-juju$ 1-bootstrap/terraform.tfvars¶k8s_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IldBbERh..."
k8s_ca_cert = "LS0tLS1CRUdJTi..."
k8s_endpoint = "https://10.x.x.x:16443"
Note
The values shown above are examples only. Use your actual values from the previous commands – the token and certificate will be much longer than shown here.
Before continuing, keep credentials, connection info, and Terraform state safe and out of version control.
Create 1-bootstrap/.gitignore:
~/terraform-juju$ 1-bootstrap/.gitignore¶terraform.tfvars
conn_info.json
.terraform*
terraform.tfstate*
Now create 1-bootstrap/main.tf to define your controller:
~/terraform-juju$ 1-bootstrap/main.tf¶resource "juju_controller" "microk8s" {
name = "my-chat-controller"
juju_binary = "/snap/juju/current/bin/juju"
cloud = {
name = "microk8s"
type = "kubernetes"
auth_types = ["oauth2"]
endpoint = var.k8s_endpoint
ca_certificates = [var.k8s_ca_cert]
host_cloud_region = "localhost"
}
cloud_credential = {
name = "microk8s-cred"
auth_type = "oauth2"
attributes = {
"Token" = var.k8s_token
}
}
lifecycle {
ignore_changes = [
cloud.region,
cloud.host_cloud_region
]
}
}
Notice how this declarative definition makes your infrastructure intentions clear: you want a Juju controller named my-chat-controller on MicroK8s with specific credentials.
To allow the deployment configuration to connect to this controller, add a resource that writes the connection details to a JSON file. Add this to 1-bootstrap/main.tf:
1-bootstrap/main.tf (add after juju_controller resource)¶resource "local_file" "conn_info" {
filename = "${path.module}/conn_info.json"
content = jsonencode({
addresses = juju_controller.microk8s.api_addresses
username = juju_controller.microk8s.username
password = juju_controller.microk8s.password
ca_cert = juju_controller.microk8s.ca_cert
})
}
On your local workstation, in your terraform-juju directory, commit your controller infrastructure definition (excluding sensitive files):
~/terraform-juju$ Now, in your VM, initialize Terraform in the bootstrap directory. If you exited the VM shell, reopen it:
~$ Initialize the bootstrap directory:
ubuntu@my-juju-vm:~$ This downloads the Juju provider plugin and prepares your workspace.
Preview what Terraform will create:
ubuntu@my-juju-vm:~$ This shows what Terraform will create without actually creating anything. Review the output carefully – you’ll see the controller resource that will be created.
Tip
Infrastructure-as-code benefit: The plan step lets you (and your team) review changes before applying them. In a team setting, you’d commit your .tf changes, open a pull request, and have teammates review the plan output before merging and applying.
Apply your infrastructure definition:
ubuntu@my-juju-vm:~$ Terraform will show you the plan again and ask for confirmation. Type yes to proceed.
Note
The bootstrap process typically takes 1-2 minutes, but may vary depending on your system and network speed. Terraform will show progress as it creates the controller.
Once complete, your Juju controller is running on MicroK8s, and Terraform has recorded its state. Terraform has also created 1-bootstrap/conn_info.json with the controller’s connection details (minified JSON on one line; to view formatted, run cat terraform-juju/1-bootstrap/conn_info.json | jq .).
Deploy infrastructure and applications¶
With your controller bootstrapped, you’ll now define and deploy the applications that make up your chat service. You’ll deploy Mattermost for the chat service, PostgreSQL for its backing database, and self-signed certificates to TLS-encrypt traffic from PostgreSQL.
To connect to your bootstrapped controller, you’ll read its connection details from the JSON file created by the bootstrap plan. This file contains the controller’s API addresses, credentials, and CA certificate.
On your local workstation, in your terraform-juju directory, create your application deployment configuration.
First, create 2-deploy/terraform.tf to configure the provider to connect to your controller:
~/terraform-juju$ 2-deploy/terraform.tf¶terraform {
required_providers {
juju = {
version = "~> 1.4"
source = "juju/juju"
}
}
}
# Read connection details from the bootstrap JSON file
locals {
conn_info = jsondecode(file("${path.module}/../1-bootstrap/conn_info.json"))
}
provider "juju" {
controller_addresses = join(",", local.conn_info.addresses)
username = local.conn_info.username
password = local.conn_info.password
ca_certificate = local.conn_info.ca_cert
}
Important
Notice there’s no controller_mode setting (it defaults to false). This configuration can manage models, applications, and integrations, but cannot manage juju_controller resources.
The provider connects to the bootstrapped controller by reading connection details from the JSON file created by the bootstrap plan. This separation is necessary because Terraform cannot configure a provider from resource outputs in the same plan.
Create 2-deploy/.gitignore to keep Terraform state out of version control:
~/terraform-juju$ 2-deploy/.gitignore¶.terraform*
terraform.tfstate*
Now create 2-deploy/main.tf to define your application resources:
~/terraform-juju$ 2-deploy/main.tf¶# Define the workspace for your applications
resource "juju_model" "chat" {
name = "chat"
}
# Define the chat application
resource "juju_application" "mattermost-k8s" {
model_uuid = juju_model.chat.uuid
charm {
name = "mattermost-k8s"
}
}
# Define the database
resource "juju_application" "postgresql-k8s" {
model_uuid = juju_model.chat.uuid
charm {
name = "postgresql-k8s"
channel = "14/stable"
}
trust = true
units = 1
config = {
profile = "testing"
}
}
# Define the TLS certificate provider
resource "juju_application" "self-signed-certificates" {
model_uuid = juju_model.chat.uuid
charm {
name = "self-signed-certificates"
}
}
# Integrate PostgreSQL with Mattermost
resource "juju_integration" "postgresql-mattermost" {
model_uuid = juju_model.chat.uuid
application {
name = juju_application.postgresql-k8s.name
endpoint = "db"
}
application {
name = juju_application.mattermost-k8s.name
}
lifecycle {
replace_triggered_by = [
juju_application.postgresql-k8s.name,
juju_application.postgresql-k8s.model_uuid,
juju_application.postgresql-k8s.constraints,
juju_application.postgresql-k8s.placement,
juju_application.postgresql-k8s.charm.name,
juju_application.mattermost-k8s.name,
juju_application.mattermost-k8s.model_uuid,
juju_application.mattermost-k8s.constraints,
juju_application.mattermost-k8s.placement,
juju_application.mattermost-k8s.charm.name,
]
}
}
# Integrate PostgreSQL with TLS certificates
resource "juju_integration" "postgresql-tls" {
model_uuid = juju_model.chat.uuid
application {
name = juju_application.postgresql-k8s.name
endpoint = "certificates"
}
application {
name = juju_application.self-signed-certificates.name
endpoint = "certificates"
}
lifecycle {
replace_triggered_by = [
juju_application.postgresql-k8s.name,
juju_application.postgresql-k8s.model_uuid,
juju_application.postgresql-k8s.constraints,
juju_application.postgresql-k8s.placement,
juju_application.postgresql-k8s.charm.name,
juju_application.self-signed-certificates.name,
juju_application.self-signed-certificates.model_uuid,
juju_application.self-signed-certificates.constraints,
juju_application.self-signed-certificates.placement,
juju_application.self-signed-certificates.charm.name,
]
}
}
Notice how this declarative definition makes your infrastructure intentions clear: you want a chat model with Mattermost, PostgreSQL, TLS certificates, and specific integrations between them.
Note
Notice the explicit endpoint values in the TLS integration – this ensures the correct relation endpoints are used.
On your local workstation, in your terraform-juju directory, commit your application infrastructure definition:
~/terraform-juju$ Now deploy your infrastructure. In your VM, initialize and preview the deployment:
ubuntu@my-juju-vm:~$ ubuntu@my-juju-vm:~$ This shows what Terraform will create without actually creating anything. Review the output carefully – you’ll see the model, applications, and integrations that will be created.
Apply your infrastructure definition:
ubuntu@my-juju-vm:~$ Terraform will show you the plan again and ask for confirmation. Type yes to proceed.
The deployment will take a few minutes. Terraform will show you the progress as it creates each resource.
Once complete, verify your applications are running. Wait for all pods to be ready:
ubuntu@my-juju-vm:~$ The Mattermost charm needs time to complete database setup before creating its workload pod. Monitor the pods until the mattermost-k8s-0 pod appears and shows Running status:
ubuntu@my-juju-vm:~$ Press Ctrl + C to exit once all pods show Running. This may take several minutes.
Once all pods are running, get the service address:
ubuntu@my-juju-vm:~$ Test your chat service (replace <address> with the output from above):
ubuntu@my-juju-vm:~$ Sample output:
{"ActiveSearchBackend":"database","AndroidLatestVersion":"","AndroidMinVersion":"","IosLatestVersion":"","IosMinVersion":"","status":"OK"}
Congratulations! Your chat service is up and running, and your entire infrastructure is defined as code.
Tip
Infrastructure-as-code benefit: Terraform’s state tracking means you can’t accidentally create duplicate resources. It knows what exists and only makes necessary changes.
To build on what you’ve learned: configure controller and model settings during bootstrap (Configure a controller), manage controllers post-bootstrap (Manage controllers), use Terraform workspaces for multiple environments (Manage models), integrate with other cloud providers, set up remote state storage for team collaboration, and explore all provider features (juju provider reference).
Tear things down¶
With Terraform, tearing down your infrastructure is as simple as deploying it.
In your VM, destroy the application infrastructure first, then the controller:
ubuntu@my-juju-vm:~$ Terraform will show you everything it will remove and ask for confirmation. Type yes to proceed.
This removes the applications, integrations, and model. Now destroy the controller:
ubuntu@my-juju-vm:~$ Type yes to confirm. This removes the Juju controller.
Notice how Terraform maintains consistency – it knows exactly what it created from the state in each directory, so it can cleanly remove everything.
Exit the VM:
ubuntu@my-juju-vm:~$ From your local workstation, delete the VM:
~$ Finally, uninstall Multipass if you no longer need it.
Your local terraform-juju directory contains your infrastructure definitions – keep this git repository to preserve your infrastructure history, or delete it if you’re done experimenting.