Manage Rancher Projects in Terraform

Posted on November 2, 2022 by Adrian Wyssmann ‐ 6 min read

While working on managing our Rancher clusters is the management of Rancher projects. I want to talk about the approach I have taken, which may be useful to you as well.

What is a project?

Rancher is using projects as an additional layer:

A project is a group of namespaces, and it is a concept introduced by Rancher. Projects allow you to manage multiple namespaces as a group and perform Kubernetes operations in them. You can use projects to support multi-tenancy, so that a team can access a project within a cluster without having access to other projects in the same cluster.

In terms of hierarchy:

  • Clusters contain projects
  • Projects contain namespaces

There are several things you can do on projects

  • Assign users to a group of namespaces (i.e., project membership).
  • Assign users specific roles in a project. A role can be owner, member, read-only, or custom.
  • Assign resources to the project.
  • Assign Pod Security Policies.

Each cluster already come with 2 pre-defined projects:

How to manage projects with Terraform?

Generic approach

The obvious resource to do that is rancher2_project, which allows you to manage a project and it’s resources. However, if you also want to manage the permissions, you also need

  • rancher2_role_template - Provides a Rancher v2 Role Template resource. This can be used to create Role Template for Rancher v2 and retrieve their information
  • rancher2_project_role_template_binding - Provides a Rancher v2 Project Role Template Binding resource. This can be used to create Project Role Template Bindings for Rancher v2 environments and retrieve their information

I consider projects, relatively static, so we can easily have multiple blocks like this

resource "rancher2_project" "default" {
  name                            ="Default"
  description                     = "Default project created for the cluster",
  cluster_id                      = rancher2_cluster.cluster.id
  container_resource_limit {
      limits_cpu      = 10m
      limits_memory   = 10Mi
      requests_cpu    = 2
      requests_memory = 2Gi
  }
  resource_quota {
    project_limit {
      requests_storage = 5000Mi
    }
    namespace_default_limit {
      requests_storage = 500Mi
    }
  }
}

Then we also need the rancher2_role_template and rancher2_project_role_template_binding, which will lead to a lot of resources and relatively tedious if you have a lot of projects and role-bindings. So my aim is to manage projects and the related role bindings together, also so that developers which are eventually not so deep into terraform can easily adjust limits and so.

How I manage projects

So I decided to have a local map, which contains all projects and it’s configuration in a file called projects.tf

locals {
  projects = {
    "default" = {
      name                       = "Default",
      description                = "Default project created for the cluster",
      limits_cpu                 = "2000m",
      limits_memory              = "1000Mi",
      requests_cpu               = "10m",
      requests_memory            = "10Mi",
      requests_storage_project   = "5000Mi",
      requests_storage_namespace = "500Mi"
    },
    "system" = {
      name                       = "System",
      description                = "System project created for the cluster",
      limits_cpu                 = "2000m",
      limits_memory              = "1000Mi",
      requests_cpu               = "10m",
      requests_memory            = "10Mi",
      requests_storage_project   = "3000Gi",
      requests_storage_namespace = "80Gi"
    },
  }
}

Projects can be dynamically created using a fore_each

resource "rancher2_project" "pr" {
  for_each = local.projects
  name                            = each.value.name
  description                     = each.value.description
  cluster_id                      = rancher2_cluster.cluster.id
  container_resource_limit {
      limits_cpu      = each.value.limits_cpu
      limits_memory   = each.value.limits_memory
      requests_cpu    = each.value.requests_cpu
      requests_memory = each.value.requests_memory
  }
  resource_quota {
    project_limit {
      requests_storage = each.value.requests_storage_project
    }
    namespace_default_limit {
      requests_storage = each.value.requests_storage_namespace
    }
  }
}

How I manage roles

First we have to understand that roles are defined in the local cluster, and in order to create the bindings, we need the role_template_id of these roles. As each cluster has it’s own state file, we need first make the current cluster aware of the remote state of the local cluster, in file data.tf. Assuming we use azurerm as backend it would look like this:

data "terraform_remote_state" "local-state" {
  backend = "azurerm"

  config = {
    use_azuread_auth     = true
    tenant_id            = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    subscription_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    resource_group_name  = "rg-rancher"
    storage_account_name = "sa-rancher"
    container_name       = "rancher"
    key                  = "rancher-local"
  }
}

We also need to expose the role_template_ids in a way, we can use them, hence we have this output.tf

output "roleids" {
  description = "IDs for roles"
  value = {
    custom-read-only=rancher2_role_template.custom-read-only.id
    custom-project-member=rancher2_role_template.custom-project-member.id
    custom-cluster-read-only=rancher2_role_template.custom-cluster-read-only.id
    custom-global-read-only=rancher2_global_role.custom-global-read-only.id
    cluster-owner="cluster-owner"
    admin="admin"
  }
}

Now we can access them using the rolename, so for example custom-read-only-role will be accessed like this:

data.terraform_remote_state.playground-local-state.outputs.roleids["custom-read-only"]

As a next step, as I already have the list local.projects, I extend it with a map memebers

locals {
  projects = {
    "default" = {
      name                       = "Default",
      description                = "Default project created for the cluster",
      limits_cpu                 = "2000m",
      limits_memory              = "1000Mi",
      requests_cpu               = "10m",
      requests_memory            = "10Mi",
      requests_storage_project   = "5000Mi",
      requests_storage_namespace = "500Mi"
         "all-ro" = {
            name="K8s_ALL_ReadOnly"
            role="custom-project-member"
         }
         "ro" = {
            name="K8s_Default_ReadOnly"
            role="custom-read-only"
         }
         "pm" = {
            name="K8s_Default_ProjectMember"
            role="custom-project-member"
         }
      }
    },
    "system" = {
      name                       = "System",
      description                = "System project created for the cluster",
      limits_cpu                 = "2000m",
      limits_memory              = "1000Mi",
      requests_cpu               = "10m",
      requests_memory            = "10Mi",
      requests_storage_project   = "5000Mi",
      requests_storage_namespace = "500Mi"
      members = {
         "all-ro" = {
            name="K8s_ALL_ReadOnly"
            role="custom-project-member"
         }
         "ro" = {
            name="K8s_System_ReadOnly"
            role="custom-read-only"
         }
         "pm" = {
            name="K8s_System_ProjectMember"
            role="custom-project-member"
         }
      }
    },
    },
  }
}

At last I create the role binding dynamically, but first I need to create an iterable list with all data from the members element of each project. Hence I iterate over all projects and grab the required inf

locals {
   prtbs = flatten([
      for projectname,project in local.projects : [
         for role, details in project.members: {
            role_name          = "${projectname}-${role}"
            project_name       = "${projectname}"
            role_template_id   = data.terraform_remote_state.local-state.outputs.roleids["${details.role}"]
            group_principal_id = "activedirectory_group://CN=${details.name},OU=K8S,OU=Groups,DC=wyssmann,DC=intra"
         }
      ]
   ])
}

Which will get me this list

local.prtbs
[
  {
    "group_principal_id" = "activedirectory_group://CN=K8s_ALL_ReadOnly,OU=K8S,OU=Groups,DC=wyssmann,DC=intra
    "project_name" = "default"
    "role_name" = "default-all-ro"
    "role_template_id" = "rt-xxxxx"
  },
  {
    "group_principal_id" = "activedirectory_group://CN=K8s_Default_ProjectMember,OU=K8S,OU=Groups,DC=wyssmann,DC=intra
    "project_name" = "default"
    "role_name" = "default-pm"
    "role_template_id" = "rt-xxxxx"
  },
  {
    "group_principal_id" = "activedirectory_group://CN=K8s_Default_ReadOnly,OU=K8S,OU=Groups,DC=wyssmann,DC=intra
    "project_name" = "default"
    "role_name" = "default-ro"
    "role_template_id" = "rt-xxxxx"
  },
  {
    "group_principal_id" = "activedirectory_group://CN=K8s_ALL_ReadOnly,OU=K8S,OU=Groups,DC=wyssmann,DC=intra
    "project_name" = "system"
    "role_name" = "system-all-ro"
    "role_template_id" = "rt-xxxxx"
  },
  {
    "group_principal_id" = "activedirectory_group://CN=K8s_System_ProjectMember,OU=K8S,OU=Groups,DC=wyssmann,DC=intra
    "project_name" = "system"
    "role_name" = "system-pm"
    "role_template_id" = "rt-xxxxx"
  },
  {
    "group_principal_id" = "activedirectory_group://CN=K8s_System_ReadOnly,OU=K8S,OU=Groups,DC=wyssmann,DC=intra
    "project_name" = "system"
    "role_name" = "system-ro"
    "role_template_id" = "rt-xxxxx"
  },
]

From this least I use for_each to create a resource for each binding

resource "rancher2_project_role_template_binding" "prtb" {
  for_each           = { for entry in local.prtbs : "${entry.role_name}" => entry }
  name               = "${each.key}"
  project_id         = rancher2_project.pr["${each.value.project_name}"].id
  role_template_id   = each.value.role_template_id
  group_principal_id = each.value.group_principal_id
}

Conclusion

When working with a lot of objects of the same type, it fastly becomes tedious to manually create multiple similar resource blocks, and adjusting the values of each of the blocks without errors. I find it easier to have a easy manageable list/map and create resources dynamically. Luckily [Terraform] supports that, although in the beginning I struggled quite a bit to get the loops and inner loops correctly. Once you manage that, you have a lot of possibilities to make your config files much more manageable and readable.