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”.
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!