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.
- 🧮 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_configurationdefaults to SSE-KMS when a CMK is supplied (else SSE-S3),enforce_workgroup_configuration = trueblocks client-side overrides, andenable_minimum_encryption_configuration = trueblocks any unencrypted result. - 🗃️ Models databases (
aws_athena_database), named queries (aws_athena_named_query), and data catalogs (aws_athena_data_catalog) as deeply-typedfor_eachmaps keyed by a stable name. - 🧵 Wires every database and named query to this workgroup (
workgroup = aws_athena_workgroup.this.id) soCREATE DATABASEDDL runs inside the governed workgroup, never falling back to the account'sprimary. - 📊 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
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.
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
| 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 |
| 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.0floor covers it. Those two guards are evaluated at plan time (they reference other variables); the enum/format guards fire at validate time.
The Terraform identity needs the following (least-privilege). Scope Athena resource ARNs to your workgroup / catalog names where governance allows.
⚠️ aws_athena_databaseis not a Glue control-plane call — it is an Athena query execution. Terraform issuesCREATE DATABASE/DROP DATABASEas a query run inside this workgroup, so the identity needs theathena:StartQueryExecutiontriad and S3 write access to the results bucket in addition to the underlyingglue:CreateDatabase. Granting onlyglue:CreateDatabasewill 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:CreateServiceLinkedRoleis required — Athena, Glue databases, and named queries do not depend on a service-linked role.
- S3 output bucket must already exist (hard prerequisite). The workgroup's
result_configuration.output_locationpoints ats3://<query_result_bucket>[/<prefix>]; there is no Athena-side bucket auto-creation. Wirequery_result_bucketfromterraform-aws-s3-bucket, which owns the SSE-KMS default encryption, the public-access block, and theaws:SecureTransportbucket policy. (Alternatively usemanaged_query_results_configurationfor Athena-managed result storage.) - KMS key policy. When
kms_key_arnis supplied, the key's policy must already grant Athena's runtime principal and the Terraform execution identitykms:GenerateDataKeyandkms:Decrypt— this module does not edit the key policy (owned byterraform-aws-kms). AwsDataCatalogalways exists. Every account has a defaultGLUEcatalog namedAwsDataCatalogthat Terraform does not manage;var.data_catalogsentries 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_DATABASELake Formation permission (separate from IAM) must also be granted to the Terraform identity, oraws_athena_databasecreation fails even with correct IAM. - Federated (
LAMBDA) catalogs require the backing Lambda connector function to already be deployed; supply its ARN in the catalog'sparameters— 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_queryminimum is10485760(10 MiB).
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
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"
}
}| 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) |
| 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 |
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" }
}Core / identity
name(required) — workgroup name (FORCE-NEW).query_result_bucket— results bucket name (required unlessmanaged_query_results_configurationis 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 (defaultnull⇒ SSE-S3).result_encryption_option—SSE_S3/SSE_KMS/CSE_KMS(default: SSE_KMS with a CMK, else SSE_S3).enforce_workgroup_configuration,enable_minimum_encryption_configuration— defaulttrue.publish_cloudwatch_metrics_enabled— defaulttrue;requester_pays_enabled— defaultfalse.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; needsiam:PassRole.identity_center_instance_arn— enables Identity Center (requiresexecution_role_arn).
Child collections
databases—map(object):name,comment,properties,force_destroy,bucket,acl_configuration,encryption_configuration. No tags.named_queries—map(object):database(required),query(required),name,description. No tags.data_catalogs—map(object):type(required),description(required),parameters(required),name,tags.
Universal
tags—map(string), merged onto the workgroup and data catalogs (default{}).
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
sensitiveoutputs are produced by this module.
- 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 noarnand notagsargument 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 noarnand notags. aws_athena_databasecreation is an Athena query execution, not a Glue control-plane call. Terraform issuesCREATE DATABASE...as a query run inside a workgroup; the module wiresworkgroup = aws_athena_workgroup.this.idso the DDL inherits this workgroup's enforced, encrypted result configuration rather than falling back to the accountprimary. This is why the identity needs theathena:StartQueryExecutiontriad + S3 write, not justglue:CreateDatabase.bucketandencryption_configurationonaws_athena_databasehave no read API. They are write-only; an imported database shows a permanent diff on them unless left null (the module default) or pinned withlifecycle { ignore_changes = [bucket] }. The module leaves them null by default so the workgroup's enforced result configuration governs encryption — avoiding the diff entirely.tags↔tags_all↔default_tags:var.tagsflows to the workgroup and (merged with per-catalog tags) to each data catalog;tags_allis the computed merge over providerdefault_tags(resource tags win on key conflict).default_tagsstays the caller's provider-block concern.enforce_workgroup_configurationgates the whole encryption default. With itfalse, a member can overrideresult_configuration(including encryption) from the client, silently defeating the module's SSE-KMS default — hence it defaultstrueand requires an explicit flip.query_results_s3_access_grants_configurationrequires Identity Center. AWS validates this cross-field dependency only at apply time; the module adds a plan-timevalidation{}guard requiringidentity_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. Withforce_destroy = false(default) the workgroup refuses to delete while it still owns named queries — do not hand-orderdepends_onagainst the children. - Region: regional service, no us-east-1 coupling; no
regionvariable (provider inheritance).
| Secure default | Enforced by | Opt-out |
|---|---|---|
| Result-set encryption ON | result_configuration.encryption_configuration — SSE_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.
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.
terraform init -backend=false→terraform validate→terraform 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 withPlan: 7 to add. - Negative tests: an invalid
data_catalogtypefails at validate;result_encryption_option = "SSE_KMS"with nokms_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.
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" }
AccessDeniedExceptioncreating a database — the identity hasglue:CreateDatabasebut is missing theathena:StartQueryExecution/GetQueryExecution/GetQueryResultstriad or S3 write on the results bucket.aws_athena_databaserunsCREATE DATABASEas an Athena query, not a Glue API call.- Database creation fails under Lake Formation — even with correct IAM, Lake Formation requires the separate
CREATE_DATABASEpermission granted to the Terraform identity. Grant it viaterraform-aws-lakeformation. - Plan fails: "result_encryption_option SSE_KMS/CSE_KMS requires a kms_key_arn" — supply
kms_key_arn(wireterraform-aws-kms) or useSSE_S3. Intended fail-fast guard, not a bug. - Plan fails: "needs one result destination" — provide
query_result_bucket(standard) ormanaged_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 addlifecycle { ignore_changes = [bucket] }. destroyfails: "workgroup is not empty" — the workgroup still owns named queries andforce_destroy = false. Let Terraform remove the children first (implicit viaworkgroup = this.id), or setforce_destroy = truewith a documented exception.- Tag drift on plan — a key exists in both provider
default_tagsand moduletags; resource tags win, so remove the duplicate from one side.tags_allshows 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.
- 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."