Skip to content

Latest commit

 

History

History
271 lines (240 loc) · 17.8 KB

File metadata and controls

271 lines (240 loc) · 17.8 KB

terraform-aws-sfn — SCOPE

Composite serverless orchestration module for AWS Step Functions. It owns a state machine (STANDARD or EXPRESS) and its optional companion activities — so a single module call yields a versioned, logged, X-Ray-traced workflow definition wired to an externally-supplied execution role, aligned with Casey's secure-by-default posture for regulated NPI/GLBA workloads.

  • Module type: Composite
  • Primary resource (keystone): aws_sfn_state_machine.this

In-scope resources

The module manages the following (allow-list):

  • aws_sfn_state_machine — keystone; STANDARD or EXPRESS workflow, ASL definition, logging_configuration, tracing_configuration, encryption_configuration, optional publish (versioning)
  • aws_sfn_activity — child collection via for_each over map(object(...))); long-polled workers for Task states of Type = "Task" with Resource = "arn:aws:states:::activity:..." (STANDARD workflows only — see Provider gotchas)

Out-of-scope resources (consumed by reference)

Referenced by arn/id, never created here:

  • State machine execution roleaws_iam_role (from terraform-aws-iam-role), supplied as role_arn. This module never creates or modifies IAM roles/policies; it only passes the role ARN to aws_sfn_state_machine.role_arn. The policy attached to that role determining what the ASL definition may actually invoke (Lambda, ECS, SNS, DynamoDB, etc.) is entirely out of scope and must be authored by the caller via terraform-aws-iam-role / terraform-aws-iam-policy — see the Required IAM permissions section below for why this matters.
  • CloudWatch Log Groupaws_cloudwatch_log_group (from terraform-aws-cloudwatch-log-group), supplied as log_destination_arn for logging_configuration.log_destination. This module does not create or manage log group retention/encryption.
  • Customer-managed KMS keyaws_kms_key (from terraform-aws-kms), supplied as kms_key_id for encryption_configuration. Defaults to AWS_OWNED_KEY (no CMK) when not supplied.
  • Downstream integration targets — Lambda functions, ECS task definitions, SNS topics, SQS queues, DynamoDB tables, etc. referenced by ARN inside the ASL definition string the caller supplies. This module treats definition as an opaque string; it does not parse or validate ASL service integrations.

Consumes

Input Type Source module
role_arn string (required) terraform-aws-iam-role
log_destination_arn string (optional, but required in practice — see Secure-by-default) terraform-aws-cloudwatch-log-group
kms_key_arn string (optional) terraform-aws-kms

Required IAM permissions

Least-privilege actions the Terraform executor identity (the CI/CD pipeline role or human operator applying this module) needs. This is distinct from — and must not be confused with — the state machine execution role (role_arn), which is a separate runtime identity assumed by the Step Functions service itself.

Action Required for
states:CreateStateMachine, states:DescribeStateMachine, states:UpdateStateMachine, states:DeleteStateMachine, states:ListStateMachines State machine lifecycle
states:CreateActivity, states:DescribeActivity, states:DeleteActivity, states:ListActivities Activity lifecycle
states:TagResource, states:UntagResource, states:ListTagsForResource Tagging both resource types
states:PublishStateMachineVersion Only when publish = true
iam:PassRole (scoped to the specific role_arn supplied, ideally via a StringEquals condition on iam:PassedToService = "states.amazonaws.com") MANDATORY. aws_sfn_state_machine.this cannot be created or updated without passing an execution role to the Step Functions service. Terraform's executing identity must hold iam:PassRole on that exact role ARN, or states:CreateStateMachine fails with an AccessDenied error even though every states:* permission above is granted.
kms:DescribeKey Only when a customer-managed kms_key_id is supplied to encryption_configuration (validates the key exists/is enabled)

⚠️ iam:PassRole is the single most common cause of first-apply failure for this module. Grant it narrowly — scope the resource to the specific execution-role ARN(s) this module is expected to pass, never Resource: "*", and add the iam:PassedToService condition so the permission cannot be reused to pass the role to an unrelated service.

Transitive-permissions gotcha (execution role, not Terraform identity)

This is the permissions gotcha that catches new module consumers. The IAM actions above only cover what Terraform needs to create the state machine resource. They say nothing about what the state machine can do once it runs. At execution time, Step Functions assumes role_arn and uses that role's own permissions — not the Terraform executor's — to carry out every Task state in the ASL definition. If the definition invokes Lambda, runs an ECS task, publishes to SNS, writes to DynamoDB, starts a nested state machine, etc., the execution role must separately hold the matching runtime permissions (e.g. lambda:InvokeFunction, ecs:RunTask + a second, distinct iam:PassRole for the ECS task role, sns:Publish, dynamodb:PutItem, states:StartExecution for nested workflows). This module has no visibility into the ASL definition string — it is treated as an opaque value — so it cannot scope, validate, or even warn about missing execution-role permissions. This is the same class of gotcha as CodePipeline/CloudFormation service roles: the orchestrator's IAM role must be provisioned with the union of every downstream action its pipeline/stack/state-machine steps will call, and that provisioning happens entirely in terraform-aws-iam-role / terraform-aws-iam-policy, outside this module's blast radius. Document the required execution-role policy alongside the ASL definition at the call site, not in this module.

AWS Prerequisites

  • No service-linked role required for Step Functions state machines or activities.

  • STANDARD vs EXPRESS is an immutable, foundational choice (type cannot be changed after creation — a type change is force-new). The two types have materially different execution, logging, and pricing models:

    STANDARD (default) EXPRESS
    Max execution duration Up to 1 year Up to 5 minutes
    Execution semantics Exactly-once Asynchronous: at-least-once; Synchronous: at-most-once
    Execution history Full history via API/console for 90 days post-completion No API/console history; must enable logging_configuration to CloudWatch Logs to retain any execution record
    Pricing Per state transition Per execution count + duration + memory consumed
    aws_sfn_activity (long-polled workers) Supported Not supported — do not pair activities with an EXPRESS state machine
    Distributed Map state Supported Not supported
    .sync / .waitForTaskToken service integration patterns Supported Not supported
    Best fit Long-running, auditable, non-idempotent workflows (payment processing, human-approval steps, EMR orchestration) High-volume, short-duration, idempotent workloads (IoT ingestion, streaming transforms, synchronous API-backed workflows)

    Choose type deliberately at design time; there is no in-place migration.

  • Logging requires a resource policy grant on the log group, not just logging_configuration. The CloudWatch Log Group (or its resource policy) must allow the Step Functions service principal (states.amazonaws.com) to write to it, and log_destination must be the log group ARN suffixed with :*. This module appends :* automatically; the log group's resource policy is provisioned by terraform-aws-cloudwatch-log-group or a standalone aws_cloudwatch_log_resource_policy, not by this module.

  • X-Ray tracing requires the execution role to hold the AWS managed policy (or equivalent) for X-Ray write access (xray:PutTraceSegments, xray:PutTelemetryRecords, xray:GetSamplingRules, xray:GetSamplingTargets) — another instance of the transitive-permissions gotcha above, since tracing_configuration.enabled = true alone does not grant the execution role anything.

  • Quotas: default execution start-rate and state-transition-rate quotas are Region-specific and adjustable via Service Quotas; high-throughput EXPRESS workloads should confirm headroom before go-live. Activities are capped per-account/Region (soft limit, raisable).

  • Encryption: encryption_configuration.type = "AWS_OWNED_KEY" (no CMK) is the provider default; supplying kms_key_id switches to CUSTOMER_MANAGED_KMS_KEY. The CMK's key policy must separately grant the execution role (and the Step Functions service principal) permission to use the key, or executions fail at runtime with a KMS access-denied error.

Emits

Output Description Consumed by
id State machine ARN (Step Functions uses the ARN as the resource id) Modules/callers that reference this state machine by id
arn State machine ARN (arn:aws:states:<region>:<account>:stateMachine:<name>) — cross-resource reference type terraform-aws-eventbridge (rule target), terraform-aws-iam-policy (states:StartExecution resource), API Gateway / Lambda callers that invoke this workflow
name State machine name Tagging, monitoring, dashboards keyed on name
state_machine_version_arn Version-qualified ARN (only set when publish = true) Callers pinning an execution to an immutable published version
status Current status (ACTIVE or DELETING) Health checks / drift detection
creation_date Creation timestamp Audit
revision_id Revision id of the definition/role_arn/configuration, incremented on every update Optimistic-concurrency checks against out-of-band changes
activity_ids / activity_arns / activity_names Maps of activity id/arn/name keyed by the caller's stable key Worker processes that long-poll GetActivityTask; the caller's own ASL definition (Task-state Resource)
tags_all All tags incl. provider default_tags Governance/audit

ℹ️ The provider schema also exposes description and version_description as computed-only attributes on aws_sfn_state_machine, but neither is documented in the current hashicorp/aws resource reference or the AWS Step Functions API (CreateStateMachine/DescribeStateMachine) as a populated field. This module deliberately does not surface them as outputs until their behavior is officially documented.

Provider gotchas

  • type is FORCE-NEW. Changing STANDARDEXPRESS destroys and recreates the state machine (and its execution history). Decide the type at design time.
  • name / name_prefix are FORCE-NEW (name changes recreate the resource, same as most named AWS resources without in-place rename). name and name_prefix are mutually exclusive.
  • definition is a live plan diff, not a hidden hash. Because ASL is supplied as a raw JSON string, whitespace/key-ordering changes can produce noisy diffs; prefer jsonencode() on a structured local in the caller to keep plans deterministic. This module passes definition through verbatim.
  • log_destination must end in :*. The provider does not append this automatically for you when composing the ARN from a plain log group ARN; the module handles the suffix internally so callers supply the bare log group ARN.
  • logging_configuration and tracing_configuration require execution-role permissions to function — see the Transitive-permissions gotcha above. Enabling them here is necessary but not sufficient.
  • id and arn are the same value for both aws_sfn_state_machine and aws_sfn_activity — Step Functions has no separate short-form id; the ARN is the id. Do not expect a bare resource id distinct from the ARN.
  • Activities are STANDARD-only. Pairing aws_sfn_activity resources with an EXPRESS state machine produces a state machine that references an activity ARN the EXPRESS runtime cannot invoke — this fails at execution time, not at plan/apply time, so it will not surface as a Terraform error. The module does not hard-block this combination (Terraform cannot inspect the ASL definition string to know which activities are referenced), but the README and variable descriptions call it out prominently.
  • encryption_configuration.type documentation discrepancy (discovered during authoring). The aws_sfn_activity resource page in the hashicorp/aws Terraform Registry documentation lists AWS_KMS_KEY as the non-CMK encryption_configuration.type value, but the authoritative AWS API EncryptionConfiguration type — shared by both CreateStateMachine and CreateActivity — documents AWS_OWNED_KEY as the only valid non-CMK value, matching aws_sfn_state_machine. Since the provider schema types encryption_configuration.type as a plain, unvalidated string (no enum enforcement at the Terraform layer), this module renders AWS_OWNED_KEY consistently for both the state machine and every activity to match the authoritative AWS API contract.
  • tags vs tags_all. var.tags flows to aws_sfn_state_machine.this and every aws_sfn_activity child; tags_all on each resource is the computed merge of resource tags over provider default_tags (resource tags win). default_tags is the caller's provider-block concern.
  • Destroy ordering. State machines and activities have no ENI/NAT-style destroy-ordering hazard, but a state machine with in-flight executions can still be deleted (Step Functions stops executions asynchronously on delete) — there is no force_delete/drain flag, so in-flight work is simply abandoned. Coordinate destroys with running workloads out of band.
  • Publishing (publish = true) creates an immutable version on every apply that changes definition, role_arn, or the config blocks — version count grows unbounded over the resource's lifetime with no built-in pruning; consider a lifecycle policy or manual cleanup if versioning is heavily used.

Secure-by-default decisions

Posture Default Opt-out
CloudWatch Logs (logging_configuration) Enabledlevel = "ALL", include_execution_data = true, requires log_destination_arn enable_logging = false (requires a documented exception — regulated workloads should retain execution history)
X-Ray tracing (tracing_configuration) Enabled (enabled = true) enable_tracing = false
Encryption at rest AWS_OWNED_KEY (AWS-managed, always on — Step Functions encrypts state data at rest unconditionally) Supply kms_key_arn to upgrade to CUSTOMER_MANAGED_KMS_KEY; there is no way to fully disable encryption (not a real opt-out, by design)
include_execution_data true by default so execution history is auditable Set logging_include_execution_data = false when the ASL definition's input/output payloads may carry NPI that should not land in CloudWatch Logs — recommended for any workflow touching member/borrower data
Log level "ALL" logging_level variable (ALL | ERROR | FATAL | OFF, closed enum)
Type STANDARD (auditable, exactly-once — the safer default for regulated workflows) type = "EXPRESS" for high-volume/idempotent workloads that explicitly accept at-least-once semantics

ℹ️ NPI callout: because include_execution_data = true logs full state input/output to CloudWatch Logs, any state machine whose ASL definition passes member/borrower NPI through state input/output should either set logging_include_execution_data = false or ensure the target log group is access-controlled and retention-managed to the same standard as any other NPI-bearing log stream. This module defaults to true for auditability; flip it per Casey's data-classification policy for the specific workflow.

Design decisions

  • The module wraps exactly one state machine plus its optional activities so a single call yields a complete, logged, traced workflow definition — the same "one call, fully wired" ergonomic as terraform-aws-vpc.
  • The execution role is deliberately out of scope and required as an input (role_arn), keeping IAM authoring (and the transitive-permissions problem) centralized in terraform-aws-iam-role rather than duplicated or hidden inside every module that needs an execution role.
  • definition is accepted as an opaque string (not parsed/typed) because ASL is a rich, evolving JSON DSL with nested Task/Map/Parallel/ Choice states — modeling it as a Terraform object() schema would be brittle and would lag new state types. Callers own the ASL, typically via jsonencode() or a templatefile() in the calling root module. aws_sfn_activity — the one first-class ASL dependency this module can model structurally — is exposed as a proper for_each child collection so activity ARNs can be referenced back into the caller's definition before the state machine is created.
  • Activities are modeled as a child collection (for_each over map(object({...}))) rather than a single resource, since a workflow may poll multiple long-running activity workers, each independently named and tagged.
  • log_destination_arn and kms_key_arn are accepted as plain ARNs (not full object blocks) since the only per-call decision is which log group / key to use — the log group's own retention, encryption, and resource policy are terraform-aws-cloudwatch-log-group's and terraform-aws-kms's concerns, not this module's.