DEV Community

Terraform Fundamentals: DSQL

Diving Deep into Terraform's DSQL: Data Source Queries for Dynamic Infrastructure

Infrastructure often requires querying existing data to drive provisioning. Consider a scenario: you need to deploy a new application, but its database credentials must be retrieved from a secrets management system based on the environment and application name. Hardcoding these values is unacceptable. Similarly, dynamically assigning IP addresses from a pool based on current utilization is a common requirement. Traditional Terraform data sources often fall short when complex queries or filtering are needed. This is where Terraform’s Data Source Query Language (DSQL) comes into play, offering a powerful way to interact with databases and APIs directly within your Terraform configurations. DSQL integrates into IaC pipelines as a data retrieval step before resource creation, enabling truly dynamic infrastructure. It’s a core component of platform engineering stacks aiming for self-service infrastructure.

What is DSQL in Terraform Context?

DSQL isn’t a standalone Terraform provider; it’s a feature within the http data source. It allows you to execute SQL-like queries against any API endpoint that returns JSON. This is achieved by leveraging the query parameter of the http data source and crafting a DSQL expression. The http data source itself is part of the core Terraform provider, meaning no external provider installation is required.

The key is understanding that DSQL isn’t executing SQL against a traditional database. It’s parsing JSON responses and extracting data based on a query language inspired by SQL. This means you need to understand the structure of the API response you’re querying.

There are a few caveats. DSQL expressions are sensitive to the JSON structure. Changes to the API response format will break your Terraform code. Error handling can be tricky, as the http data source only provides limited information about query failures. Performance can also be a concern with complex queries against slow APIs.

Use Cases and When to Use

DSQL shines in scenarios where traditional Terraform data sources are insufficient:

  1. Dynamic Credential Retrieval: Fetching database credentials from a secrets manager (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault) based on environment tags and application names. This avoids hardcoding secrets and enables rotation.
  2. IP Address Allocation: Querying an IP address management (IPAM) system to find available IP addresses within a specific subnet, based on current utilization. This automates network configuration.
  3. Service Discovery: Discovering the endpoint of a running service (e.g., a Kubernetes service) to configure load balancers or other dependent resources.
  4. Compliance Checks: Querying a compliance API to verify that a resource meets specific security requirements before provisioning.
  5. Cost Optimization: Querying a cloud provider’s cost explorer API to determine the optimal instance type based on current pricing and usage patterns. This is particularly valuable for SREs focused on cost management.

Key Terraform Resources

Here are some essential resources for working with DSQL:

  1. http Data Source: The core resource for executing DSQL queries.
   data "http" "example" {
     url = "https://api.example.com/data"
     method = "GET"
     query = "SELECT name FROM data WHERE status = 'active'"
   }
Enter fullscreen mode Exit fullscreen mode
  1. jsondecode Function: Used to parse the JSON response from the http data source.
   locals {
     parsed_data = jsondecode(data.http.example.body)
   }
Enter fullscreen mode Exit fullscreen mode
  1. length Function: Determines the number of elements in a list or map. Useful for iterating over query results.
   output "number_of_results" {
     value = length(local.parsed_data)
   }
Enter fullscreen mode Exit fullscreen mode
  1. lookup Function: Retrieves a value from a map based on a key.
   output "first_name" {
     value = lookup(local.parsed_data[0], "name", "N/A")
   }
Enter fullscreen mode Exit fullscreen mode
  1. tolist Function: Converts a set or map to a list.
   locals {
     data_list = tolist(local.parsed_data)
   }
Enter fullscreen mode Exit fullscreen mode
  1. for_each Meta-Argument: Iterates over a list or map to create multiple resources.
   resource "aws_instance" "example" {
     for_each = { for item in local.data_list : item.id => item }
     ami           = "ami-0c55b2ab99196936a"
     instance_type = "t2.micro"
     tags = {
       Name = each.value.name
     }
   }
Enter fullscreen mode Exit fullscreen mode
  1. try Function: Handles potential errors during data retrieval.
   locals {
     safe_data = try(jsondecode(data.http.example.body), [])
   }
Enter fullscreen mode Exit fullscreen mode
  1. flatten Function: Converts a nested list or map into a single list.
   locals {
     flattened_data = flatten([for item in local.parsed_data : [item.value]])
   }
Enter fullscreen mode Exit fullscreen mode

Common Patterns & Modules

Using DSQL with a remote backend (e.g., Terraform Cloud, S3) is crucial for state management and collaboration. Dynamic blocks are frequently used to iterate over query results and create multiple resources. The for_each meta-argument is essential for this.

A layered module structure is recommended. A base module handles the http data source and DSQL query, while child modules consume the retrieved data to provision specific resources. Monorepos are well-suited for managing complex DSQL-driven infrastructure.

While there aren’t many publicly available modules specifically for DSQL, you can find modules for interacting with specific APIs (e.g., Vault, AWS Secrets Manager) that can be adapted to use DSQL for more complex queries.

Hands-On Tutorial

Let's retrieve a list of active users from a mock API and create an AWS IAM user for each.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

data "http" "users" {
  url = "https://jsonplaceholder.typicode.com/users"
  method = "GET"
}

locals {
  parsed_users = jsondecode(data.http.users.body)
}

resource "aws_iam_user" "user" {
  for_each = { for user in local.parsed_users : user.id => user }
  name = each.value.username
  path = "/"
}

output "user_arns" {
  value = [for user_id, user in aws_iam_user.user : user.arn]
}
Enter fullscreen mode Exit fullscreen mode

terraform plan output will show the creation of multiple IAM users, one for each user returned by the API. terraform apply will create them. terraform destroy will remove them.

This example demonstrates a simple integration within a CI/CD pipeline. The Terraform configuration would be version-controlled, and a pipeline would execute terraform fmt, terraform validate, terraform plan, and terraform apply on each commit.

Enterprise Considerations

Large organizations leverage Terraform Cloud/Enterprise for state management, remote operations, and collaboration. Sentinel or Open Policy Agent (OPA) are used for policy-as-code, enforcing constraints on the data retrieved via DSQL (e.g., only allowing users with specific roles to be created).

IAM design is critical. The Terraform service account needs least-privilege access to the API endpoint and the cloud provider resources. State locking is essential to prevent concurrent modifications. Secure workspaces should be used to isolate environments.

Costs are primarily driven by API usage and the number of resources provisioned based on the query results. Scaling requires careful consideration of API rate limits and the performance of the http data source. Multi-region deployments require replicating the DSQL query to each region.

Security and Compliance

Enforce least privilege by granting the Terraform service account only the necessary permissions to the API endpoint and cloud resources. Use aws_iam_policy (or equivalent for other providers) to define granular permissions.

resource "aws_iam_policy" "http_policy" {
  name        = "http-data-source-policy"
  description = "Policy for accessing the user API"
  policy      = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = ["sts:AssumeRole"]
        Effect   = "Allow"
        Resource = "*"
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

Implement drift detection to identify discrepancies between the desired state (defined in Terraform) and the actual state. Tagging policies ensure consistent metadata for auditing and cost allocation. Audit logs should be enabled to track all DSQL queries and resource changes.

Integration with Other Services

  1. HashiCorp Vault: Retrieve secrets using DSQL to query Vault’s API.
  2. AWS Secrets Manager: Fetch database credentials or API keys.
  3. Azure Key Vault: Similar to AWS Secrets Manager, retrieve secrets from Azure.
  4. Kubernetes API: Discover service endpoints or pod information.
  5. IPAM Systems (Infoblox, BlueCat): Allocate IP addresses dynamically.
graph LR
    A[Terraform Configuration] --> B(http Data Source with DSQL);
    B --> C{API Endpoint (e.g., Vault, AWS Secrets Manager)};
    C --> B;
    B --> D[Parsed JSON Data];
    D --> E[Resource Provisioning (e.g., AWS IAM User)];
Enter fullscreen mode Exit fullscreen mode

Module Design Best Practices

Abstract DSQL into reusable modules. Define clear input variables for the API URL, query, and authentication details. Use output variables to expose the retrieved data. Leverage locals to simplify complex queries. Document the module thoroughly, including examples and limitations. Use a backend (e.g., S3) for state storage.

CI/CD Automation

# .github/workflows/terraform.yml

name: Terraform Apply

on:
  push:
    branches:
      - main

jobs:
  apply:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: hashicorp/setup-terraform@v2
      - run: terraform fmt
      - run: terraform validate
      - run: terraform plan -out=tfplan
      - run: terraform apply tfplan
Enter fullscreen mode Exit fullscreen mode

Pitfalls & Troubleshooting

  1. JSON Structure Changes: API updates break queries. Monitor API documentation.
  2. Rate Limiting: APIs impose rate limits. Implement retry logic.
  3. Authentication Errors: Incorrect credentials. Verify API keys/tokens.
  4. DSQL Syntax Errors: Incorrect query syntax. Use a JSON parser to validate the query.
  5. Data Type Mismatches: Incorrect data types in the query. Use appropriate functions (e.g., tonumber, tostring).
  6. Empty Results: Query returns no data. Verify the query logic and API data.

Pros and Cons

Pros:

  • Flexibility: Queries any API returning JSON.
  • Dynamic Infrastructure: Enables truly dynamic provisioning.
  • Reduced Hardcoding: Avoids hardcoding secrets and configuration values.

Cons:

  • Fragility: Sensitive to API changes.
  • Complexity: Requires understanding of DSQL and API structure.
  • Performance: Can be slow for complex queries.
  • Error Handling: Limited error information from the http data source.

Conclusion

DSQL is a powerful tool for building dynamic and adaptable infrastructure with Terraform. It bridges the gap between traditional IaC and the need to interact with external APIs and data sources. While it introduces complexity, the benefits of increased flexibility and automation are significant. Engineers should prioritize experimenting with DSQL in proof-of-concept projects, evaluating existing modules, and integrating it into their CI/CD pipelines to unlock its full potential.

Top comments (0)