Skip to content

microsoftexpert/tf-mod-snowflake-task

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

❄️ Snowflake Task Terraform Module

Manages a single Snowflake task (snowflake_task) — SQL execution, schedule/DAG triggering, and compute source — targeting snowflakedb/snowflake ~> 2.17.

Terraform Snowflake Provider Module Version Module Type Resource Count Posture


🧩 Overview

  • ❄️ Creates and owns exactly one snowflake_task.this — no children, no for_each.
  • 🧱 Defaults to the safest posture Snowflake allows for a newly-applied task: started = false (suspended) — a task never silently starts running SQL against a live account on its first apply.
  • 🔌 Accepts a warehouse, a notification integration, and predecessor/finalizer tasks purely by reference (warehouse, error_integration, after, finalize) — every referenced object is owned elsewhere, most commonly a sibling module or another instance of this same module.
  • 🧬 Models the schedule/finalize/after trigger-mechanism exclusivity, the warehouse/user_task_managed_initial_warehouse_size compute-source exclusivity, the config JSON well-formedness and $$-wrapping constraint, and the string-typed allow_overlapping_execution field — every schema quirk the live provider exposes for this resource.
  • 🚫 Owns no privilege grants and no DAG-wide invariants across multiple task instances — a caller composing several instances into a DAG is responsible for that graph-level discipline (exactly one root, no orphaned finalizer, no cycles).

💡 Why it matters: an empty call to this module produces a task that is fully configured but suspended — nothing executes until a caller explicitly reviews the SQL, schedule, and DAG wiring and sets started = true. This is the same "the empty call must produce the safe resource" posture this suite's secure-by-default convention requires everywhere else in the > Snowflake estate, applied to the one Snowflake object type in this library that can execute arbitrary SQL on a timer.


❤️ 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

flowchart LR
 classDef moduleNode fill:#29B5E8,color:#ffffff,stroke:#1b7ea6,stroke-width:1px;
 classDef keystoneNode fill:#11567F,color:#ffffff,stroke:#0b3e5c,stroke-width:1px;
 classDef neutralNode fill:#eef1f5,color:#1a1a1a,stroke:#c7ccd4,stroke-width:1px;

 DB["tf-mod-snowflake-database"]:::neutralNode
 SCHEMA["tf-mod-snowflake-schema"]:::neutralNode
 WH["tf-mod-snowflake-warehouse"]:::neutralNode
 NI["tf-mod-snowflake-notification-integration"]:::neutralNode
 STREAM["tf-mod-snowflake-stream"]:::neutralNode
 SIBTASK["sibling task instance (DAG predecessor/root)"]:::neutralNode
 MOD["tf-mod-snowflake-task"]:::moduleNode
 RES["snowflake_task.this"]:::keystoneNode

 DB -->|"name output to database input"| MOD
 SCHEMA -->|"name output to schema input"| MOD
 WH -->|"fully_qualified_name output to warehouse input"| MOD
 NI -->|"fully_qualified_name output to error_integration input"| MOD
 STREAM -->|"fully_qualified_name interpolated into sql_statement"| MOD
 MOD -->|"creates"| RES
 SIBTASK -->|"fully_qualified_name output to after or finalize input"| MOD
 MOD -->|"fully_qualified_name output to after or finalize input"| SIBTASK
Loading

This module is standalone, but sits at the center of a rich sibling family: database/schema are sourced from tf-mod-snowflake-database's/tf-mod-snowflake-schema's name outputs; warehouse is consumed by reference from tf-mod-snowflake-warehouse's fully_qualified_name (mutually exclusive with user_task_managed_initial_warehouse_size, which needs no sibling since it targets serverless compute); error_integration is consumed from tf-mod-snowflake-notification-integration's fully_qualified_name; sql_statement typically interpolates a tf-mod-snowflake-stream instance's fully_qualified_name as a plain string (not a structured input/output wire — the same distinction tf-mod-snowflake-stream's own README draws for its on_table reference); and after/finalize are self-referential edges back to other instances of this same module, forming a task DAG. Validated via the Mermaid Chart MCP before embedding.


🧬 What this builds

flowchart LR
 classDef moduleNode fill:#29B5E8,color:#ffffff,stroke:#1b7ea6,stroke-width:1px;
 classDef keystoneNode fill:#11567F,color:#ffffff,stroke:#0b3e5c,stroke-width:1px;
 classDef neutralNode fill:#eef1f5,color:#1a1a1a,stroke:#c7ccd4,stroke-width:1px;

 subgraph INPUTS["Typed inputs"]
 direction TB
 I1["database / name / schema / sql_statement (required)"]:::neutralNode
 I2["started"]:::neutralNode
 I3["schedule / after / finalize (mutually exclusive trigger)"]:::neutralNode
 I4["warehouse / user_task_managed_initial_warehouse_size (mutually exclusive compute)"]:::neutralNode
 I5["serverless_task_max_statement_size / serverless_task_min_statement_size"]:::neutralNode
 I6["when / error_integration / config / allow_overlapping_execution"]:::neutralNode
 I7["suspend_task_after_num_failures / task_auto_retry_attempts / user_task_timeout_ms / user_task_minimum_trigger_interval_in_seconds"]:::neutralNode
 I8["target_completion_interval"]:::neutralNode
 I9["comment"]:::neutralNode
 end

 RES["snowflake_task.this"]:::keystoneNode

 subgraph OUTPUTS["Outputs"]
 direction TB
 O1["fully_qualified_name"]:::neutralNode
 O2["name"]:::neutralNode
 O3["id"]:::neutralNode
 end

 INPUTS -->|"rendered 1:1, no for_each (standalone)"| RES
 RES -->|"computed attributes"| OUTPUTS
Loading

Resource inventory: exactly one resource, snowflake_task.this. No for_each, no count, no child resources of any kind — this is a standalone module. Validated via the Mermaid Chart MCP before embedding.


✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
snowflakedb/snowflake provider ~> 2.17 (resolved to v2.18.0 at authoring time)
Provider block None — the caller's root module configures it, including any role alias

Schema notes that bite (verified against the live v2.18.0 schema):

  • schedule conflicts with both finalize and after (confirmed in the provider's own schema text); finalize/after mutual exclusivity is this module's own inference from Snowflake's task-graph model, not a directly-confirmed schema fact — see Architecture Notes.
  • warehouse and user_task_managed_initial_warehouse_size are mutually exclusive (confirmed directly in the schema text).
  • allow_overlapping_execution is a STRING field ("true" / "false" / "default"), not a boolean — this module exposes it as a real bool and converts it in main.tf.
  • config is wrapped in $$...$$ by the provider itself; the literal substring $$ is disallowed inside the value. config, show_output.config, and show_output.definition are not marked sensitive by the provider — do not put regulated data in them.
  • user_task_managed_initial_warehouse_size's own schema documentation is truncated in the live v2.18.0 docs (the live provider schema returned a literal unrendered "%s" for its legal-value list) — this module infers the same enum documented in full for serverless_task_max_statement_size/serverless_task_min_statement_size.
  • target_completion_interval is a nested block (hours/minutes/seconds), not the bare string the resource's Example Usage block misleadingly shows — the authoritative Schema section defines it as a block, and this module follows the Schema section.
  • The resource also exposes a generic Terraform Plugin Framework timeouts {} meta-argument (create/delete/read/update). This module deliberately does not surface it — the Snowflake universal tail in this library is comment only.

🔑 Required Snowflake Privileges

  • CREATE TASK on the schema (to create the keystone snowflake_task.this).
  • USAGE on the database and schema.
  • USAGE on the warehouse named in warehouse, if set (grant via tf-mod-snowflake-role-grant-bundle, not inside this module).
  • EXECUTE TASK at the account level — required to resume (started = true) or otherwise execute a task, and transitively for any task this task lists as a predecessor via after.
  • OWNERSHIP (or the ability to acquire it) on the task for most lifecycle operations beyond initial creation.

⚠️ Flagged for human/security review: Snowflake's account-privilege documentation (not the Terraform provider's schema docs, which this authoring pass is grounded in and which do not model privilege requirements at all) additionally documents an EXECUTE MANAGED TASK account-level privilege specifically for running serverless tasks (no warehouse set). This module's authoring process could not independently verify that requirement against ground truth — confirm the exact privilege set for serverless tasks against Snowflake's current account-privilege documentation, or a live account, before assuming EXECUTE TASK alone is sufficient. See SCOPE.md for the full callout.

Snowflake Prerequisites

  • No preview feature flag required — snowflake_task is a GA ("Stable") resource as of provider v2.18.0.
  • The target database/schema must already exist before this module applies.
  • Any warehouse or error_integration referenced must already exist before this module applies.
  • Any task referenced in after or finalize must already exist (or be created in the same apply, with Terraform's dependency graph ordering creation via the fully_qualified_name reference) before this task can be started.

📁 Module Structure

tf-mod-snowflake-task/
├── providers.tf # required_providers + required_version — no provider {} block
├── variables.tf # deeply-typed variable schema, secure defaults, universal tail (comment)
├── main.tf # the total renderer — one keystone `this`, no children
├── outputs.tf # fully_qualified_name, then name, then id
├── README.md # this file
├── SCOPE.md # lightweight standalone spec (design intent, privileges, prerequisites, emits, gotchas)
└── examples/ # runnable example call sites

⚙️ Quick Start

module "nightly_refresh" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = module.reporting_db.name
  schema        = module.reporting_schema.name
  name          = "TASK_NIGHTLY_REFRESH"
  warehouse     = module.wh_etl.fully_qualified_name
  sql_statement = "CALL REFRESH_REPORTING_TABLES"

  schedule = { using_cron = "0 2 * * *" }
}

The caller's root module configures the snowflake provider (account identifier, authentication, and any role alias) — this module never declares a provider {} block or a credential-shaped variable. The empty/minimal call produces started = false (suspended) — the caller must explicitly set started = true after reviewing the task.


🔌 Cross-Module Contract

Consumes

Input Type Source module
database string (name) tf-mod-snowflake-database
schema string (name) tf-mod-snowflake-schema
warehouse string (optional, fully_qualified_name) tf-mod-snowflake-warehouse
error_integration string (optional, fully_qualified_name) tf-mod-snowflake-notification-integration
after / finalize set(string) / string (optional, fully_qualified_name) Another instance of this same module
sql_statement (typically) string (plain interpolation, not a structured wire) tf-mod-snowflake-stream

Emits

Output Description Consumed by
fully_qualified_name The task's fully-qualified identifier Another instance of this same module (after/finalize)
name The task's bare name Documentation / display only
id The task's Terraform resource id Import / drift-detection tooling

📚 Example Library

1 · Minimal call — suspended by secure default
module "task_minimal" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_MINIMAL"
  warehouse     = "WH_ETL"
  sql_statement = "SELECT 1"
}

⚠️ started defaults to false (suspended) — this task will not run on any schedule until the caller explicitly sets started = true, after reviewing the SQL, schedule, and any DAG wiring. A newly-applied task must never silently start executing SQL against a live account.

2 · Standalone scheduled task — minutes interval
module "task_every_five_minutes" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_REFRESH_5MIN"
  warehouse     = "WH_ETL"
  sql_statement = "CALL REFRESH_STAGING"
  started       = true

  schedule = { minutes = 5 }
}

💡 schedule is a single nested object; exactly one of hours, minutes, seconds, or using_cron must be set — enforced at terraform validate time.

3 · Standalone scheduled task — cron expression
module "task_nightly_cron" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_NIGHTLY_CRON"
  warehouse     = "WH_ETL"
  sql_statement = "CALL NIGHTLY_ROLLUP"
  started       = true

  schedule = { using_cron = "0 2 * * *" }
}

ℹ️ using_cron supports a subset of standard cron syntax per the provider's own documentation; this module validates only that it is the sole non-null sub-field of schedule, not the cron expression's own syntax.

4 · Two-task DAG — root task with schedule, child task with after
module "task_dag_root" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_DAG_ROOT"
  warehouse     = "WH_ETL"
  sql_statement = "CALL LOAD_RAW"
  started       = true

  schedule = { hours = 1 }
}

module "task_dag_child" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_DAG_CHILD"
  warehouse     = "WH_ETL"
  sql_statement = "CALL TRANSFORM_RAW"
  started       = true

  after = [module.task_dag_root.fully_qualified_name]
}

⚠️ schedule and after are mutually exclusive, enforced at terraform validate time — the root task carries the schedule; the child task carries after and must NOT also set schedule. This module does not verify DAG-wide invariants (e.g. that task_dag_root really is the graph's only root) — that discipline is the caller's responsibility across however many instances are composed.

5 · Finalizer task
module "task_dag_finalizer" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_DAG_FINALIZER"
  warehouse     = "WH_ETL"
  sql_statement = "CALL CLEANUP_AND_NOTIFY"
  started       = true

  finalize = module.task_dag_root.fully_qualified_name
}

⚠️ finalize is mutually exclusive with both schedule and after — a finalizer task runs only after every other task in the graph rooted at task_dag_root completes.

6 · Serverless task — Snowflake-managed compute
module "task_serverless" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_SERVERLESS"
  sql_statement = "CALL LIGHTWEIGHT_REFRESH"
  started       = true

  schedule                                 = { minutes = 15 }
  user_task_managed_initial_warehouse_size = "XSMALL"
}

⚠️ warehouse and user_task_managed_initial_warehouse_size are mutually exclusive, enforced at terraform validate time — omit warehouse entirely to use Snowflake-managed serverless compute, optionally hinting an initial size for the first few runs.

7 · Serverless task with statement-size bounds and a target completion interval
module "task_serverless_bounded" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_SERVERLESS_BOUNDED"
  sql_statement = "CALL VARIABLE_LOAD_JOB"
  started       = true

  schedule                           = { minutes = 30 }
  serverless_task_min_statement_size = "XSMALL"
  serverless_task_max_statement_size = "LARGE"
  target_completion_interval         = { minutes = 20 }
}

ℹ️ serverless_task_max_statement_size/serverless_task_min_statement_size and target_completion_interval are task-specific fields this module's scaffold omitted from its original field list — added during authoring since they are genuinely task-specific rather than generic session parameters. Not cross-validated against each other by this module; an inconsistent min/max pair surfaces as a Snowflake API error at apply time.

8 · Task gated by a boolean SQL condition
module "task_conditional" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_CONDITIONAL_REFRESH"
  warehouse     = "WH_ETL"
  sql_statement = "CALL PROCESS_NEW_ROWS"
  started       = true

  schedule = { minutes = 10 }
  when     = "SYSTEM$STREAM_HAS_DATA('ANALYTICS.STAGING.RAW_EVENTS_STREAM')"
}

💡 If when evaluates false at a scheduled occurrence, the task skips that run — and any task naming this one as a predecessor via after also doesn't run.

9 · Task with a shared JSON config payload
module "task_with_config" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_ML_SCORING"
  warehouse     = "WH_ETL"
  sql_statement = "CALL RUN_SCORING_JOB"
  started       = true

  schedule = { hours = 6 }
  config   = jsonencode({ output_dir = "/temp/", learning_rate = 0.1 })
}

🔒 config is validated as well-formed JSON and as not containing the literal substring $$ at terraform validate time — but it is not marked sensitive by the provider. Do not put member/borrower PII or other regulated data in this value.

10 · Task with error-notification integration
module "task_with_error_notifications" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database          = "ANALYTICS"
  schema            = "STAGING"
  name              = "TASK_CRITICAL_LOAD"
  warehouse         = "WH_ETL"
  sql_statement     = "CALL CRITICAL_LOAD"
  started           = true
  error_integration = "NI_TASK_FAILURES"

  schedule = { hours = 1 }
}

💡 error_integration is typically wired from a sibling tf-mod-snowflake-notification- integration instance's fully_qualified_name output — see the end-to-end composition below.

11 · Task allowing overlapping executions
module "task_overlap_allowed" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database                    = "ANALYTICS"
  schema                      = "STAGING"
  name                        = "TASK_LONG_RUNNING"
  warehouse                   = "WH_ETL"
  sql_statement               = "CALL LONG_RUNNING_JOB"
  started                     = true
  allow_overlapping_execution = true

  schedule = { minutes = 5 }
}

⚠️ allow_overlapping_execution defaults to false in this module (matching Snowflake's own effective account default) — a caller must opt in explicitly, as shown here, to permit multiple concurrent runs of this task's DAG.

12 · Task with retry, timeout, and auto-suspend tuning
module "task_tuned" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_FLAKY_UPSTREAM"
  warehouse     = "WH_ETL"
  sql_statement = "CALL SYNC_FROM_FLAKY_UPSTREAM"
  started       = true

  schedule                                      = { minutes = 15 }
  suspend_task_after_num_failures               = 5
  task_auto_retry_attempts                      = 3
  user_task_timeout_ms                          = 1800000
  user_task_minimum_trigger_interval_in_seconds = 60
}

ℹ️ suspend_task_after_num_failures defaults to 0 (the provider's own documented default — never auto-suspend). task_auto_retry_attempts, user_task_timeout_ms, and user_task_minimum_trigger_interval_in_seconds default to null — the live schema documents no explicit computed default for any of the three, so null defers to Snowflake's own account/object-level default rather than this module guessing a value.

13 · Hours-based schedule with a comment
module "task_hourly_with_comment" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_HOURLY_SYNC"
  warehouse     = "WH_ETL"
  sql_statement = "CALL HOURLY_SYNC"
  started       = true
  comment       = "Hourly sync of upstream reference data"

  schedule = { hours = 1 }
}

ℹ️ comment is the only universal-tail variable this provider carries — no tags, no timeouts. This example also demonstrates the third schedule interval variant (hours), alongside the minutes and using_cron variants shown earlier.

14 · Rejecting a malformed call (illustrative — fails terraform validate)
module "task_invalid_example" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database      = "ANALYTICS"
  schema        = "STAGING"
  name          = "TASK_INVALID"
  warehouse     = "WH_ETL"
  sql_statement = "SELECT 1"

  schedule = { minutes = 5 }
  after    = [module.task_dag_root.fully_qualified_name] # invalid: conflicts with schedule
}

⚠️ This call fails at terraform validate with an actionable error naming schedule and after — never as an opaque Snowflake API error at apply time. Included here to demonstrate the type-as-contract philosophy, not as a call site to actually use.

15 · 🏗️ End-to-end composition: stream, warehouse, and notification integration wired into one task
module "raw_events_stream" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-stream.git?ref=v1.0.0"

  database = "ANALYTICS"
  schema   = "STAGING"
  name     = "RAW_EVENTS_STREAM"
  on_table = "ANALYTICS.STAGING.RAW_EVENTS"
}

module "wh_etl" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-warehouse.git?ref=v1.0.0"

  name           = "WH_ETL"
  warehouse_size = "SMALL"
}

module "ni_task_failures" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-notification-integration.git?ref=v1.0.0"

  name = "NI_TASK_FAILURES"
  #... notification-provider-specific configuration
}

module "task_process_stream" {
  source = "git::https://github.com/microsoftexpert/tf-mod-snowflake-task.git?ref=v1.0.0"

  database  = "ANALYTICS"
  schema    = "STAGING"
  name      = "TASK_PROCESS_STREAM"
  warehouse = module.wh_etl.fully_qualified_name

  # ℹ️ sql_statement is a plain string interpolation of the stream's fully_qualified_name — not
  # a structured input/output wire between the two modules, the same distinction
  # tf-mod-snowflake-stream's own README draws for its on_table reference.
  sql_statement     = "INSERT INTO ANALYTICS.STAGING.PROCESSED_EVENTS SELECT * FROM ${module.raw_events_stream.fully_qualified_name}"
  error_integration = module.ni_task_failures.fully_qualified_name
  when              = "SYSTEM$STREAM_HAS_DATA('${module.raw_events_stream.fully_qualified_name}')"
  started           = true

  schedule = { minutes = 5 }
}

💡 This wires three real sibling relationships: tf-mod-snowflake-stream's fully_qualified_name interpolated into this task's sql_statement and when (plain string interpolation, not a structured wire), tf-mod-snowflake-warehouse's fully_qualified_name into this task's warehouse input, and tf-mod-snowflake-notification-integration's fully_qualified_name into this task's error_integration input.


📥 Inputs

Grouped summary: identity (database, name, schema, sql_statement), lifecycle (started), trigger mechanism (schedule, after, finalize — mutually exclusive), compute source (warehouse, user_task_managed_initial_warehouse_size — mutually exclusive; serverless_task_max_statement_size, serverless_task_min_statement_size), execution tuning (when, error_integration, allow_overlapping_execution, config, suspend_task_after_num_failures, task_auto_retry_attempts, user_task_timeout_ms, user_task_minimum_trigger_interval_in_seconds, target_completion_interval), and the universal tail (comment).

Full variable schema
Variable Type Default Notes
database string — (required) Bare identifier; avoid |, ., ".
name string — (required) Bare identifier; avoid |, ., ".
schema string — (required) Bare identifier; avoid |, ., ".
sql_statement string — (required) Non-empty; Snowflake validates the SQL itself.
started bool false House secure default — suspended until reviewed.
after set(string) [] Predecessor task fully_qualified_names; mutually exclusive with schedule/finalize.
schedule object({hours,minutes,seconds,using_cron}) null Exactly one sub-field when set; mutually exclusive with after/finalize.
finalize string null Root task fully_qualified_name; mutually exclusive with schedule/after.
warehouse string null fully_qualified_name; mutually exclusive with user_task_managed_initial_warehouse_size.
when string null Boolean SQL gating expression.
error_integration string null fully_qualified_name of a notification integration.
allow_overlapping_execution bool false Converted to provider string "true"/"false" in main.tf.
config string null Must be valid JSON and must not contain $$.
comment string null Universal tail.
suspend_task_after_num_failures number 0 Provider's own documented default.
task_auto_retry_attempts number null No documented provider default; defers to account default.
user_task_managed_initial_warehouse_size string null Size enum inferred from sibling fields (see gotchas).
serverless_task_max_statement_size string null Full size enum confirmed in schema.
serverless_task_min_statement_size string null Full size enum confirmed in schema.
user_task_timeout_ms number null No documented provider default; defers to account default.
user_task_minimum_trigger_interval_in_seconds number null No documented provider default; defers to account default.
target_completion_interval object({hours,minutes,seconds}) null Exactly one sub-field when set.

🧾 Outputs

Output Description Sensitive / Conditional
fully_qualified_name The task's fully-qualified identifier No
name The task's bare name No
id The task's Terraform resource id No

🧠 Architecture Notes

  • started = false secure default: a deliberate house choice (the provider has no computed default of its own for this required field) per this suite's "Object dropping protection"/ cautious-default philosophy — a newly-applied task must never silently start executing SQL against a live account on its first apply. The caller must set started = true explicitly, and only after reviewing the task's SQL, schedule, and DAG wiring.
  • The schedule/finalize/after mutual-exclusivity rule is the single most important validation in this module. Confirmed directly against the live provider schema text for schedule: "(Conflicts with finalize and after;...)" — a fact from the provider's own docs. The schema text does not separately confirm that finalize and after conflict with each other; this module enforces that third leg anyway, as a reasonable inference from Snowflake's task-graph model (a finalizer task is a distinct role, not composable with predecessor linkage) — not a directly-confirmed schema fact. See variables.tf's comments on the schedule/finalize validation blocks for the precise fact-vs-inference boundary, and re-verify against a live plan if a use case genuinely requires combining after and finalize.
  • warehouse/user_task_managed_initial_warehouse_size mutual exclusivity: confirmed directly in the schema text (warehouse's own description states "Conflicts with user_task_managed_initial_warehouse_size"). Omitting both is valid — Snowflake-managed serverless compute with no initial size hint.
  • allow_overlapping_execution bool-to-string conversion: the underlying field is a STRING ("true" / "false" / "default"), not a boolean. This module models it as a real bool, defaulting false, and never surfaces "default" as a caller-facing option — main.tf performs the conversion inline: var.allow_overlapping_execution ? "true": "false".
  • Deliberate v1.0.0 scope decision — generic session parameters omitted: the live snowflake_task resource exposes roughly 60 pass-through Snowflake session-parameter arguments (autocommit, timezone, statement_timeout_in_seconds, query_tag, every client_*/timestamp_*/date_*/time_* parameter, and dozens more). This module does not model any of them individually, and does not add a generic session_parameters escape hatch either — the gap is left explicit rather than papered over. A future minor version could add either the individually-typed high-value parameters or a narrowly-scoped escape hatch; that decision is deliberately deferred, not an oversight.
  • after/finalize are self-referential: both are expected to be another instance of this same module's fully_qualified_name output. This module does not manage DAG-wide concerns (ensuring exactly one root task per graph, detecting orphaned finalizers, detecting cycles in the after chain) — that discipline is the caller's responsibility across however many module instances they compose.
  • for_each note: not applicable — this is a standalone module with no child/relationship records, so there is no for_each key-stability concern to document here.

🧱 Design Principles

Concern Secure default Opt-out (caller must be explicit)
Task lifecycle state started = false — suspended until reviewed Caller sets started = true explicitly
Overlapping executions false — one DAG instance at a time Caller sets allow_overlapping_execution = true explicitly
Future grants / grant scope Not applicable — this module owns no grants N/A
Object comment null — no default disclosure of intent Caller sets comment explicitly
Suspend-after-failures 0 — provider's own documented default (never auto-suspend) Caller sets an explicit non-zero value
Compute source null/null — neither warehouse nor user_task_managed_initial_warehouse_size set implicitly Caller supplies one explicitly (or neither, for unbounded serverless)

🚀 Runbook

cd C:\GitHubCode\newsnowflakemodules\tf-mod-snowflake-task
terraform init -backend=false
terraform validate
terraform fmt -check

Pin ?ref=v1.0.0 in every call site — never a branch. This is a plan-only authoring pipeline; a human runs terraform apply from a governed CI pipeline.


🧪 Testing

  • terraform init -backend=false / terraform validate / terraform fmt -check cover: type correctness (every object/scalar schema matches the live provider schema), HCL syntax, and every closed-enum / cross-variable validation {} block (schedule/finalize/after exclusivity, warehouse/user_task_managed_initial_warehouse_size exclusivity, config JSON well-formedness) catching a malformed input before any API call.
  • What these three commands do not cover: privilege sufficiency (e.g. whether the executing role actually holds CREATE TASK, EXECUTE TASK, or the flagged-for-review EXECUTE MANAGED TASK), object-existence dependencies (whether a referenced warehouse, error_integration, or predecessor/finalizer task actually exists), DAG-wide invariants across multiple task instances, and DESC/SHOW-output-driven permadiffs. Only a real terraform plan against a live Snowflake account exercises those — and per this library's plan-only posture, that step belongs to a human running it from governed CI.

💬 Example Output

$ terraform output

fully_qualified_name = "\"ANALYTICS\".\"STAGING\".\"TASK_NIGHTLY_REFRESH\""
name = "TASK_NIGHTLY_REFRESH"
id = "ANALYTICS|STAGING|TASK_NIGHTLY_REFRESH"

🔍 Troubleshooting

Symptom Cause Fix
terraform validate fails on schedule/finalize/after More than one of the three trigger mechanisms is set Set exactly one: schedule for a standalone/root task, after for a child task, finalize for a finalizer task.
terraform validate fails on warehouse/user_task_managed_initial_warehouse_size Both are set Choose one — a named warehouse, or a serverless size hint — or omit both for unbounded serverless compute.
terraform validate fails on config Value is not valid JSON, or contains the literal substring $$ Fix the JSON, or remove the $$ — the provider wraps the value in $$...$$ itself.
Task never runs even though started = true schedule and after are both unset, and this is not a Triggered Task Set a schedule for a standalone/root task, or trigger it manually via the provider's snowflake_execute resource.
Apply fails resuming the task with an authorization error Executing role lacks EXECUTE TASK (or, for serverless tasks, potentially EXECUTE MANAGED TASK — see the flagged privilege-review note in §7/SCOPE.md) Re-verify the executing role's privileges against Snowflake's current account-privilege documentation or a live account.
Unexpected diff on comment/config every plan after import Import didn't populate an optional field in the generated config Set the field explicitly to the live value shown in terraform show after import (a cross-cutting behavior noted for several resources in this provider, not confirmed specifically for snowflake_task).
Child task never runs after its predecessor completes after references a task by bare name instead of fully_qualified_name, or the predecessor task itself is suspended (started = false) Wire module.task_x.fully_qualified_name into after, and confirm the predecessor's started = true.

🔗 Related Docs

  • Provider resource docs: snowflake_task
  • Sibling modules: tf-mod-snowflake-database, tf-mod-snowflake-schema, tf-mod-snowflake-warehouse, tf-mod-snowflake-notification-integration, tf-mod-snowflake-stream
  • This module's SCOPE.md

Packages

 
 
 

Contributors

Languages