Managing Multiple commercetools Projects with Terraform

The commercetools platform simplifies managing products, categories, user accounts, and other aspects of a modern eCommerce website. However, commercetools does not have an environment migration function to promote changes. To facilitate project migrations, there is a Terraform provider for commercetools to make it easier to administer these settings, an essential component to moving changes from a development project to a production project. This article will explore how to manage Project Settings as a general example of organizing a commercetools Terraform project.

Change Management

Commercetools is a modern composable headless eCommerce platform that can be customized with microservices using the commercetools APIs. The platform simplifies managing products, categories, user accounts, and other aspects of a modern eCommerce website. Additionally, various administrative configuration settings are available to add functionality related to taxes, currencies, shipping methods, languages, and more. These are largely managed within the commercetools Merchant Center. However, commercetools does not have an environment migration function to promote changes. To facilitate project migrations, there is a Terraform provider for commercetools to make it easier to administer these settings, an essential component to moving changes from a development project to a production project.

Terraform Projects

Our experience with the commercetools Terraform provider has led us to create four separate Terraform projects that are largely data-driven to manage multiple commercetools projects that will be explored:

  • Project Settings
    Manages countries, languages, currencies, tax categories and rates, shipping methods, and other similar settings in the “Project settings” within the Merchant Center
  • Product Types
    Maintains the complexities of ensuring product types and attributes are consistent in each commercetools project without requiring custom API calls
  • Categories
    Builds categories and their parent/child relationships
  • API Clients
    Ensures the various commercetools API clients are properly configured with the scopes necessary for each application component and removes human error

This article will explore how to manage Project Settings as a general example of organizing a commercetools Terraform project.

Commercetools Projects & Terraform Workspaces

Terraform workspaces can be used to simplify managing multiple commercetools projects by using the same terraform to manage each commercetools project separately. When setting up new commercetools projects, follow a naming standard that makes it simple to identify the environment or difference between each. The Terraform workspace names will match these commercetools project names.

Example commercetools project naming scheme: {ORG}{ENV}{NUM}

where: {ORG} = an organization identifier, like “aries”

{ENV} = the environment tied to the commercetools project, like “dev” or “prod”

{NUM} = a digit representing the numbered environment

Project Settings

Requirements:

  • The latest version of Terraform
  • A text editor; must be a text editor, like Sublime or Notepad++
  • Access to the commercetools git repositories
  • Have a commercetools API “environment variable” client with admin privileges to manage project settings, categories, product types, and API client

Projects for commercetools must be created manually, but all project configurations can be managed with API calls. Terraform can manage all of the project settings that have an API.

Workspace Validation

When using workspace names tied to specific project input data, and a provider that relies on environment variables to connect to the project, care must be taken to ensure the workspace and the project set in the environment match. Additionally, since the workspace name must match a commercetools project name, Terraform will exit with a message to select or create a project workspace if terraform.workspace is set to “default.” External data sources can be used as a fail-safe to validate the workspace and environment match, printing some messages if there’s a mismatch.

Example Code, validate.tf:

locals {
  err_workspace = <<EOF
ERROR: default workspace is not allowed!
      create or select a workspace by checking the terraform.tfvars file
      and use one of the project names. Example, to use the dev settings:

terraform workspace select aries-dev-1 || \   terraform workspace new aries-dev-1
EOF

  err_project = <<EOF
ERROR: commercetools project does not match workspace name!
        create or select a workspace with a name that matches the        commercetools project in $CTP_PROJECT_KEY.
      project from workspace  : ${terraform.workspace}
      project from environment: ${data.external.env.result[“CTP_PROJECT_KEY”]}

terraform workspace select ${data.external.env.result[“CTP_PROJECT_KEY”]} || \
  terraform workspace new ${data.external.env.result[“CTP_PROJECT_KEY”]}
EOF
}

# Exit with Error Message if running in the default workspace
data “external” “check_workspace” {
  count = terraform.workspace == “default” ? 1 : 0
  program = [“sh”, “-c”, “>&2 printf ‘${local.err_workspace}’; exit 1”]
}

# Capture CTP_PROJECT_KEY from env
data “external” “env” {
  program = [“bash”, “-c”, “echo -n \”{\\\”CTP_PROJECT_KEY\\\”:\\\”$CTP_PROJECT_KEY\\\”}\””]
}

# Exit with Error Message if workspace name doesn’t match the# CTP_PROJECT_KEY environment variable
data “external” “check_project” {
  count = terraform.workspace == data.external.env.result[“CTP_PROJECT_KEY”] ? 0 : 1
  program = [“sh”, “-c”, “>&2 printf ‘${local.err_project}’; exit 1”]
}

In each top-level resource, set the depends_on list to make sure the validations are performed.

Example Code:

  depends_on = [
    data.external.check_workspace,
    data.external.check_project
  ]

Data Structure

Input data will be managed in terraform.tfvars using a variable and local configuration based on the workspace name. A freeform map variable is needed for shared settings.

Example Code, variables.tf:

variable “shared_config” {
  type = map(any)
}

The input data structure maps to this variable.

Example Code, terraform.tfvars:

# Settings shared by all commercetools projectsshared_config = {  # Each section maps to a commercetools terraform resource, example for  # commercetools_project_settings:  project_settings = {    enable_search_index_products = true    enable_search_index_orders   = false    carts = {      country_tax_rate_fallback_enabled   = false      delete_days_after_last_modification = 90    }    messages = {      enabled                    = true      delete_days_after_creation = 15    }
    countries        = [“US”]
    currencies       = [“USD”]
    languages        = [“en-US”]  }  # additional data can be added to meet the needs of the projects
}

These variables can be mapped to locals for simpler access to the underlying data. Additionally, the commercetools project can be captured from the workspace name.

Example Code, locals.tf:

locals {
  ct_project = terraform.workspace
 
  # Shared Settings
  project_settings = var.shared_config.project_settings

Example Resource: commercetools_project_settings

The input data is used to create the commercetools object.

Example Code, project-settings.tf:

resource “commercetools_project_settings” “project” {
  name                         = local.ct_project
  countries                    = local.project_settings.countries
  currencies                   = local.project_settings.currencies
  languages                    = local.project_settings.languages

  enable_search_index_products = local.project_settings.enable_search_index_products
  enable_search_index_orders   = local.project_settings.enable_search_index_orders

 
  dynamic “carts” {
    for_each = local.project_settings.carts
    content {
      country_tax_rate_fallback_enabled   = carts.value.country_tax_rate_fallback_enabled
      delete_days_after_last_modification = carts.value.delete_days_after_last_modification
    }
  }
  dynamic “messages” {
    for_each = local.project_settings.messages
    content {
      enabled = messages.value.enabled
      delete_days_after_creation = messages.value.delete_days_after_creation
    }
  }

  depends_on = [
    data.external.check_workspace,
    data.external.check_project
  ]
}

In this case, there is a single object with minimal input data manipulation. However, this data structure becomes more compelling when a for_each loop is needed to build out multiple resources of a type.

Example Resource: commercetools_shipping_zone

To create multiple shipping zones, update the project_settings data structure to include a shipping_zones map.

Example Code, terraform.tfvars:

# Settings shared by all commercetools projects
shared_config = {
  # … add:
  shipping_zones = {    US = {      name        = “United States”      description = “Shipping zone for United States”      locations   = [{ country = “US”, state = null }]    }    CA = {      name        = “Canada”      description = “Shipping zone for Canada”      locations   = [{ country = “CA”, state = null }]    }  }
  # additional data can be added to meet the needs of the projects
}

A local variable can be created to simplify access to the data.

Example Code, locals.tf:

locals {
  # … add:
  shipping_zones = var.shared_config.shipping_zones
}

Shipping zones for the projects are then created using the input data for shipping zones.

Example Code, shipping-zone.tf:

resource “commercetools_shipping_zone” “this” {
  for_each    = local.shipping_zones
  key         = each.key
  name        = each.value.name
  description = each.value.description
  dynamic “location” {
    for_each = each.value.locations
    content {
      state   = location.value.state
      country = location.value.country
    }
  }

  depends_on = [
    data.external.check_workspace,
    data.external.check_project
  ]
}

Running the Terraform Project

Create a commercetools admin API Client for Infrastructure as Code (IaC) processes using the “Admin client” scopes profile and name it “iac”. Save the “environment variables” API client into an rc file with the name of the project. (For example: aries-dev-1.rc.)

To apply changes to a commercetools project, source the project’s API client environment variables, initialize the terraform project, select or create the project’s workspace, and apply the terraform.

cd tools-commercetools-project
source ~/aries-dev-1.rc
terraform init
terraform workspace select aries-1 || terraform workspace new aries-dev-1
terraform apply

CI/CD

A CI/CD pipeline can be created for the build tools in use that follows the above procedure. However, move the commercetools API client ID and Secret to a secrets manager or vault, and retrieve those values as part of the CI/CD process. Additionally, pipeline progression of changes from one environment to another can be easily done with CI/CD controls since the changes for the commercetools projects are tied to the workspace names.

Importing Pre-existing Resources

Existing commercetools projects that have been in use will need to be imported into the Terraform state file to properly manage the settings. Select the commercetools project that has the most current settings and configuration, build out the input data file to match, import all commercetools resources, and make sure the Terraform state matches what’s in that project by checking that there are no changes when running “terraform plan”.

The method to do this is to iterate on the selected project with the following steps:

1. Create a commercetools admin API Client for Infrastructure as Code (IaC) processes using the “Admin client” scopes profile named “iac”.

terraform project example

2. Extract the input data from the commercetools project and populate the data file with the settings to match for each resource type.

3. Import the resources from the selected project:

terraform import ‘some.resource[index]’ ‘resource-id’

4. Run the “terraform plan” until it shows no changes.

In some cases, the Terraform state requires the removal of one or more resources that become out-of-sync with the selected primary commercetools project, followed by reimporting it into the state file.

Conclusion

Moving changes between commercetools projects can be made easily when using Terraform with workspaces to separate the resource state. Additionally, using input data to drive the creation of resources using for_each allows for a data-driven Terraform model that is manageable for engineers that are less comfortable with Terraform.

Leverage the Aries Solutions expertise for your business and contact us today!