# CloudFormation Components

Define CloudFormation stacks in stack manifests when you want Atmos to
deploy them alongside Terraform, Helm, Kubernetes, Helmfile, Packer, and
Ansible components. A CloudFormation component describes which template to
deploy, which parameters and capabilities to pass, and how the stack should
behave for each environment — all deployed directly through the AWS SDK for
Go v2, with no `aws` CLI or `cfn`/`sam`/`Rain` binary dependency.

> ⚠️ Experimental

## Available Configuration Sections

CloudFormation components use the same stack sections as other Atmos
components, so the stack can inherit values, run hooks, use Auth, declare
dependencies, and be included in affected runs.

- **[`metadata`](/stacks/components/component-metadata)**
  Component behavior, inheritance, and base component selection.
- **[`vars`](/stacks/vars)**
  Variables available to stack template rendering.
- **[`env`](/stacks/env)**
  Environment variables applied before CloudFormation operations run.
- **[`settings`](/stacks/settings)**

  Integration metadata and legacy dependency settings, including
  `settings.aws_cloudformation.region` (see [Region Resolution](/cli/configuration/components/aws-cloudformation#region-resolution)).
- **[`hooks`](/stacks/hooks)**

  Lifecycle event handlers. `diff`/`plan`, `apply`/`deploy`, `delete`,
  `drift detect`, and `drift describe` fire `before`/`after` events (`before.aws/cloudformation.diff`,
  `after.aws/cloudformation.apply`, and so on) — every other verb, including
  `render`, `validate`, `output`, `fmt`, `tree`, `logs`, `watch`,
  `changeset *`, `get *`, `stackset *`, `list`, `backend *`, and
  `source *`, does not fire hook events.
- **`source`**

  JIT provisioning of a remote template before operations run — see
  [Source Provisioning](#source-provisioning) below.
- **`provision`**

  Delivery targets: the account/region direct-deploy default, `kind: aws/s3`
  (template packaging), or `kind: git` (a Git deployment repository) — all
  selectable via `apply --target`. A fourth kind, `kind: aws/stackset`, is
  _not_ selected through `apply --target`; it's used exclusively by the
  `stackset` verb group. See [Delivery Targets](#delivery-targets) below for
  the full picture, including `provision.backend.enabled` auto-provisioning.
- **`auth`**
  Component-level Atmos Auth providers, identities, and integrations.
- **[`dependencies`](/stacks/dependencies/components)**
  Cross-component ordering for 
  `--all`
   and 
  `--affected`
   runs.

CloudFormation components do **not** support a `generate:` section — there is
no codegen-artifact output the way Terraform generates backend/provider files.
They also do not support a `plugins:` section — there is no chart-style plugin
system, unlike native Helm.

## CloudFormation-Specific Sections

- **`template` / `path` (one required)**

  The CloudFormation template, provided one of two mutually exclusive ways:
  - `template` — an **inline** template body, either a literal string (a
    `|` block scalar, YAML or JSON) or a structured YAML map. Because it's
    inline stack config, it flows through Atmos's own `{{ }}` templating
    pipeline before being sent to CloudFormation — file-based templates
    (`path:`) do not get this.
  - `path` — a **file reference**: the path to a template file, relative to
    the component directory under the base path.
  Setting both is an error.
- **`stack_name`**
  The explicit CloudFormation stack name. There is no legacy name-pattern interpolation — set the name you want directly (Go templates are supported, like any other stack field).
- **`parameters`**

  CloudFormation template parameters, as either a YAML map of name to value
  or a list of `ParameterKey`/`ParameterValue` entries (the AWS CLI and Rain
  parameter-file format, which also works through `!include`). Values are
  normalized at the API boundary: scalars are stringified, and list values are
  comma-joined to match CloudFormation's `List<Type>`/`CommaDelimitedList`
  wire format (the API only accepts strings). Any other shape, or a malformed
  list entry, is an error that names the problem; Atmos never falls back to
  the template defaults. Atmos config is the source of truth for every
  parameter on every deploy, the same declarative stance the Terraform
  component takes toward variables, so `UsePreviousValue` is not needed in
  the map form. The list form accepts `UsePreviousValue: true` (without a
  `ParameterValue`) for the rare update that must keep a stack's current value.
  To use only the `Parameters` section of a Rain config file, select it with
  `!include rain-config.yaml .Parameters`.
- **`capabilities`**

  Acknowledged IAM capabilities: `CAPABILITY_IAM`, `CAPABILITY_NAMED_IAM`, or
  `CAPABILITY_AUTO_EXPAND`. Atmos checks each value locally, and an unknown
  value fails before any AWS call, naming the value and the valid set.
- **`tags`**
  A map of 
  `key: value`
   tags applied to the CloudFormation stack (not to be confused with Atmos's own component 
  `tags`
  /
  `--tags`
   selection, which is separate).
- **`stack_policy`**

  Protects specific resources from update during `UpdateStack`. Set
  `stack_policy.file` to a stack policy JSON document path, relative to the
  component directory under the base path. Applied after a successful `apply`.
- **`role_arn`**
  IAM role ARN that CloudFormation assumes to deploy the stack.
- **`notification_arns`**
  SNS topic ARNs that CloudFormation publishes stack events to.
- **`disable_rollback`**
  Prevents automatic rollback on stack creation failure.
- **`termination_protection`**

  Prevents the stack from being deleted. `atmos aws cloudformation delete`
  respects this and fails with an actionable hint instead of silently
  disabling it — pass `--disable-termination-protection` to delete anyway.
  Setting this to `false` and re-applying does not disable protection: apply
  only ever turns protection on, never off.
- **`timeout_in_minutes`**

  Accepted for compatibility, but ignored with a warning: the CloudFormation
  changeset APIs do not support a stack timeout. Atmos stops watching after
  60 minutes; this does not cancel the AWS operation.

## Example

**File:** `stacks/catalog/vpc.yaml`

```yaml
components:
  "aws/cloudformation":
    vpc:
      path: template.yaml
      stack_name: "{{ .vars.stage }}-vpc"
      parameters:
        CidrBlock: "10.0.0.0/16"
        AvailabilityZones:
          - us-east-1a
          - us-east-1b
      capabilities:
        - CAPABILITY_IAM
      tags:
        team: platform
      stack_policy:
        file: stack-policy.json
      termination_protection: true
      timeout_in_minutes: 30
      dependencies:
        components:
          - vpc-flow-logs
```

Inline templates skip the on-disk file entirely and support two forms — a
literal string body, or a structured map:

**File:** `stacks/catalog/bucket.yaml`

```yaml
components:
  "aws/cloudformation":
    bucket-inline-string:
      stack_name: "{{ .vars.stage }}-bucket"
      template: |
        AWSTemplateFormatVersion: '2010-09-09'
        Resources:
          Bucket:
            Type: AWS::S3::Bucket
            Properties:
              BucketName: "{{ .vars.stage }}-bucket"

    bucket-inline-map:
      stack_name: "{{ .vars.stage }}-bucket-map"
      template:
        AWSTemplateFormatVersion: "2010-09-09"
        Resources:
          Bucket:
            Type: AWS::S3::Bucket
            Properties:
              BucketName: "{{ .vars.stage }}-bucket-map"
```

## Component Directory Structure

CloudFormation components are located under
`components."aws/cloudformation".base_path` from `atmos.yaml` (defaults to
`components/cloudformation`):

```text
components/cloudformation/
└── vpc/
    ├── template.yaml
    └── stack-policy.json
```

## Source Provisioning

Point a component at a remote template through the top-level `source:`
section — the same JIT vendoring used by other component types — instead of
committing the template to your infrastructure repository. Two shapes are
supported:

- **Directory/subdirectory source**

  Any go-getter URI (Git, HTTP archive, S3, OCI) pointing at a directory. The
  component directory (template, stack policy, and any local assets) is
  vendored, and `path:` resolves relative to it, exactly like other
  component types' `source:` behavior.
  ```yaml
  components:
    "aws/cloudformation":
      vpc:
        source:
          uri: github.com/acme/cfn-templates.git//vpc?ref={{ .Version }}
          version: 1.2.0
        path: template.yaml
  ```
- **Single-file source**

  When the `source.uri` resolves to exactly one file — a bare template URI,
  with no surrounding directory structure — Atmos fetches it directly as the
  component's `path:` file. A CloudFormation component is often exactly
  one file, and demanding a directory structure around it would be
  ceremony.
  ```yaml
  components:
    "aws/cloudformation":
      dns:
        stack_name: dns-dev
        source:
          uri: https://raw.githubusercontent.com/acme/cfn-templates/v1.2.0/dns.yaml
  ```
  `path:` does not need to be set in the single-file case — Atmos names
  the vendored file after the source URI's basename and inspects the provisioned result.
  Exactly one regular root file is inferred; ambiguous or nested contents require an explicit
  `path`. Provisioner metadata files are ignored. Deployed reads and deletion do not fetch the
  source; dry-run defers inference.

:::note Manifest-driven vendoring is not supported
`aws/cloudformation` components cannot be vendored through a `vendor.yaml`
manifest (`atmos vendor pull`) — only the `source:`-based JIT provisioning
described above, the same restriction native Helm and Kubernetes components
have. Use `source:` for every CloudFormation component that isn't authored
directly in your infrastructure repository.
:::

## Delivery Targets

By default, `apply`/`deploy` deploy directly to the account/region resolved
for the component (see
[Region Resolution](/cli/configuration/components/aws-cloudformation#region-resolution))
via `CreateChangeSet`/`ExecuteChangeSet` — this is the implicit behavior when
`--target` is omitted and no `provision.default` is set. A component can
declare additional named targets under `provision.targets`, selected with
`--target`:

- **`kind: aws/cloudformation`**

  The direct-deploy target. It is implicit — named `default`, with no
  configuration — whenever nothing else is selected, so most components never
  declare it. Declare it explicitly when it needs configuration, which today
  means the `packaging:` field described in [Choosing the Packaging
  Bucket](#choosing-the-packaging-bucket), or when you want a name to select
  with `--target` or `provision.default`.
  ```yaml
  components:
    "aws/cloudformation":
      vpc:
        provision:
          default: direct
          targets:
            direct:
              kind: aws/cloudformation
              packaging: artifacts
            artifacts:
              kind: aws/s3
              bucket: my-cfn-artifacts
              region: us-east-1
  ```
  The `provision.default` field names the target used when `--target` is
  omitted, and `--target` overrides it for one run.
- **`kind: aws/s3`**

  Uploads the template to S3 without deploying a stack. Selecting the
  `packaged` target below with `--target packaged` only uploads the template,
  for example for a review step.

  During a direct deployment, Atmos uses the S3 target as a packaging
  destination when the template exceeds CloudFormation's 51,200-byte inline
  size limit, then deploys the uploaded template. To deploy, omit `--target`
  when no `provision.default` is set, or select a direct
  `kind: aws/cloudformation` target. `bucket` and `region` are both required: `region`
  builds the `https://` S3 URL passed as `CreateChangeSet`'s `TemplateURL`
  (AWS rejects a bare `s3://` URI there), so it can't be left to be inferred
  later.
  ```yaml
  components:
    "aws/cloudformation":
      vpc:
        provision:
          targets:
            packaged:
              kind: aws/s3
              bucket: my-cfn-artifacts
              prefix: templates
              region: us-east-1
  ```
  Atmos uploads each template to a content-addressed key,
  `<prefix>/<stack>/<component>/template-<digest>.yaml`, next to a
  `.metadata.json` sidecar. Leading and trailing slashes on `prefix` are
  trimmed, so `prefix: /templates/` and `prefix: templates` produce the same
  keys. When an object with the same key and digest already exists, Atmos
  reuses it instead of uploading again, so repeated runs on a versioned
  bucket do not accumulate object versions. A publish-only `apply --target     <name>` reports the uploaded location as an `s3://` URI and an `https://`
  URL.

  The bucket must exist before `apply`/`deploy` uploads to it — either
  provision it explicitly with
  [`atmos aws cloudformation backend create`](/cli/commands/aws/cloudformation/backend/create),
  or set `provision.backend.enabled: true` (a sibling of `targets`, not
  nested under a target) to have `apply`/`deploy` create it automatically
  the first time it's missing. Auto-provisioning only runs the existence
  check up front — it never reconciles a bucket that already exists, unlike
  `backend create`/`update` run explicitly, which always re-applies the
  secure defaults (versioning, encryption, public access blocking, tags).
  ```yaml
  components:
    "aws/cloudformation":
      vpc:
        provision:
          backend:
            enabled: true
          targets:
            packaged:
              kind: aws/s3
              bucket: my-cfn-artifacts
              region: us-east-1
  ```
  Only `apply` and `deploy` create the bucket. The `validate`, `diff`, and
  `changeset create` verbs also upload an oversized template, but they never
  create infrastructure: when the bucket is missing they fail with a hint to
  run `backend create` or `apply`.
- **`kind: git`**

  Commits the template YAML to a Git repository (declared in
  `git.repositories`) instead of deploying it — for example, a review or
  GitOps-style pipeline that applies from the committed template separately.
  Follows the same `provision.targets` shape used by native Helm and
  Kubernetes components.
- **`kind: aws/stackset`**

  Delivers to a multi-account/multi-region
  [CloudFormation StackSet](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html)
  instead of a single-account/region stack. Unlike `kind: aws/s3` and
  `kind: git`, an `aws/stackset` target is **not** selected through
  `apply --target` — it is used exclusively by the
  [`atmos aws cloudformation stackset`](/cli/commands/aws/cloudformation/stackset)
  verb group, and only by `create`/`update`, which resolve it to learn the
  target accounts/regions/permission model. `delete` and `instances` act
  directly on `stack_name` (the StackSet's own name) and don't resolve or
  require a `provision.targets` entry.
  ```yaml
  components:
    "aws/cloudformation":
      vpc:
        provision:
          targets:
            multi-account:
              kind: aws/stackset
              accounts:
                - "111111111111"
                - "222222222222"
              regions:
                - us-east-1
                - us-west-2
              permission_model: SELF_MANAGED
              administration_role_arn: arn:aws:iam::111111111111:role/AWSCloudFormationStackSetAdministrationRole
              execution_role_name: AWSCloudFormationStackSetExecutionRole
  ```
  - **`accounts`**
    Target AWS account IDs for stack instances.
  - **`regions`**
    Target AWS regions for stack instances.
  - **`permission_model`**
    `SELF_MANAGED`
     (default) or 
    `SERVICE_MANAGED`
    . Atmos supports instance targeting only with 
    `SELF_MANAGED`
    . A 
    `SERVICE_MANAGED`
     target with both 
    `accounts`
     and 
    `regions`
     is rejected before the StackSet is created. Empty service-managed StackSets can be created and deleted; manage their instances with AWS CloudFormation.
  - **`administration_role_arn`**
    IAM role ARN CloudFormation assumes to create/manage the StackSet (self-managed permissions).
  - **`execution_role_name`**
    IAM role name CloudFormation assumes in each target account (self-managed permissions).
  `stackset create` creates the StackSet's initial stack instances only when
  both `accounts` and `regions` are set; `stackset update` updates the
  template/parameters/capabilities and propagates the change to every
  existing instance, without changing which accounts/regions have one.

### Choosing the Packaging Bucket

Packaging uploads an oversized template through a `kind: aws/s3` target. Atmos
picks that target as follows:

- When the selected target is itself `kind: aws/s3`, that target is used.
- When the component declares exactly one `kind: aws/s3` target, it is used
  automatically.
- When the component declares two or more, the selected deploy target must name
  one with `packaging: <target-name>`. Atmos never guesses.

The implicit direct-deploy target has no configuration, so it cannot carry
`packaging:`. With two or more `kind: aws/s3` targets, declare the deploy target
explicitly and make it the default:

```yaml
components:
  "aws/cloudformation":
    vpc:
      provision:
        default: direct
        targets:
          direct:
            kind: aws/cloudformation
            packaging: artifacts
          artifacts:
            kind: aws/s3
            bucket: my-cfn-artifacts
            region: us-east-1
          archive:
            kind: aws/s3
            bucket: my-cfn-archive
            region: us-east-1
```

Naming the explicit target `default` works too and replaces the implicit one,
so `provision.default` is not needed. A `kind: git` deploy target takes the same
`packaging:` field.

### Dry Run

A dry run (`--dry-run`) validates the configuration statically and makes no AWS
calls. It applies the same rules the real run enforces before it changes anything: an
unknown `--target`, a `kind: aws/s3` target without `bucket` or `region`, a
template over the 51,200-byte inline limit with no packaging target, and for
StackSets an unknown `permission_model` or a `SERVICE_MANAGED` target that sets
both `accounts` and `regions`. Values that come from templates or YAML functions
are not evaluated, so they are not judged. The output names the verb, component,
and stack, for example `Dry run: apply vpc in plat-ue2-dev: configuration
validated; no AWS calls made`.

## Related

- [`atmos aws cloudformation`](/cli/commands/aws/cloudformation) command reference
- [`atmos aws cloudformation stackset`](/cli/commands/aws/cloudformation/stackset) command reference
- [CloudFormation `atmos.yaml` configuration](/cli/configuration/components/aws-cloudformation)
- [Component dependencies](/stacks/dependencies/components)
