Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS Athena Terraform Module

A secure-by-default Amazon Athena serverless-SQL workgroup — a governed query engine with its child databases, saved named queries, and federated/Hive/cross-account data catalogs, with the query-results S3 destination and encryption pinned at the workgroup level so members cannot silently query into an unencrypted or un-governed location. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 🧮 Provisions an Athena Workgroup (aws_athena_workgroup) as the keystone — the query engine boundary that scopes result location, encryption, engine version, cost cutoffs, and metrics for every query run under it.
  • 🔐 Enforces encryption at the workgroup level: result_configuration.encryption_configuration defaults to SSE-KMS when a CMK is supplied (else SSE-S3), enforce_workgroup_configuration = true blocks client-side overrides, and enable_minimum_encryption_configuration = true blocks any unencrypted result.
  • 🗃️ Models databases (aws_athena_database), named queries (aws_athena_named_query), and data catalogs (aws_athena_data_catalog) as deeply-typed for_each maps keyed by a stable name.
  • 🧵 Wires every database and named query to this workgroup (workgroup = aws_athena_workgroup.this.id) so CREATE DATABASE DDL runs inside the governed workgroup, never falling back to the account's primary.
  • 📊 Turns CloudWatch metrics on and offers a per-query bytes-scanned cost cutoff, an engine-version pin, Identity-Center / notebook execution roles, S3 Access Grants, and Spark monitoring destinations — all optional, all typed.
  • 🧩 Consumes the results S3 bucket, a KMS CMK, an IAM execution role, and an Identity Center instance strictly by reference — never creating them here.

💡 Why it matters: Athena queries routinely scan PII under privacy-regulation, and a query result lands in S3 as a file. An un-encrypted results bucket or a workgroup a member can redirect from the client is a far larger blast radius than the convenience saved — so this module pins SSE-KMS results, enforces the workgroup configuration, and blocks unencrypted output, making you opt out explicitly.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

flowchart LR
 S3["terraform-aws-s3-bucket<br/>(query_result_bucket)"]
 KMS["terraform-aws-kms<br/>(kms_key_arn)"]
 IAM["terraform-aws-iam-role<br/>(execution_role_arn)"]
 IDC["terraform-aws-iam-identity-center<br/>(identity_center_instance_arn)"]
 GLUE["terraform-aws-glue<br/>(catalog databases/tables)"]
 ATH["terraform-aws-athena"]
 LF["terraform-aws-lakeformation<br/>(CREATE_DATABASE grant)"]
 POL["terraform-aws-iam-policy<br/>(workgroup-scoped grants)"]
 CW["terraform-aws-cloudwatch-log-group<br/>(Spark monitoring)"]

 S3 -->|bucket name| ATH
 KMS -->|key arn| ATH
 IAM -->|role arn| ATH
 IDC -->|instance arn| ATH
 GLUE -->|database/table metadata| ATH
 LF -.->|governs catalog| ATH
 CW -->|log group| ATH
 ATH -->|workgroup arn| POL

 style ATH fill:#FF9900,color:#fff
Loading

Athena is an analytics platform module. It is downstream of the S3 / KMS / IAM / Identity Center foundations, queries data cataloged by Glue and governed by Lake Formation, and is upstream of the IAM policies that scope query access to its workgroup ARN.


🧬 What this module builds

flowchart TB
 subgraph mod["terraform-aws-athena"]
 WG["aws_athena_workgroup.this<br/>KEYSTONE"]
 DB["aws_athena_database.this<br/>for_each · no tags/arn"]
 NQ["aws_athena_named_query.this<br/>for_each · no tags/arn"]
 DC["aws_athena_data_catalog.this<br/>for_each · arn + tags"]
 end

 DB -->|workgroup = this.id<br/>CREATE DATABASE DDL| WG
 NQ -->|workgroup = this.id| WG
 NQ -.->|references database| DB

 style WG fill:#FF9900,color:#fff
Loading
Resource Role
aws_athena_workgroup.this Keystone — query engine settings, result configuration (S3 output + SSE-KMS/SSE-S3), CloudWatch metrics, bytes-scanned cutoff, engine version, Identity Center. Exposes arn + tags
aws_athena_database.this for_each — Glue-Catalog-backed logical databases created via CREATE DATABASE DDL run inside this workgroup. No tags, no arn (id = name)
aws_athena_named_query.this for_each — saved SQL associated with the workgroup + a database. No tags, no arn (id = a system UUID)
aws_athena_data_catalog.this for_each — additional LAMBDA (federated) / HIVE (external) / GLUE (cross-account) catalogs. Exposes arn + tags

✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0

No provider {} block is declared inside the module — it inherits the caller's configured provider (credential chain + Region). Athena is a regional service with no us-east-1 global constraint.

ℹ️ The module uses cross-variable validation (a KMS-mode-without-a-key guard and a result-destination guard), which requires Terraform 1.9+; the >= 1.12.0 floor covers it. Those two guards are evaluated at plan time (they reference other variables); the enum/format guards fire at validate time.


🔑 Required IAM Permissions

The Terraform identity needs the following (least-privilege). Scope Athena resource ARNs to your workgroup / catalog names where governance allows.

⚠️ aws_athena_database is not a Glue control-plane call — it is an Athena query execution. Terraform issues CREATE DATABASE / DROP DATABASE as a query run inside this workgroup, so the identity needs the athena:StartQueryExecution triad and S3 write access to the results bucket in addition to the underlying glue:CreateDatabase. Granting only glue:CreateDatabase will fail on database creation.

Action Required for Notes
athena:CreateWorkGroup, athena:GetWorkGroup, athena:UpdateWorkGroup, athena:DeleteWorkGroup, athena:ListWorkGroups Workgroup lifecycle Keystone resource
athena:TagResource, athena:UntagResource, athena:ListTagsForResource Tagging Workgroup + data catalogs (databases/named queries are not taggable)
athena:StartQueryExecution, athena:GetQueryExecution, athena:GetQueryResults, athena:StopQueryExecution aws_athena_database create/destroy The CREATE DATABASE / DROP DATABASE DDL runs as a query inside this workgroup
glue:CreateDatabase, glue:GetDatabase, glue:DeleteDatabase, glue:GetTables, glue:DeleteTable Underlying Glue Catalog operations the DDL performs DeleteTable/GetTables only when a database force_destroy = true on a non-empty database
athena:CreateNamedQuery, athena:GetNamedQuery, athena:DeleteNamedQuery, athena:ListNamedQueries Named-query lifecycle for_each over var.named_queries
athena:CreateDataCatalog, athena:GetDataCatalog, athena:UpdateDataCatalog, athena:DeleteDataCatalog, athena:ListDataCatalogs Federated/Hive/cross-account catalog lifecycle for_each over var.data_catalogs
s3:GetBucketLocation, s3:PutObject, s3:GetObject, s3:ListBucket Athena writing/reading query results Granted on the caller-supplied results bucket, not by this module
kms:GenerateDataKey, kms:Decrypt, kms:DescribeKey SSE-KMS / CSE-KMS result + managed-log encryption Only when var.kms_key_arn (a CMK) is supplied; granted on the key policy, not by this module
iam:PassRole (scoped to var.execution_role_arn) Passing the execution role to Athena Only when execution_role_arn is set (notebook / Identity-Center workgroups); condition on iam:PassedToService = athena.amazonaws.com

ℹ️ No iam:CreateServiceLinkedRole is required — Athena, Glue databases, and named queries do not depend on a service-linked role.


📋 AWS Prerequisites

  • S3 output bucket must already exist (hard prerequisite). The workgroup's result_configuration.output_location points at s3://<query_result_bucket>[/<prefix>]; there is no Athena-side bucket auto-creation. Wire query_result_bucket from terraform-aws-s3-bucket, which owns the SSE-KMS default encryption, the public-access block, and the aws:SecureTransport bucket policy. (Alternatively use managed_query_results_configuration for Athena-managed result storage.)
  • KMS key policy. When kms_key_arn is supplied, the key's policy must already grant Athena's runtime principal and the Terraform execution identity kms:GenerateDataKey and kms:Decrypt — this module does not edit the key policy (owned by terraform-aws-kms).
  • AwsDataCatalog always exists. Every account has a default GLUE catalog named AwsDataCatalog that Terraform does not manage; var.data_catalogs entries are for additional federated (LAMBDA), external (HIVE), or cross-account (GLUE) catalogs only.
  • Lake Formation caveat. If the account uses AWS Lake Formation to govern the Glue Catalog, the CREATE_DATABASE Lake Formation permission (separate from IAM) must also be granted to the Terraform identity, or aws_athena_database creation fails even with correct IAM.
  • Federated (LAMBDA) catalogs require the backing Lambda connector function to already be deployed; supply its ARN in the catalog's parameters — this module does not deploy connectors.
  • Region: No global-service constraint — Athena is regional; standard provider inheritance applies. No us-east-1 requirement.
  • Quotas (soft, raise via Service Quotas): 1,000 workgroups per account/Region; 256 data catalogs per account; databases bounded by the Glue Catalog's 100-databases default. bytes_scanned_cutoff_per_query minimum is 10485760 (10 MiB).

📁 Module Structure

terraform-aws-athena/
├── providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); NO provider block
├── variables.tf # deeply-typed object schemas; optional secure defaults; validation{}
├── main.tf # keystone workgroup + database / named-query / data-catalog for_each collections
├── outputs.tf # id + arn + name + tags_all + child reference maps
├── README.md # this file
└── SCOPE.md # in-scope/out-of-scope, Consumes/Emits, IAM, prerequisites, gotchas

⚙️ Quick Start

module "athena" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"

  name                = "analytics-wg"
  query_result_bucket = module.results_bucket.id # terraform-aws-s3-bucket
  kms_key_arn         = module.data_kms.arn      # terraform-aws-kms → SSE-KMS results

  databases = {
    curated = { comment = "Curated analytics database" }
  }

  named_queries = {
    top-customers = {
      database = "curated"
      query    = "SELECT customer_id, SUM(amount) AS total FROM sales GROUP BY 1 ORDER BY 2 DESC LIMIT 10;"
    }
  }

  tags = {
    environment         = "prod"
    data_classification = "npi"
  }
}

🔌 Cross-Module Contract

Consumes

Input Type Source module
query_result_bucket string (bucket name) terraform-aws-s3-bucket — results output_location
kms_key_arn string (optional) terraform-aws-kms — SSE-KMS/CSE-KMS result & log encryption
execution_role_arn string (optional) terraform-aws-iam-role — notebook / Identity Center; needs iam:PassRole
identity_center_instance_arn string (optional) terraform-aws-iam-identity-center
cloudwatch_log_group / s3_logging (in monitoring_configuration) string (optional) terraform-aws-cloudwatch-log-group, terraform-aws-s3-bucket
Federated catalog parameters["function"] string (Lambda ARN) serverless/compute module (out of scope here)

Emits

Output Description Consumed by
id Workgroup name (workgroups are named, not UUID-keyed) reference / athena:workgroup IAM conditions
arn Workgroup ARN — cross-resource reference type terraform-aws-iam-policy Resource clauses scoping query access
name Workgroup name (readable duplicate of id) query clients, JDBC/ODBC config, dashboards
effective_engine_version AWS-resolved engine version in effect drift/version audit
tags_all Merged tags incl. default_tags governance / audit
database_ids Map key ⇒ database id (name) named-query wiring, Glue / Lake Formation
named_query_ids Map key ⇒ named-query id (system UUID) saved-query reference in query clients / CI runners
data_catalog_ids / data_catalog_arns Maps key ⇒ catalog name / ARN athena:GetDataCatalog IAM Resource clauses

📚 Example Library (copy-paste)

1 · Minimal workgroup (SSE-S3, no CMK)
# Simplest call. With no kms_key_arn, results are still encrypted — SSE-S3 (S3-managed keys).
module "athena" {
  source              = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                = "sandbox-wg"
  query_result_bucket = module.results_bucket.id
}
2 · Secure baseline with a customer-managed KMS key
# The recommended posture: a CMK powers SSE-KMS results automatically.
module "athena" {
  source              = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                = "governed-wg"
  query_result_bucket = module.results_bucket.id
  kms_key_arn         = module.data_kms.arn # result_encryption_option resolves to SSE_KMS
}
3 · Database + named query (wired to this workgroup)
module "athena" {
  source              = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                = "reporting-wg"
  query_result_bucket = module.results_bucket.id
  kms_key_arn         = module.data_kms.arn

  databases = {
    curated = { comment = "Curated marts" }
    raw     = { comment = "Raw landing zone" }
  }

  named_queries = {
    daily-revenue = {
      database    = "curated" # CREATE DATABASE runs inside THIS workgroup
      query       = "SELECT dt, SUM(amount) FROM curated.orders GROUP BY 1;"
      description = "Daily revenue rollup"
    }
  }
}
4 · Tags merging with provider default_tags
# Provider default_tags (caller's concern):
provider "aws" {
  region = "us-east-1"
  default_tags { tags = { owner = "data-platform", cost_center = "1234" } }
}

module "athena" {
  source              = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                = "tagged-wg"
  query_result_bucket = module.results_bucket.id
  kms_key_arn         = module.data_kms.arn
  tags                = { environment = "prod", data_classification = "npi" }
  # tags_all on the workgroup = { owner, cost_center, environment, data_classification }
  # NOTE: databases and named queries are NOT taggable — only the workgroup + data catalogs carry tags.
}
5 · Federated LAMBDA data catalog
# The backing Lambda connector must already be deployed; supply its ARN in parameters.
data_catalogs = {
  cloudwatch-logs = {
    type        = "LAMBDA"
    description = "CloudWatch Logs federated connector"
    parameters  = { "function" = "arn:aws:lambda:us-east-1:123456789012:function:cw-connector" }
    tags        = { connector = "cloudwatch" }
  }
}
6 · HIVE (external metastore) data catalog
data_catalogs = {
  onprem-hive = {
    type        = "HIVE"
    description = "External Hive metastore via connector Lambda"
    parameters  = { "metadata-function" = "arn:aws:lambda:us-east-1:123456789012:function:hive-connector" }
  }
}
7 · Cross-account GLUE data catalog
data_catalogs = {
  shared-catalog = {
    type        = "GLUE"
    description = "Cross-account Glue Data Catalog"
    parameters  = { "catalog-id" = "210987654321" } # the owning account id
  }
}
8 · Bytes-scanned cost cutoff
module "athena" {
  source                         = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                           = "cost-guarded-wg"
  query_result_bucket            = module.results_bucket.id
  kms_key_arn                    = module.data_kms.arn
  bytes_scanned_cutoff_per_query = 10737418240 # 10 GiB — any query scanning more fails (min 10 MiB)
}
9 · Explicit result encryption override (SSE-KMS)
# Force SSE-KMS explicitly (SSE_KMS/CSE_KMS require a kms_key_arn — a plan-time guard).
module "athena" {
  source                   = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                     = "explicit-kms-wg"
  query_result_bucket      = module.results_bucket.id
  kms_key_arn              = module.data_kms.arn
  result_encryption_option = "SSE_KMS"
  query_result_prefix      = "athena/results"
}
10 · Disabled workgroup (freeze query execution)
# Keeps the workgroup + its history but blocks new query execution.
module "athena" {
  source              = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                = "frozen-wg"
  query_result_bucket = module.results_bucket.id
  kms_key_arn         = module.data_kms.arn
  state               = "DISABLED"
}
11 · Secure-by-default opt-out (⚠️ review carefully)
# EXCEPTION PATH — relaxing enforcement lets any workgroup member redirect output or
# relax encryption from the client side, defeating the module's encryption default.
# A reviewer should question why an PII-bearing workgroup would allow this.
module "athena" {
  source                                  = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                                    = "legacy-wg"
  query_result_bucket                     = module.results_bucket.id
  kms_key_arn                             = module.data_kms.arn
  enforce_workgroup_configuration         = false # ⚠️ documented exception required
  enable_minimum_encryption_configuration = false # ⚠️ documented exception required
}
12 · for_each across multiple databases and named queries
locals {
  domains = ["orders", "customers", "payments"]
}

module "athena" {
  source              = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                = "multidomain-wg"
  query_result_bucket = module.results_bucket.id
  kms_key_arn         = module.data_kms.arn

  databases = { for d in local.domains : d => { comment = "${d} domain" } }

  named_queries = { for d in local.domains : "count-${d}" => {
    database = d
    query    = "SELECT COUNT(*) FROM ${d}.events;"
  } }
}
13 · Identity-Center-enabled (notebook) workgroup
# Identity-Center workgroups run under an execution role — execution_role_arn is required,
# and the Terraform identity needs iam:PassRole scoped to it.
module "athena" {
  source                       = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                         = "spark-notebook-wg"
  query_result_bucket          = module.results_bucket.id
  kms_key_arn                  = module.data_kms.arn
  execution_role_arn           = module.athena_exec_role.arn         # terraform-aws-iam-role
  identity_center_instance_arn = module.identity_center.instance_arn # terraform-aws-iam-identity-center
}
14 · Import an existing workgroup & database (with lifecycle ignore)
import {
  to = module.athena.aws_athena_workgroup.this
  id = "analytics-wg" # workgroup name
}

import {
  to = module.athena.aws_athena_database.this["curated"]
  id = "curated" # database name
}

# ⚠️ aws_athena_database.bucket and encryption_configuration have NO read API — an imported
# database shows a permanent diff on those fields unless left null (the module default) or
# pinned with a lifecycle { ignore_changes = [bucket] } in a thin wrapper.
15 · End-to-end composition (the finale)
# A complete governed Athena footprint wiring kms, s3-bucket, and iam-role sibling modules.
module "data_kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  name   = "analytics-data"
}

module "results_bucket" {
  source      = "git::https://github.com/microsoftexpert/terraform-aws-s3-bucket?ref=v1.0.0"
  bucket      = "casey-athena-results"
  kms_key_arn = module.data_kms.arn # SSE-KMS default encryption + public-access block owned here
}

module "athena_exec_role" {
  source             = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"
  name               = "athena-notebook-exec"
  assume_role_policy = data.aws_iam_policy_document.athena_trust.json
}

module "athena" {
  source              = "git::https://github.com/microsoftexpert/terraform-aws-athena?ref=v1.0.0"
  name                = "analytics-wg"
  query_result_bucket = module.results_bucket.id
  query_result_prefix = "wg/results"
  kms_key_arn         = module.data_kms.arn
  execution_role_arn  = module.athena_exec_role.arn

  bytes_scanned_cutoff_per_query = 10737418240

  databases = {
    curated = { comment = "Curated analytics database" }
    raw     = { comment = "Raw landing database" }
  }

  named_queries = {
    top-customers = {
      database = "curated"
      query    = "SELECT customer_id, SUM(amount) AS total FROM curated.sales GROUP BY 1 ORDER BY 2 DESC LIMIT 10;"
    }
  }

  data_catalogs = {
    federated-cw = {
      type        = "LAMBDA"
      description = "CloudWatch Logs federated connector"
      parameters  = { "function" = "arn:aws:lambda:us-east-1:123456789012:function:cw-connector" }
    }
  }

  tags = { environment = "prod", data_classification = "npi" }
}

📥 Inputs

Core / identity

  • name (required) — workgroup name (FORCE-NEW).
  • query_result_bucket — results bucket name (required unless managed_query_results_configuration is set).
  • query_result_prefix, expected_bucket_owner, result_acl_configuration — result-location tuning.
  • description, state (ENABLED/DISABLED), selected_engine_version.

Encryption / security

  • kms_key_arn — customer-managed KMS key ARN (default null ⇒ SSE-S3).
  • result_encryption_optionSSE_S3 / SSE_KMS / CSE_KMS (default: SSE_KMS with a CMK, else SSE_S3).
  • enforce_workgroup_configuration, enable_minimum_encryption_configuration — default true.
  • publish_cloudwatch_metrics_enabled — default true; requester_pays_enabled — default false.
  • bytes_scanned_cutoff_per_query — per-query cost cutoff (min 10 MiB).
  • customer_content_encryption_kms_key, managed_query_results_configuration, monitoring_configuration, query_results_s3_access_grants_configuration — Spark / managed-results / Access-Grants blocks.

Execution / Identity Center

  • execution_role_arn — notebook / Identity-Center execution role; needs iam:PassRole.
  • identity_center_instance_arn — enables Identity Center (requires execution_role_arn).

Child collections

  • databasesmap(object): name, comment, properties, force_destroy, bucket, acl_configuration, encryption_configuration. No tags.
  • named_queriesmap(object): database (required), query (required), name, description. No tags.
  • data_catalogsmap(object): type (required), description (required), parameters (required), name, tags.

Universal

  • tagsmap(string), merged onto the workgroup and data catalogs (default {}).

🧾 Outputs

  • id, arn, name — the keystone workgroup (primary).
  • effective_engine_version — AWS-resolved engine version.
  • tags_all — merged tags incl. default_tags.
  • database_ids — map key ⇒ database id (name); no ARN (resource limitation).
  • named_query_ids — map key ⇒ named-query id (a system-generated UUID, unknown until apply); no ARN.
  • data_catalog_ids / data_catalog_arns — maps keyed by caller key.

No secret-bearing values are ever emitted. Athena named queries hold SQL text (not credentials); no sensitive outputs are produced by this module.


🧠 Architecture Notes

  • ID / ARN formats differ per resource type — do not assume uniformity:
  • Workgroup id = the bare name; arn = arn:aws:athena:<region>:<account>:workgroup/<name>.
  • Data catalog id = the bare name; arn = arn:aws:athena:<region>:<account>:datacatalog/<name>.
  • Database id = the database name; it has no arn and no tags argument at all — tag governance on the database must happen on the underlying Glue database (out of scope here).
  • Named query id = a system-generated UUID (not the name), unknown until after apply; it has no arn and no tags.
  • aws_athena_database creation is an Athena query execution, not a Glue control-plane call. Terraform issues CREATE DATABASE... as a query run inside a workgroup; the module wires workgroup = aws_athena_workgroup.this.id so the DDL inherits this workgroup's enforced, encrypted result configuration rather than falling back to the account primary. This is why the identity needs the athena:StartQueryExecution triad + S3 write, not just glue:CreateDatabase.
  • bucket and encryption_configuration on aws_athena_database have no read API. They are write-only; an imported database shows a permanent diff on them unless left null (the module default) or pinned with lifecycle { ignore_changes = [bucket] }. The module leaves them null by default so the workgroup's enforced result configuration governs encryption — avoiding the diff entirely.
  • tagstags_alldefault_tags: var.tags flows to the workgroup and (merged with per-catalog tags) to each data catalog; tags_all is the computed merge over provider default_tags (resource tags win on key conflict). default_tags stays the caller's provider-block concern.
  • enforce_workgroup_configuration gates the whole encryption default. With it false, a member can override result_configuration (including encryption) from the client, silently defeating the module's SSE-KMS default — hence it defaults true and requires an explicit flip.
  • query_results_s3_access_grants_configuration requires Identity Center. AWS validates this cross-field dependency only at apply time; the module adds a plan-time validation{} guard requiring identity_center_instance_arn.
  • Destroy ordering: named queries and databases reference aws_athena_workgroup.this.id, so Terraform's implicit dependency graph removes them before the workgroup. With force_destroy = false (default) the workgroup refuses to delete while it still owns named queries — do not hand-order depends_on against the children.
  • Region: regional service, no us-east-1 coupling; no region variable (provider inheritance).

🧱 Design Principles

Secure default Enforced by Opt-out
Result-set encryption ON result_configuration.encryption_configurationSSE_KMS with a CMK, else SSE_S3 result_encryption_option = "SSE_S3", or supply no kms_key_arn (still encrypted)
Client-side override protection enforce_workgroup_configuration = true enforce_workgroup_configuration = false (documented exception)
Minimum-encryption enforcement enable_minimum_encryption_configuration = true enable_minimum_encryption_configuration = false (documented exception)
KMS-mode-without-a-key is rejected cross-variable validation{} (plan time) supply kms_key_arn, or use SSE_S3
CloudWatch metrics ON publish_cloudwatch_metrics_enabled = true set false
Requester-pays OFF requester_pays_enabled = false set true
Workgroup destroy safety force_destroy = false (refuses while named queries exist) force_destroy = true
Per-database destroy safety per-database force_destroy = false (tables unrecoverable if forced) force_destroy = true in the map object
Workgroup enabled state = "ENABLED" state = "DISABLED" (freeze without deleting)

Other principles: one keystone resource named this; children via for_each over keyed maps (no count); deeply-typed object schemas; no credential variables; no region variable; no provider {} block.


🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
# plan/apply require valid AWS credentials (profile / SSO / OIDC) + a region:
terraform plan
terraform apply
terraform output

⚠️ Always pin the module by immutable tag — ?ref=v1.0.0 — never a branch.


🧪 Testing

  • terraform init -backend=falseterraform validateterraform fmt -check (offline gate; all pass).
  • Offline plan smoke test with a credential-skipping provider (skip_credentials_validation, skip_requesting_account_id, skip_metadata_api_check) confirms all dynamic blocks and validations evaluate. A rich call (workgroup + 2 databases + 2 named queries + 2 data catalogs) plans to 7 resources with Plan: 7 to add.
  • Negative tests: an invalid data_catalog type fails at validate; result_encryption_option = "SSE_KMS" with no kms_key_arn, and a workgroup with no result destination, both fail at plan (cross-variable guards).
  • Live apply is a separate, human-reviewed step in a non-production account — never in CI.

💬 Example Output

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

Outputs:
arn = "arn:aws:athena:us-east-1:123456789012:workgroup/analytics-wg"
id = "analytics-wg"
name = "analytics-wg"
effective_engine_version = "Athena engine version 3"
database_ids = { "curated" = "curated", "raw" = "raw" }
named_query_ids = { "top-customers" = "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d" }
data_catalog_arns = { "federated-cw" = "arn:aws:athena:us-east-1:123456789012:datacatalog/federated-cw" }

🔍 Troubleshooting

  • AccessDeniedException creating a database — the identity has glue:CreateDatabase but is missing the athena:StartQueryExecution / GetQueryExecution / GetQueryResults triad or S3 write on the results bucket. aws_athena_database runs CREATE DATABASE as an Athena query, not a Glue API call.
  • Database creation fails under Lake Formation — even with correct IAM, Lake Formation requires the separate CREATE_DATABASE permission granted to the Terraform identity. Grant it via terraform-aws-lakeformation.
  • Plan fails: "result_encryption_option SSE_KMS/CSE_KMS requires a kms_key_arn" — supply kms_key_arn (wire terraform-aws-kms) or use SSE_S3. Intended fail-fast guard, not a bug.
  • Plan fails: "needs one result destination" — provide query_result_bucket (standard) or managed_query_results_configuration (Athena-managed); a workgroup needs exactly one.
  • Permanent diff on an imported database's bucket — those fields have no read API; leave them null (module default) or add lifecycle { ignore_changes = [bucket] }.
  • destroy fails: "workgroup is not empty" — the workgroup still owns named queries and force_destroy = false. Let Terraform remove the children first (implicit via workgroup = this.id), or set force_destroy = true with a documented exception.
  • Tag drift on plan — a key exists in both provider default_tags and module tags; resource tags win, so remove the duplicate from one side. tags_all shows the merged result. (Databases/named queries carry no tags at all.)
  • Credential-chain / region failure on plan/apply — no valid AWS_PROFILE/SSO/OIDC session or region; the module inherits the caller's provider and configures nothing itself.

🔗 Related Docs

  • Terraform Registry — aws_athena_workgroup, aws_athena_database, aws_athena_named_query, aws_athena_data_catalog.
  • AWS — Amazon Athena User Guide (workgroups, workgroup settings override, minimum encryption configuration), Encrypting Athena query results, Using Athena Data Catalogs (federated/Lambda, external Hive, cross-account Glue), Athena service quotas, Using Lake Formation with Athena.
  • Sibling modules — terraform-aws-s3-bucket, terraform-aws-kms, terraform-aws-iam-role, terraform-aws-iam-identity-center, terraform-aws-glue, terraform-aws-lakeformation, terraform-aws-cloudwatch-log-group.

🧡 "Infrastructure as Code should be standardized, consistent, and secure."