Terraform Integration

TagOps Terraform integration automatically calculates tags for your resources at deploy time. You include the TagOps module in your Terraform configuration, and it returns computed tags based on your tagging rules — which you then apply to your resources.

Terraform Module

This feature is built on a public Terraform module: terraform-aws-tagops. The module sends your resource definitions to the TagOps SaaS API and returns the computed tags as a Terraform output map. You reference the output map to apply tags to your resources.

The module is published on the Terraform Registry.

How It Works

  1. You add module "tagops" to your Terraform configuration and define which resources need tags.
  2. On every terraform plan or terraform apply, the module sends a request to the TagOps SaaS API.
  3. The API applies your tagging rules and returns computed tags for each resource.
  4. You use module.tagops.tags["<resource_key>"] to apply the tags to your resources.
Your AWS Account                         TagOps SaaS
┌─────────────────────────┐             ┌──────────────────────────┐
│ Terraform               │             │                          │
│   module "tagops"       │             │  Terraform API           │
│         │               │             │  - Applies tagging rules │
│         ▼               │   HTTP POST │  - Calculates tags       │
│ data "http" "tagops"    │────────────▶│  - Returns tag map       │
│ (sends resource list)   │  Bearer     │    per resource          │
│                         │  token      │                          │
└─────────────────────────┘             └──────────────────────────┘

The module makes a fresh API request on every plan/apply run, so changes to your tagging rules in TagOps are reflected immediately — no module update or cache invalidation is needed.

Prerequisites

  • API token for authentication. See API Tokens for how to create and manage tokens.
  • Terraform >= 1.5.0
  • AWS provider >= 4.0.0

Installation

Add the TagOps module to your Terraform configuration:

module "tagops" {
  source  = "tagops/tagops/aws"
  version = "~> 0.1.1"

  api_token = data.aws_ssm_parameter.tagops_api_token.value
  # ...
}

Or reference the GitHub repository directly:

module "tagops" {
  source = "git::https://github.com/tagops/terraform-aws-tagops.git?ref=main"

  api_token = data.aws_ssm_parameter.tagops_api_token.value
  # ...
}

Storing the API Token

Store your API token securely using AWS Secrets Manager, AWS SSM Parameter Store (as a SecureString), or another secret store. Never hardcode API tokens in your Terraform files or commit them to version control.

SSM Parameter Store (SecureString):

data "aws_ssm_parameter" "tagops_api_token" {
  name            = "/tagops/api_token"
  with_decryption = true
}

module "tagops" {
  source    = "tagops/tagops/aws"
  version   = "~> 0.1.1"
  api_token = data.aws_ssm_parameter.tagops_api_token.value
  # ...
}

AWS Secrets Manager:

data "aws_secretsmanager_secret_version" "tagops_api_token" {
  secret_id = "tagops/api_token"
}

module "tagops" {
  source    = "tagops/tagops/aws"
  version   = "~> 0.1.1"
  api_token = data.aws_secretsmanager_secret_version.tagops_api_token.secret_string
  # ...
}

Module Variables

Variable Type Required Default Description
api_token string yes TagOps API token. Marked as sensitive.
api_url string no https://api.tagops.cloud/terraform TagOps API endpoint URL.
default_tags map(string) no {} Default tags sent for all tag calculations.
default_resources list(string) no [] List of Terraform resource types for tag calculation.
custom_resources map(object) no {} Map of resources with type, name, and optional tags.

Module Outputs

Output Description
tags Map of computed tags keyed by resource identifier.
status_code HTTP status code from the TagOps API.

Usage

Default Resources

Use default_resources when your TagOps rules do not depend on individual resource names or existing tag values. All resources of the same type receive the same set of tags. This is the simplest approach and can be demostrated well with community modules like terraform-aws-modules/vpc/aws.

Each resource type in the list produces a tag entry keyed as default_<resource_type>.

locals {
  default_tags = {
    managed-by       = "terraform"
    environment-name = "production"
  }
}

data "aws_ssm_parameter" "tagops_api_token" {
  name            = "/tagops/api_token"
  with_decryption = true
}

module "tagops" {
  source       = "tagops/tagops/aws"
  version      = "~> 0.1.1"
  api_token    = data.aws_ssm_parameter.tagops_api_token.value
  default_tags = local.default_tags
  default_resources = [
    "aws_vpc",
    "aws_subnet",
    "aws_internet_gateway",
    "aws_route_table",
    "aws_nat_gateway",
    "aws_eip",
    "aws_network_acl",
    "aws_security_group",
  ]
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "6.5.1"
  name    = "production"
  cidr    = "10.0.0.0/16"
  # ...

  vpc_tags         = merge(module.tagops.tags["default_aws_vpc"], { Name = "vpc-production" })
  igw_tags         = merge(module.tagops.tags["default_aws_internet_gateway"], { Name = "igw-production" })
  nat_gateway_tags = merge(module.tagops.tags["default_aws_nat_gateway"], { Name = "nat-production" })
  nat_eip_tags     = merge(module.tagops.tags["default_aws_eip"], { Name = "eip-production-nat" })
}

Custom Resources

Use custom_resources when your TagOps rules do depend on individual resource names or existing tag values — meaning different resources of the same type may receive different tags.

Each entry in the map is keyed by a name you choose. The key is used to reference the computed tags in module.tagops.tags["<key>"].

module "tagops" {
  source       = "tagops/tagops/aws"
  version      = "~> 0.1.1"
  api_token    = data.aws_ssm_parameter.tagops_api_token.value
  default_tags = local.default_tags
  custom_resources = {
    s3_logs = {
      type = "aws_s3_bucket"
      name = "s3-bucket-for-logs-123456789012"
      tags = {
        app     = "logs"
        product = "tagops"
      }
    }
    s3_test = {
      type = "aws_s3_bucket"
      name = "s3-bucket-test-123456789012"
      tags = {
        app     = "test"
        product = "tagops"
      }
    }
  }
}

module "s3_bucket_for_logs" {
  source = "terraform-aws-modules/s3-bucket/aws"
  bucket = "s3-bucket-for-logs-123456789012"

  tags = merge({
    app     = "logs"
    product = "tagops"
  }, module.tagops.tags["s3_logs"])
}

module "s3_bucket_test" {
  source = "terraform-aws-modules/s3-bucket/aws"
  bucket = "s3-bucket-test-123456789012"

  tags = merge({
    app     = "test"
    product = "tagops"
  }, module.tagops.tags["s3_test"])
}

Combining Default and Custom Resources

You can use both default_resources and custom_resources together. They are merged internally into a single API request:

module "tagops" {
  source       = "tagops/tagops/aws"
  version      = "~> 0.1.1"
  api_token    = data.aws_ssm_parameter.tagops_api_token.value
  default_tags = local.default_tags

  default_resources = ["aws_vpc", "aws_subnet"]

  custom_resources = {
    web_server = {
      type = "aws_instance"
      name = "web-production-01"
      tags = { Name = "web-production-01" }
    }
  }
}

Multi AWS Providers (Multi-Region/Account)

The module detects the AWS account ID and region from the AWS provider. When you manage resources across multiple regions/accounts, create a separate TagOps module instance for each region/account and pass an aliased provider using the providers block. Each instance makes its own API call with the correct region/account.

provider "aws" {
  default_tags {
    tags = local.default_tags
  }
}

provider "aws" {
  alias  = "us-east-1"
  region = "us-east-1"
  default_tags {
    tags = local.default_tags
  }
}

# Default region
module "tagops" {
  source       = "tagops/tagops/aws"
  version      = "~> 0.1.1"
  api_token    = data.aws_ssm_parameter.tagops_api_token.value
  default_tags = local.default_tags
  custom_resources = {
    foo = {
      type = "aws_ssm_parameter"
      name = "foo"
      tags = { app = "test", product = "tagops" }
    }
  }
}

# us-east-1 region
module "tagops_us_east_1" {
  source       = "tagops/tagops/aws"
  version      = "~> 0.1.1"
  providers = {
    aws = aws.us-east-1
  }
  api_token    = data.aws_ssm_parameter.tagops_api_token.value
  default_tags = local.default_tags
  custom_resources = {
    foo2 = {
      type = "aws_ssm_parameter"
      name = "foo2"
      tags = { app = "test", product = "tagops" }
    }
  }
}

resource "aws_ssm_parameter" "foo" {
  name  = "foo"
  type  = "String"
  value = "bar"
  tags  = merge({ app = "test", product = "tagops" }, module.tagops.tags["foo"])
}

resource "aws_ssm_parameter" "foo2" {
  provider = aws.us-east-1
  name     = "foo2"
  type     = "String"
  value    = "bar2"
  tags     = merge({ app = "test", product = "tagops" }, module.tagops_us_east_1.tags["foo2"])
}

Default Tags

The default_tags variable sends baseline tags to TagOps for every resource in the request. This should typically match the tags in your AWS provider default_tags block, so TagOps rules can evaluate against the full set of tags each resource will have.

locals {
  default_tags = {
    managed-by       = "terraform"
    environment-name = "production"
    environment-type = "prod"
    product          = "tagops"
  }
}

provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = local.default_tags
  }
}

module "tagops" {
  # ...
  default_tags = local.default_tags
}

Tag Calculation

For each resource in the request, TagOps calculates the final set of tags by merging three sources. Tags from higher-priority sources override tags with the same key from lower-priority sources.

Tag Sources (in order of priority, lowest to highest)

Priority Source Description
1 (lowest) Default Tags (default_tags variable) Baseline tags sent with the request. Applied to all resources.
2 Resource Tags (tags in custom_resources) Existing tags on the resource, passed via custom_resources.
3 (highest) TagOps Rule Tags Tags calculated by TagOps based on your tagging rules and account settings.

Merge Behavior

  • Tags are merged by key. If the same key exists in multiple sources, the higher-priority source wins.
  • For default_resources, only Default Tags and TagOps Rule Tags are evaluated (there are no resource-level tags).
  • For custom_resources, all three sources are evaluated.
  • The module returns the merged result — you apply it to your resources using merge() with any additional tags (like Name).

Example

Given default_tags = { managed-by = "terraform", team = "platform" } and a custom resource with tags = { team = "data-engineering", app = "my-app" }, and a TagOps rule that adds cost-center = "engineering":

Key Value Source
managed-by terraform Default Tags
team data-engineering Resource Tags (overrides platform from Default Tags)
app my-app Resource Tags
cost-center engineering TagOps Rule Tags

Handling Dynamic Tags

TagOps may add dynamic tags such as created-by and creation-date when resources are first discovered by Scan-Based Tagging or Event-Based Tagging. On subsequent terraform apply runs, Terraform would detect these tags as drift and try to remove them.

Use lifecycle.ignore_changes to prevent Terraform from removing these tags:

resource "aws_ssm_parameter" "foo" {
  name  = "foo"
  type  = "String"
  value = "bar"
  tags = merge({
    app     = "test"
    product = "tagops"
  }, module.tagops.tags["foo"])

  lifecycle {
    ignore_changes = [
      tags["created-by"],
      tags["creation-date"]
    ]
  }
}

This applies to any resource type. Add ignore_changes for the specific tag keys that are managed outside of Terraform.

Supported Resources

TagOps applies tags to all Terraform resource types that support tagging. If a resource type in the request is not recognized, the module returns only the default_tags for that resource. See Supported Services for the full list.

Limitations

  • creation-date dynamic tag is not available at deploy time. The API call happens before resources are created, so there is no creation timestamp. TagOps sets creation-date via Event-Based Tagging from the CloudTrail event timestamp.

  • Single API call per module instance. The module makes one API request per plan/apply. If you need tags for resources across multiple accounts or regions, use separate module instances (see Multi AWS Providers).