Manages a single Snowflake external stage targeting S3-API-compatible object storage that is not Amazon S3 itself β on-premises or MinIO-style endpoints (
snowflake_stage_external_s3_compatible) β targetingsnowflakedb/snowflake ~> 2.17.
- βοΈ Creates and owns exactly one
snowflake_stage_external_s3_compatible.thisβ no children, no grants, noCOPY INTO/loading-pipeline logic. - π§± Targets S3-API-compatible object storage that is not Amazon S3 β on-premises or
MinIO-style endpoints. Distinct from
terraform-snowflake-stage-external-s3, which targets Amazon S3 itself. - π Accepts the parent database and schema purely by reference
(
var.database/var.schema) β both owned by siblingterraform-snowflake-database/terraform-snowflake-schemainstances. - π
credentials(an inline AWS-style access-key/secret-key pair) is the only authentication mechanism this resource exposes β there is nostorage_integrationreference path at all, unlike this module's S3/Azure/GCS siblings. - π« Has no
encryption,use_privatelink_endpoint, oraws_access_point_arnfields β all three confirmed absent from the live schema, not accidentally dropped.
π‘ Why it matters: an empty call to this module (no
credentials, nodirectory, nofile_format) produces the smallest possible stage definition β no directory table, no implied file format, no credentials sent to Snowflake β leaving every additional behavior an explicit, reviewable opt-in, consistent with this suite's "the empty call must produce the safe resource" philosophy.
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
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["terraform-snowflake-database"]:::neutralNode
SCHEMA["terraform-snowflake-schema"]:::neutralNode
MOD["terraform-snowflake-stage-external-s3-compatible"]:::moduleNode
RES["snowflake_stage_external_s3_compatible.this"]:::keystoneNode
DB -->|"fully_qualified_name as database"| MOD
SCHEMA -->|"fully_qualified_name as schema"| MOD
MOD -->|"creates"| RES
This module consumes terraform-snowflake-database's and terraform-snowflake-schema's
fully_qualified_name outputs as its required database/schema inputs β the same
cross-reference convention every schema-scoped object module in this library uses. There is
deliberately no terraform-snowflake-storage-integration-aws edge in this diagram β this resource
has no storage_integration argument, so unlike its terraform-snowflake-stage-external-s3 sibling,
there is nothing to wire from a storage-integration module here. No downstream consumer module
exists yet in this catalog for this module's own fully_qualified_name output. Validated via the
Mermaid Chart MCP before embedding.
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 (required)"]:::neutralNode
I2["url / endpoint (required)"]:::neutralNode
I3["credentials (optional, sensitive)"]:::neutralNode
I4["directory (optional)"]:::neutralNode
I5["file_format (optional)"]:::neutralNode
I6["comment (optional)"]:::neutralNode
end
RES["snowflake_stage_external_s3_compatible.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
Resource inventory: exactly one resource, snowflake_stage_external_s3_compatible.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.
| 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):
- No
storage_integration,encryption,use_privatelink_endpoint, oraws_access_point_arnfields exist on this resource. Confirmed absent, not an oversight βcredentialsis the only authentication mechanism available. credentialsis not read back fromDESCRIBE/SHOW STAGEoutput. The provider's own docs state this explicitly β rotating the S3-compatible endpoint's access key outside Terraform produces no diff on the nextplan.directory.auto_refreshforce-new / alter-on-unset asymmetry. Settingauto_refreshto a new explicit value forces stage replacement; unsetting it instead alters the wholedirectoryfield using the currentenablevalue β not symmetric operations.refresh_on_createis create-only. Changes to it after the stage exists are silently ignored by the provider (not force-new, not applied).- This resource is meant only for S3-compatible endpoints, not Amazon S3 itself. The
provider's own docs explicitly say not to use
s3://URLs with this resource β useterraform-snowflake-stage-external-s3instead. This module'surlvalidation enforces thes3compat://scheme atterraform validatetime. - Persistent diffs after
terraform importmay require enabling theIMPORT_BOOLEAN_DEFAULTexperimental feature and reimporting β see the provider'sMIGRATION_GUIDE.md. - Several
file_formatsub-fields (e.g.csv.trim_space,json.multi_line) are STRING tri-state sentinels ("true"/"false"/ the internal"default"value) on the provider resource, not native booleans β this module models them as realboolvariables and converts internally.
βΉοΈ The resource's own Terraform docs page describes argument/attribute shape only and carries no dedicated "required privileges" subsection β the privileges below are sourced from Snowflake's
CREATE STAGESQL reference, not the Terraform provider docs. Treat as a least-privilege starting point and verify against the executing role's actual grant set before relying on this list as exhaustive.
CREATE STAGEon the target schema (to create the keystonesnowflake_stage_external_s3_compatible.this) β the executing role must own the schema (OWNERSHIP) or hold this privilege explicitly granted on the schema.USAGEon the parent database and schema.- No additional Snowflake privilege is required to populate
credentialsβ it is stage-object metadata, not a Snowflake principal grant. Any access control on the S3-compatible endpoint itself (bucket policy, IAM-equivalent) is out of band on the storage provider's side.
- No preview feature flag required β
snowflake_stage_external_s3_compatibleis a GA ("Stable") resource as of provider v2.18.0. - The parent database and schema referenced by
var.database/var.schemamust already exist. - The S3-compatible endpoint itself must already exist and be network-reachable from Snowflake
before the stage is usable for
COPY INTO/ directory-table refresh β this module cannot validate reachability atterraform validate/plantime. - If
var.file_format.format_nameis set, the referencedsnowflake_file_formatobject must already exist. - Temporary stages are not applicable to this resource (per the provider's own docs).
terraform-snowflake-stage-external-s3-compatible/
βββ 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
module "stage_minio_raw" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_MINIO_RAW"
url = "s3compat://raw-bucket/landing/"
endpoint = "minio.internal.example.com"
}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 at the provider level. The call above produces the smallest possible stage: no
credentials sent (an anonymous/public-read bucket), no directory table, no implied file format.
Consumes
| Input | Type | Source module |
|---|---|---|
database |
string (required, fully_qualified_name) |
terraform-snowflake-database |
schema |
string (required, fully_qualified_name) |
terraform-snowflake-schema |
file_format.format_name |
string (optional, fully_qualified_name) |
A snowflake_file_format object (no dedicated module exists yet in this catalog) |
Emits
| Output | Description | Consumed by |
|---|---|---|
fully_qualified_name |
The stage's fully-qualified identifier | No cataloged consumer yet |
name |
The stage's bare name | Documentation / display only |
id |
The stage's Terraform resource id | Import / drift-detection tooling |
1 Β· Minimal / empty call β anonymous, public-read bucket
module "stage_minimal" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_MINIMAL"
url = "s3compat://public-bucket/landing/"
endpoint = "s3.my-provider.com"
}π‘
credentialsis genuinely optional at the provider level β an anonymous/public-read S3-compatible bucket needs no credentials at all. This is the smallest legal call.
2 Β· Representative on-premises / MinIO-style endpoint
module "stage_minio_onprem" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_MINIO_ONPREM"
url = "s3compat://onprem-bucket/exports/"
endpoint = "minio.corp.internal:9000"
}βΉοΈ This resource targets S3-API-compatible object storage that is not Amazon S3 itself β an on-premises or MinIO-style endpoint, addressed with a custom hostname (and optionally a port, as shown here). For Amazon S3 itself, use
terraform-snowflake-stage-external-s3instead β that module also supports astorage_integrationreference, which this resource has no equivalent for.
3 Β· Stage with inline credentials
module "stage_with_credentials" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_MINIO_AUTHENTICATED"
url = "s3compat://private-bucket/landing/"
endpoint = "minio.corp.internal:9000"
credentials = {
aws_key_id = var.minio_access_key_id # sourced from a secret store, never a literal
aws_secret_key = var.minio_secret_access_key # sourced from a secret store, never a literal
}
}π
aws_key_id/aws_secret_keyare long-lived secrets sourced from a secret store upstream (e.g. Azure Key Vault, a CI secret store) and injected at apply time β never a literal in.tfvarsor committed configuration. This is the only authentication mechanism this resource exposes; there is nostorage_integrationreference-based alternative to offer here, unlike this module's S3/Azure/GCS siblings.
4 Β· Rejecting one-sided credentials (illustrative β fails terraform validate)
module "stage_invalid_credentials" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_INVALID"
url = "s3compat://private-bucket/landing/"
endpoint = "minio.corp.internal:9000"
credentials = {
aws_key_id = "AKIAEXAMPLE" # aws_secret_key omitted β invalid
}
}
β οΈ This call fails atterraform validatewith a type error (aws_secret_keyis a required attribute of thecredentialsobject) before any API call βaws_key_idandaws_secret_keyare both required, non-optional attributes on the object type, so the type system itself rejects a one-sided credentials object. No separatevalidation {}block was needed for this constraint. Included here to demonstrate the type-as-contract philosophy, not as a call site to actually use.
5 Β· Rejecting a plain s3:// URL (illustrative β fails terraform validate)
module "stage_wrong_module" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_WRONG_SCHEME"
url = "s3://my-aws-bucket/path/" # invalid for this module
endpoint = "s3.amazonaws.com"
}
β οΈ This call fails atterraform validateβurlmust use thes3compat://scheme. This module targets S3-compatible (non-AWS) endpoints only. For a plains3://URL against Amazon S3 itself, useterraform-snowflake-stage-external-s3instead β the mirror-image footgun of that module's owns3compat://-rejection callout.
6 Β· Stage with a directory table enabled
module "stage_with_directory" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_DIRECTORY_ENABLED"
url = "s3compat://private-bucket/landing/"
endpoint = "minio.corp.internal:9000"
directory = {
enable = true
refresh_on_create = true
auto_refresh = false
}
}
β οΈ Changingdirectory.auto_refreshto a new explicit value forces this stage to be recreated; removing it from configuration instead alters the wholedirectoryblock using the currentenablevalue β these are not symmetric operations.refresh_on_createonly takes effect at creation time; later changes to it are silently ignored by the provider.
7 Β· file_format referencing a pre-existing named file format (house-preferred)
module "stage_named_format" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_NAMED_FORMAT"
url = "s3compat://private-bucket/landing/"
endpoint = "minio.corp.internal:9000"
file_format = {
format_name = "\"RAW\".\"LANDING\".\"FF_CSV_STANDARD\""
}
}π‘
format_name(a reference to a pre-existing, independently governedsnowflake_file_formatobject) is the house-preferred option β reusable across every stage that needs the same format, versus repeating an inline definition per stage.
8 Β· file_format with an inline CSV block (advanced/secondary)
module "stage_inline_csv" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_INLINE_CSV"
url = "s3compat://private-bucket/landing/"
endpoint = "minio.corp.internal:9000"
file_format = {
csv = {
compression = "GZIP"
field_delimiter = "|"
skip_header = 1
empty_field_as_null = true
encoding = "UTF8"
}
}
}
β οΈ Inlinefile_formatblocks (csv/json/avro/orc/parquet/xml) are the advanced/secondary option, only for a one-off, stage-specific format β preferformat_name(Example 7) for anything reused across multiple stages.file_formatmay set at most one offormat_name/avro/csv/json/orc/parquet/xmlβ enforced atterraform validatetime.
9 Β· file_format with an inline JSON block
module "stage_inline_json" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_INLINE_JSON"
url = "s3compat://private-bucket/exports/"
endpoint = "minio.corp.internal:9000"
file_format = {
json = {
compression = "AUTO"
strip_outer_array = true
ignore_utf8_errors = false
}
}
}βΉοΈ Every boolean-shaped
file_formatsub-field (e.g.strip_outer_array,ignore_utf8_errors) is modeled as a realbooleven though the underlying provider field is a STRING tri-state sentinel ("true"/"false"/internal"default") βmain.tfconverts this centrally.
10 Β· Stage with an explicit comment
module "stage_documented" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_VENDOR_EXPORTS"
url = "s3compat://vendor-bucket/exports/"
endpoint = "s3.vendor-provider.com"
comment = "Nightly vendor export drop zone β MinIO-compatible endpoint"
}βΉοΈ
commentis the only universal-tail variable this provider carries β notags, notimeouts.
11 Β· No encryption block to configure (unlike S3/Azure/GCS siblings)
module "stage_no_encryption_option" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_NO_ENCRYPTION_FIELD"
url = "s3compat://vendor-bucket/exports/"
endpoint = "s3.vendor-provider.com"
}βΉοΈ This module has no
encryptionblock to configure β confirmed absent from the live schema, unlike its S3/Azure/GCS siblings. Encryption for an S3-compatible endpoint is a property of the target storage system itself, entirely outside this resource's schema; there is nothing to set here even if the underlying MinIO/on-prem storage supports server-side encryption.
12 Β· Rejecting a negative skip_header (illustrative β fails terraform validate)
module "stage_invalid_skip_header" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_INVALID_SKIP_HEADER"
url = "s3compat://private-bucket/landing/"
endpoint = "minio.corp.internal:9000"
file_format = {
csv = {
skip_header = -1 # invalid β the provider's own internal "unset" sentinel, not caller-settable
}
}
}
β οΈ -1is the provider's internal "unset, defer to Snowflake default" sentinel forskip_headerβ the same pattern asterraform-snowflake-warehouse'sauto_suspend. This module'svalidation {}block rejects a literal-1atterraform validate; passnull(or omit the field) instead.
13 Β· Fully configured production stage β directory table + named format + comment
module "stage_production" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = "RAW"
schema = "LANDING"
name = "STAGE_PARTNER_FEED"
url = "s3compat://partner-feed-bucket/daily/"
endpoint = "minio.partner-dc.example.com:9000"
credentials = {
aws_key_id = var.partner_feed_access_key_id
aws_secret_key = var.partner_feed_secret_access_key
}
directory = {
enable = true
auto_refresh = true
}
file_format = {
format_name = "\"RAW\".\"LANDING\".\"FF_CSV_STANDARD\""
}
comment = "Daily partner feed drop zone (MinIO-compatible endpoint)"
}π‘ Combines the module's full surface: credentials sourced from a secret store, a directory table with
auto_refreshexplicitly set (reviewed for the force-new implication), a house-preferred named file format reference, and a descriptive comment.
14 Β· ποΈ End-to-end composition: database and schema out to this module's inputs
module "db_raw" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-database.git?ref=v1.0.0"
name = "RAW"
}
module "schema_landing" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-schema.git?ref=v1.0.0"
database = module.db_raw.fully_qualified_name
name = "LANDING"
}
module "stage_minio_feed" {
source = "git::https://github.com/microsoftexpert/terraform-snowflake-stage-external-s3-compatible.git?ref=v1.0.0"
database = module.db_raw.fully_qualified_name
schema = module.schema_landing.fully_qualified_name
name = "STAGE_MINIO_PARTNER_FEED"
url = "s3compat://partner-feed-bucket/daily/"
endpoint = "minio.partner-dc.example.com:9000"
credentials = {
aws_key_id = var.partner_feed_access_key_id
aws_secret_key = var.partner_feed_secret_access_key
}
comment = "Daily partner feed drop zone"
}π‘ This wires two real sibling relationships:
terraform-snowflake-database'sfully_qualified_nameinto this module'sdatabaseinput, andterraform-snowflake-schema'sfully_qualified_name(itself built from the database's ownfully_qualified_name) into this module'sschemainput. Nostorage_integrationmodule is wired here β this resource has none to consume.
Grouped summary: identity (database, name, schema), location (url, endpoint),
authentication (credentials, sensitive), directory table (directory), file format
(file_format), and the universal tail (comment).
Full variable schema
| Variable | Type | Default | Notes |
|---|---|---|---|
database |
string |
β (required) | Avoid |, ., " β enforced by validation. Prefer a terraform-snowflake-database fully_qualified_name. |
name |
string |
β (required) | Avoid |, ., " β enforced by validation. |
schema |
string |
β (required) | Avoid |, ., " β enforced by validation. Prefer a terraform-snowflake-schema fully_qualified_name. |
url |
string |
β (required) | Must match ^s3compat:// β enforced by validation. |
endpoint |
string |
β (required) | Must be non-empty β enforced by validation. |
credentials |
object({ aws_key_id, aws_secret_key }) |
null |
Both fields required together if the object is supplied β enforced by the type itself. sensitive = true. |
directory |
object({ enable, auto_refresh, refresh_on_create }) |
null |
enable required bool; auto_refresh/refresh_on_create real bool, converted to provider string sentinels. |
file_format |
object({ format_name, avro, csv, json, orc, parquet, xml }) |
null |
At most one of the seven sub-keys may be set β enforced by validation. |
comment |
string |
null |
Universal tail. |
file_format's full nested type (reproduced verbatim from variables.tf for exhaustive
reference β each of avro/csv/json/orc/parquet/xml mirrors the provider's own nested
schema field-for-field):
file_format = object({
format_name = optional(string, null)
avro = optional(object({
compression = optional(string, null) # AUTO|GZIP|BROTLI|ZSTD|DEFLATE|RAW_DEFLATE|NONE
null_if = optional(list(string), null)
replace_invalid_characters = optional(bool, null)
trim_space = optional(bool, null)
}), null)
csv = optional(object({
binary_format = optional(string, null) # HEX|BASE64|UTF8
compression = optional(string, null) # AUTO|GZIP|BZ2|BROTLI|ZSTD|DEFLATE|RAW_DEFLATE|NONE
date_format = optional(string, null)
empty_field_as_null = optional(bool, null)
encoding = optional(string, null) # closed enum β see variables.tf
error_on_column_count_mismatch = optional(bool, null)
escape = optional(string, null)
escape_unenclosed_field = optional(string, null)
field_delimiter = optional(string, null)
field_optionally_enclosed_by = optional(string, null)
file_extension = optional(string, null)
multi_line = optional(bool, null)
null_if = optional(list(string), null)
parse_header = optional(bool, null)
record_delimiter = optional(string, null)
replace_invalid_characters = optional(bool, null)
skip_blank_lines = optional(bool, null)
skip_byte_order_mark = optional(bool, null)
skip_header = optional(number, null) # -1 sentinel forbidden β use null
time_format = optional(string, null)
timestamp_format = optional(string, null)
trim_space = optional(bool, null)
}), null)
json = optional(object({
allow_duplicate = optional(bool, null)
binary_format = optional(string, null) # HEX|BASE64|UTF8
compression = optional(string, null) # AUTO|GZIP|BZ2|BROTLI|ZSTD|DEFLATE|RAW_DEFLATE|NONE
date_format = optional(string, null)
enable_octal = optional(bool, null)
file_extension = optional(string, null)
ignore_utf8_errors = optional(bool, null)
multi_line = optional(bool, null)
null_if = optional(list(string), null)
replace_invalid_characters = optional(bool, null)
skip_byte_order_mark = optional(bool, null)
strip_null_values = optional(bool, null)
strip_outer_array = optional(bool, null)
time_format = optional(string, null)
timestamp_format = optional(string, null)
trim_space = optional(bool, null)
}), null)
orc = optional(object({
null_if = optional(list(string), null)
replace_invalid_characters = optional(bool, null)
trim_space = optional(bool, null)
}), null)
parquet = optional(object({
binary_as_text = optional(bool, null)
compression = optional(string, null) # AUTO|LZO|SNAPPY|NONE
null_if = optional(list(string), null)
replace_invalid_characters = optional(bool, null)
trim_space = optional(bool, null)
use_logical_type = optional(bool, null)
use_vectorized_scanner = optional(bool, null)
}), null)
xml = optional(object({
compression = optional(string, null) # AUTO|GZIP|BZ2|BROTLI|ZSTD|DEFLATE|RAW_DEFLATE|NONE
disable_auto_convert = optional(bool, null)
ignore_utf8_errors = optional(bool, null)
preserve_space = optional(bool, null)
replace_invalid_characters = optional(bool, null)
skip_byte_order_mark = optional(bool, null)
strip_outer_element = optional(bool, null)
}), null)
})| Output | Description | Sensitive / Conditional |
|---|---|---|
fully_qualified_name |
The stage's fully-qualified identifier | No |
name |
The stage's bare name | No |
id |
The stage's Terraform resource id | No |
credentials is never re-exposed as an output β it is sensitive = true on the input side only,
and the provider does not read it back from the live object either (see Troubleshooting).
credentials's both-required-together constraint is enforced by the type system, not avalidation {}block. The live schema documentsaws_key_id/aws_secret_keyas both Required within the (Optional)credentialsblock;variables.tfmirrors this exactly βobject({ aws_key_id = string, aws_secret_key = string })with no per-fieldoptionalwrapper β so a one-sided credentials object fails atterraform validatewith a parse-time type error, before any API call. This is a deliberate correction against the authoring prompt's speculative fallback shape, made after Phase 1's live-schema grounding confirmed the type system alone already expresses the constraint.- Tri-state bool-to-string-sentinel conversion, centralized.
directory.auto_refresh/directory.refresh_on_createand every boolean-shapedfile_formatsub-field are modeled as realboolvariables even though the underlying provider fields are STRING sentinels ("true"/"false"/internal"default", unsettable manually) β the same pattern already established byterraform-snowflake-schema(with_managed_access/is_transient) andterraform-snowflake-warehouse(auto_resume).main.tfperforms every conversion in onelocals.bool_strmap rather than scattering ternaries across eachdynamicblock, keepingmain.tfa "total renderer" per this suite's design convention. file_format's at-most-one-of-seven constraint (format_name,avro,csv,json,orc,parquet,xml) is enforced by a cross-attributevalidation {}block usingcompactover a list of conditionally-populated markers β this cannot be expressed by the type system alone (Terraform's object type has no native mutual-exclusivity constraint between attributes).- No
for_eachin this module. Standalone module, one keystone resource, no children β nofor_eachkey-stability concern to document. - No
storage_integrationrelationship exists to model, deliberately β this is the one stage variant in this catalog with no reference-based auth path at all. Do not add one in a future revision without first confirming a live schema change; as of provider v2.18.0 the field does not exist on this resource.
| Concern | Secure default | Opt-out (caller must be explicit) |
|---|---|---|
| Object comment | null β no default disclosure of intent |
Caller sets comment explicitly |
| Directory table | null β none created implicitly |
Caller sets directory = { enable = true,... } |
| File format | null β Snowflake infers at COPY INTO time |
Caller sets file_format explicitly (format_name preferred) |
| Secrets-bearing resources | credentials accepted only as a reference-shaped object, sensitive = true, sourced from a secret store upstream β never a .tfvars literal |
N/A β this resource has no reference-only (storage_integration) alternative to offer; this is stated honestly rather than a fabricated relationship being invented |
| Grant scope | Not applicable β this module owns no grants | N/A |
| Object dropping protection | Temporary stages are not supported by this resource at all (per the provider) | N/A |
π Honest limitation, stated plainly per this suite's Secure-by-default table: the house-wide "prefer a reference over a raw secret" principle cannot be fully honored for this specific resource. Every other secret-bearing Snowflake module in this catalog that has one offers a
storage_integration/similar reference-only path as its preferred pattern (terraform-snowflake-stage-external-s3,-external-azure,-external-gcsall do). This resource's live schema has no such field βcredentialsis the only mechanism the provider exposes for S3-compatible endpoints. This module therefore does the next-best thing:credentialsissensitive = true, is expected to be sourced from a secret store upstream and injected at apply time, and is never modeled as a plaintext literal β but it cannot be a bare reference the way its siblings' credentials can.
cd C:\GitHubCode\newsnowflakemodules\terraform-snowflake-stage-external-s3-compatible
terraform init -backend=false
terraform validate
terraform fmt -checkPin ?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.
terraform init -backend=false/terraform validate/terraform fmt -checkcover: type correctness (everyobject/scalar schema matches the live provider schema), HCL syntax, theurlscheme rejection, thecredentialsboth-required-together rejection (enforced by the type itself β no separatevalidation {}block exists for it), thefile_formatat-most-one-of-seven rejection, and every closed-enumvalidation {}block (compression, encoding, binary_format) catching a malformed input before any API call.- What these three commands do not cover: privilege sufficiency (whether the executing role
actually holds
CREATE STAGEon the target schema), object-existence dependencies (whether the referenced database/schema/file-format actually exist, whether the S3-compatibleendpointis actually reachable), whether suppliedcredentialsare valid against the endpoint, and whethercredentialsdrift silently β since the provider does not readcredentialsback fromDESCRIBE/SHOW STAGE, this module's state is authoritative only going forward from the lastapply, and only a realterraform plan/applyagainst a live Snowflake account (and a reachable endpoint) exercises any of this. Per this library's plan-only posture, that step belongs to a human running it from governed CI, not to this authoring process.
$ terraform output
fully_qualified_name = "\"RAW\".\"LANDING\".\"STAGE_MINIO_PARTNER_FEED\""
name = "STAGE_MINIO_PARTNER_FEED"
id = "\"RAW\".\"LANDING\".\"STAGE_MINIO_PARTNER_FEED\""
credentials is never printed here, even redacted β it is a sensitive = true input, not an
output, and this module does not re-expose it in any form.
| Symptom | Cause | Fix |
|---|---|---|
terraform validate fails on url |
url does not start with s3compat:// (e.g. a plain s3:// URL) |
Use terraform-snowflake-stage-external-s3 for Amazon S3 itself; use s3compat:// here for S3-compatible endpoints only. |
terraform validate fails on credentials with a missing-attribute error |
Only one of aws_key_id/aws_secret_key was supplied |
Supply both, or omit the credentials block entirely for an anonymous/public-read bucket. |
terraform validate fails on file_format |
More than one of format_name/avro/csv/json/orc/parquet/xml was set |
Set only one β prefer format_name referencing a pre-existing snowflake_file_format. |
terraform validate fails on file_format.csv.skip_header (or similar) |
Caller passed -1 literally |
Pass null instead β -1 is the provider's internal sentinel, not a caller-settable value. |
Rotated the S3-compatible endpoint's access key but plan shows no diff |
The provider does not read credentials back from DESCRIBE/SHOW STAGE output |
Treat this module's state as authoritative only going forward from the last apply; re-apply explicitly with the new key to confirm the correction. |
Plan wants to recreate the stage after changing directory.auto_refresh |
Confirmed provider behavior β setting auto_refresh to a new explicit value forces replacement |
Expected; review the recreate before applying. To avoid it, unset the field instead (alters directory in place using the current enable value). |
Changed directory.refresh_on_create on an existing stage but nothing happened |
This field is create-only; the provider silently ignores post-creation changes | Expected β there is no in-place way to trigger a one-time refresh via this field after creation. |
Persistent diff after terraform import |
Known provider import quirk with boolean-shaped fields | Enable the IMPORT_BOOLEAN_DEFAULT experimental feature and reimport β see the provider's MIGRATION_GUIDE.md. |
| Sibling module can't find this stage as a target | Consuming module referenced name instead of fully_qualified_name |
Always wire module.stage_x.fully_qualified_name, never the bare name, into a consuming module. |
- Provider resource docs:
snowflake_stage_external_s3_compatible - Sibling modules:
terraform-snowflake-database,terraform-snowflake-schema,terraform-snowflake-stage-external-s3(the Amazon S3 variant),terraform-snowflake-stage-internal,terraform-snowflake-stage-external-azure,terraform-snowflake-stage-external-gcs - This module's
SCOPE.md