Terraform workspaces are a great way to manage different environments for the same application. For testing changes we will be using this with Bookstack and its associated database in docker. All of the terraform in this tutorial can be found here.

Setup

Workspaces can be created using the command terraform workspace new name_of_new_env. We will be using the workspace names dev, and prod.

Create Development Workspace

Create a new directory for this test and run the following.

mkdir workspace_test && cd workspace_test
terraform workspace new dev

This will put you in the new environment.

We also want to setup a separate directory for our docker volumes. This is because we don’t want them destroyed when we stop our environment.

mkdir volumes && cd volumes
terraform workspace new dev

Docker Volume

Now the docker_volume needs to be setup.

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "2.15.0"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

resource "docker_volume" "bookstack_data" {
  name = "bookstack_data_${terraform.workspace}"
}

Notice that the name of the volume contains the workspace. This is so the data between workspaces is completely separate. Now run that terraform.

terraform init
terraform apply

Now leave this directory and head back to the main folder.

Basic Terraform

Next we will create the backbone of our terraform.

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "2.15.0"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

resource "docker_network" "bookstack_network" {
    name = "bookstack_network_${terraform.workspace}"
}

resource "docker_image" "bookstack" {
    name = "linuxserver/bookstack:21.11.3"
    keep_locally = true
}

resource "docker_image" "mariadb" {
    name = "linuxserver/mariadb:10.5.13"
    keep_locally = true
}
  • Lines 1-8 set the required provider for this terraform.
  • Lines 10-12 instantiates the provider.
  • Lines 14-16 creates the docker network. Notice that we use ${terraform.workspace} to specify the workspace. This means that we are making a different network for each workspace so that the environments are separated.
  • Lines 18-26 create the images we need for our containers.

Database Terraform

The next step will be to create the database container. We will use ${terraform.workspace} to ensure that this is separated from other workspaces.

resource "docker_container" "bookstack_mariadb" {
  image = docker_image.mariadb.latest
  name  = "bookstack_mariadb_${terraform.workspace}"
  networks_advanced {
    name = "bookstack_network_${terraform.workspace}"
  }
  volumes {
    volume_name = "bookstack_data_${terraform.workspace}"
    container_path = "/config"
  }
  env = [
      "PUID=1000",
      "PGID=1000",
      "MYSQL_ROOT_PASSWORD=myrootpassword",
      "TZ=AMERICA/NEW_YORK",
      "MYSQL_DATABASE=bookstackapp",
      "MYSQL_USER=bookstack",
      "MYSQL_PASSWORD=bookstackpass"
  ]
    depends_on = [
      docker_image.mariadb,
      docker_network.bookstack_network,
  ]
}

resource "docker_container" "bookstack" {
  image = docker_image.bookstack.latest
  name  = "bookstack_${terraform.workspace}"
  networks_advanced {
    name = "bookstack_network_${terraform.workspace}"
  }
  ports {
    internal = 80
    external = 8080
  }
  volumes {
    volume_name = "bookstack_data_${terraform.workspace}"
    container_path = "/config"
  }
  env = [
      "PUID=1000",
      "PGID=1000",
      "APP_URL=http://localhost:8080",
      "DB_HOST=bookstack_mariadb_${terraform.workspace}",
      "DB_USER=bookstack",
      "DB_DATABASE=bookstackapp",
      "DB_PASSWORD=bookstackpass"
  ]
  depends_on = [
      docker_container.bookstack_mariadb,
      docker_image.bookstack,
      docker_network.bookstack_network,
  ]
}

Below are the most important parts to note for this tutorial.

  • Line 3 sets the container name to a workspace specific name.
  • Lines 4-6 attach to our workspace specific network.
  • Lines 11-19 is required for the container to start properly. Please use your own passwords there.

Bookstack Terraform

The last step is to setup the main container.

resource "docker_container" "bookstack" {
  image = docker_image.bookstack.latest
  name  = "bookstack_${terraform.workspace}"
  networks_advanced {
    name = "bookstack_network_${terraform.workspace}"
  }
  ports {
    internal = 80
    external = (terraform.workspace == "dev") ? 8081 : 8080
  }
  volumes {
    volume_name = "bookstack_data_${terraform.workspace}"
    container_path = "/config"
  }
  env = [
      "PUID=1000",
      "PGID=1000",
      (terraform.workspace == "dev") ? "APP_URL=http://localhost:8081" : "APP_URL=http://localhost:8080",
      "DB_HOST=bookstack_mariadb_${terraform.workspace}",
      "DB_USER=bookstack",
      "DB_DATABASE=bookstackapp",
      "DB_PASSWORD=bookstackpass"
  ]
  depends_on = [
      docker_container.bookstack_mariadb,
      docker_image.bookstack,
      docker_network.bookstack_network,
  ]
}

Here notice that the workspace is used for the volume_name, network name, and container name. This allows full separation between Bookstack instances. The port number is changed for the dev workspace as well so we can run both instances at the same time on different ports. In a bigger example it may be more difficult to contain multiple configurations in terraform using workspaces.

Now run an apply and the dev environment should be brought online.

terraform apply

You should now see both containers running.

$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                       NAMES
4f7c4650b62a   5493ec0d4a06   "/init"                  4 seconds ago   Up 4 seconds   443/tcp, 0.0.0.0:8081->80/tcp               bookstack_dev
337c8693ce32   0d93b86d22bd   "/init"                  5 seconds ago   Up 4 seconds   3306/tcp                                    bookstack_mariadb_dev

Development Bookstack

Now we need to make a change that will show our environments are completely separate. Login to this url with username: [email protected], and password: password.

In the top right click on Admin > Edit Profile. Now under “User Password” set a new password for the admin user.

Logout and login again using the new password to test.

Production Bookstack

The last step is to bring the production workspace online. The following script will get the volume created.

cd volumes
terraform workspace new prod
terraform init
terraform apply
cd ..

Now the same with the main folder.

terraform workspace new prod
terraform init
terraform apply

Both environments are now up and running.

$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                       NAMES
e6e648c51ced   5493ec0d4a06   "/init"                  9 seconds ago    Up 8 seconds    443/tcp, 0.0.0.0:8080->80/tcp               bookstack_prod
0a9e0ed3b610   0d93b86d22bd   "/init"                  10 seconds ago   Up 8 seconds    3306/tcp                                    bookstack_mariadb_prod
1fa6e5b49a48   5493ec0d4a06   "/init"                  17 minutes ago   Up 17 minutes   443/tcp, 0.0.0.0:8081->80/tcp               bookstack_dev
337c8693ce32   0d93b86d22bd   "/init"                  23 minutes ago   Up 23 minutes   3306/tcp                                    bookstack_mariadb_dev

If you try to login here you should not be able to with the new password you set. That is because the production admin user password was never updated, while the development Bookstack has.

Conclusion

Terraform workspaces are a great way to share code and have a QA test environment. Here is an amazing site to learn more: https://www.terraform.io/language/state/workspaces