# Create Atmos Stacks

In the previous steps, we configured the Terraform components and started the [local sandbox](/quick-start/advanced/start-sandbox).

Next step is to create and configure [Atmos stacks](/learn/stacks) — the YAML configuration that tells Atmos how each component should be provisioned in
each environment. This is the heart of the tutorial, and it's where most of the Atmos design patterns show up.

## Create Catalog for Components

Atmos supports the [Configuration Catalog](/design-patterns/component-catalog) pattern to define default settings for Atmos components.
All the common default settings for each component go into a separate file under the `stacks/catalog` directory, which is then imported into the
parent Atmos stacks. This keeps the stack configurations DRY by reusing the component config that's common to every environment.

Refer to [Stack Imports](/stacks/imports) for more details on Atmos imports.

### `kms-key`

The KMS key encrypts every other resource, so it's the root of the dependency graph. Notice there is **no per-component identity binding**: every component runs
under the [default identity](/quick-start/advanced/configure-project) (`local-aws`, declared with `default: true` in `atmos.yaml`), so it reaches the local
sandbox automatically. To run a component under a _different_ identity you'd use the `--identity` flag or a component-level `auth` section — not a `settings` field.

**File:** `stacks/catalog/kms-key/defaults.yaml`

```yaml
components:
  terraform:
    kms-key:
      metadata:
        component: kms-key
        description: "Customer-managed KMS key that encrypts the application backend."
      vars:
        name: app
        enable_key_rotation: true
```

### `s3-bucket`

The bucket is server-side encrypted with the KMS key. Two patterns appear here for the first time:

- **`dependencies.components`** declares that the bucket depends on `kms-key`, so Atmos deploys the key first (see [Component Dependencies](/stacks/dependencies)).
- **Stack name templates** build predictable resource coordinates from `namespace`, `tenant`, `environment`, and `stage`, so the full graph can be planned before first-run state exists.

It also declares `validation` (covered in [Configure Validation](/quick-start/advanced/configure-validation)) and a `hooks` block that publishes the
bucket's coordinate to the `config/ssm` store after apply (covered under [service discovery](#two-ways-components-share-data) below).

**File:** `stacks/catalog/s3-bucket/defaults.yaml`

```yaml
components:
  terraform:
    s3-bucket:
      metadata:
        component: s3-bucket
        description: "Encrypted S3 bucket for application assets."
      settings:
        validation:
          validate-s3-bucket-with-jsonschema:
            schema_type: jsonschema
            schema_path: "s3-bucket/validate-s3-bucket-component.json"
            description: Validate 's3-bucket' variables using JSON Schema
          check-s3-bucket-with-opa-policy:
            schema_type: opa
            schema_path: "s3-bucket/validate-s3-bucket-component.rego"
            module_paths:
              - "catalog/constants"
            description: Check 's3-bucket' configuration using OPA policy
            timeout: 10
      dependencies:
        components:
          - name: kms-key
      hooks:
        publish-coordinates:
          events:
            - after.terraform.apply
          kind: store
          name: config/ssm
          outputs:
            bucket_id: .bucket_id
      vars:
        name: assets
        versioning_enabled: true
        kms_key_arn: "alias/{{ .vars.namespace }}-{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}-app"
```

### `dynamodb-table`

The DynamoDB table has no dependencies of its own; it just publishes its table name to the `config/ssm` store after apply.

**File:** `stacks/catalog/dynamodb-table/defaults.yaml`

```yaml
components:
  terraform:
    dynamodb-table:
      metadata:
        component: dynamodb-table
        description: "DynamoDB table for application state."
      hooks:
        publish-coordinates:
          events:
            - after.terraform.apply
          kind: store
          name: config/ssm
          outputs:
            table_name: .table_name
      vars:
        name: state
        hash_key: id
```

### `sns-topic`

The SNS topic is KMS-encrypted with the same deterministic key alias and depends on `kms-key`.

**File:** `stacks/catalog/sns-topic/defaults.yaml`

```yaml
components:
  terraform:
    sns-topic:
      metadata:
        component: sns-topic
        description: "SNS topic for application events."
      dependencies:
        components:
          - name: kms-key
      hooks:
        publish-coordinates:
          events:
            - after.terraform.apply
          kind: store
          name: config/ssm
          outputs:
            topic_arn: .topic_arn
      vars:
        name: events
        kms_key_arn: "alias/{{ .vars.namespace }}-{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}-app"
```

### `sqs-queue`

The queue subscribes to the SNS topic. It uses the stack-derived topic ARN and depends on `sns-topic`, so the graph still deploys the topic first.

**File:** `stacks/catalog/sqs-queue/defaults.yaml`

```yaml
components:
  terraform:
    sqs-queue:
      metadata:
        component: sqs-queue
        description: "SQS queue subscribed to the events topic."
      dependencies:
        components:
          - name: sns-topic
      hooks:
        publish-coordinates:
          events:
            - after.terraform.apply
          kind: store
          name: config/ssm
          outputs:
            queue_url: .queue_url
      vars:
        name: events
        topic_arn: "arn:aws:sns:{{ .vars.region }}:000000000000:{{ .vars.namespace }}-{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}-events"
```

### `app-config`

The `app-config` component ties everything together. It depends on all five resources, uses stack-derived coordinates for the values it publishes, and
pulls two [secrets](/quick-start/advanced/configure-secrets) with the [`!secret`](/functions/yaml/secret) function. Keeping the coordinates template-based
lets `atmos terraform deploy --all` build the full graph on the first run, before any Terraform state or store values exist.

**File:** `stacks/catalog/app-config/defaults.yaml`

```yaml
components:
  terraform:
    app-config:
      metadata:
        component: app-config
        description: "Publishes resolved app configuration and secrets to SSM Parameter Store."
      dependencies:
        components:
          - name: kms-key
          - name: s3-bucket
          - name: dynamodb-table
          - name: sns-topic
          - name: sqs-queue
      secrets:
        vars:
          API_KEY:
            description: "Third-party API key for the application."
            store: secrets/ssm
            required: true
          DB_CONFIG:
            description: "Structured database credentials (JSON) stored in Secrets Manager."
            store: secrets/asm
            required: true
      vars:
        name: app
        kms_key_arn: "alias/{{ .vars.namespace }}-{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}-app"
        bucket_id: "{{ .vars.namespace }}-{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}-assets"
        table_name: "{{ .vars.namespace }}-{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}-state"
        topic_arn: "arn:aws:sns:{{ .vars.region }}:000000000000:{{ .vars.namespace }}-{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}-events"
        queue_url: "http://sqs.{{ .vars.region }}.localhost.localstack.cloud:4566/000000000000/{{ .vars.namespace }}-{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}-events"
        api_key: !secret API_KEY
        db_password: !secret DB_CONFIG | path ".password"
```

### Two ways components share data

This example deliberately shows **two cross-component patterns**, and it's worth pausing on the distinction:

- **Templates plus dependencies** — stack templates produce predictable names and ARNs, while `dependencies.components` controls apply/destroy order.
  This is the primary path in the quick start because it supports first-run `atmos terraform deploy --all`.

- **Stores** — components also write their actual outputs to a [store](/cli/configuration/stores) after apply using a [hook](/stacks/hooks). Operators
  can inspect those published coordinates, and larger stacks can use [`!store`](/functions/yaml/store) when consumers should discover values that are not
  predictable from naming conventions.

## Group the catalog with `catalog/backend`

Rather than importing all six catalog files everywhere, the example collects them (plus the emulator) into a single `catalog/backend` manifest. Importing
`catalog/backend` once brings the whole backend into a stack.

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

```yaml
import:
  - catalog/emulator/aws
  - catalog/kms-key/defaults
  - catalog/s3-bucket/defaults
  - catalog/dynamodb-table/defaults
  - catalog/sns-topic/defaults
  - catalog/sqs-queue/defaults
  - catalog/app-config/defaults
```

## Atmos Top-level Stacks

When executing the [CLI commands](/cheatsheets/commands), Atmos does not use the stack file names and their filesystem locations to search for the stack
where the component is defined. Instead, Atmos uses the context variables (`namespace`, `tenant`, `environment`, `stage`) to search for the stack. The
stack config file names can be anything, and they can be in any folder or sub-folder in the `stacks` directory.

For example, when executing the `atmos terraform apply app-config -s plat-ue2-dev`
command, the Atmos stack `plat-ue2-dev` is specified by the `-s` flag. Atmos evaluates `name_template: "{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.stage }}"`
(see [Configure CLI](/quick-start/advanced/configure-project)) for each top-level stack. The stack whose variables render to `plat-ue2-dev` is the match.
Then Atmos searches the top-level stack manifests (in the `stacks` directory) where `tenant: plat`, `environment: ue2` and `stage: dev` are defined
(inline or via imports).

We use a [hierarchical layout](/design-patterns/stack-organization/organizational-hierarchy-configuration) that follows the way AWS thinks about
infrastructure (organization → OU/tenant → account/stage → region). This works very well as you grow to dozens or hundreds of accounts and regions, and
it's the recommended starting point even for a small example like this one.

Create the following filesystem layout (the final layout for this Quick Start guide):

**File:** `infra-live/`

```
│   # Centralized stacks configuration
   ├── stacks
   │   ├── catalog
   │   │    ├── backend.yaml
   │   │    ├── emulator
   │   │    │   └── aws.yaml
   │   │    ├── kms-key
   │   │    │   └── defaults.yaml
   │   │    ├── s3-bucket
   │   │    │   └── defaults.yaml
   │   │    ├── dynamodb-table
   │   │    │   └── defaults.yaml
   │   │    ├── sns-topic
   │   │    │   └── defaults.yaml
   │   │    ├── sqs-queue
   │   │    │   └── defaults.yaml
   │   │    └── app-config
   │   │        └── defaults.yaml
   │   ├── mixins
   │   │    └── region
   │   │        ├── global-region.yaml
   │   │        ├── us-east-2.yaml
   │   │        └── us-west-2.yaml
   │   └── orgs
   │        └── acme
   │            ├── _defaults.yaml
   │            ├── core
   │            │   └── _defaults.yaml
   │            └── plat
   │                 ├── _defaults.yaml
   │                 ├── dev
   │                 │   ├── _defaults.yaml
   │                 │   ├── global-region.yaml
   │                 │   ├── us-east-2.yaml
   │                 │   └── us-west-2.yaml
   │                 ├── prod
   │                 │   ├── _defaults.yaml
   │                 │   ├── global-region.yaml
   │                 │   ├── us-east-2.yaml
   │                 │   └── us-west-2.yaml
   │                 └── staging
   │                     ├── _defaults.yaml
   │                     ├── global-region.yaml
   │                     ├── us-east-2.yaml
   │                     └── us-west-2.yaml
   │
   │   # Centralized components configuration. Components are broken down by tool
   └── components
       └── terraform   # Terraform components (Terraform root modules)
           ├── kms-key
           ├── s3-bucket
           ├── dynamodb-table
           ├── sns-topic
           ├── sqs-queue
           └── app-config
```

### Configure Region Mixins

[Mixins](/design-patterns/component-catalog/mixins) are a special kind of "[import](/stacks/imports)".
It's simply a convention we recommend to distribute reusable snippets of configuration that alter behavior in some deliberate way.
Mixins are not handled in any special way — they are technically identical to all other imports.

Reach for a mixin when a snippet is **genuinely cross-cutting** — reused across many stacks regardless of where they sit in the hierarchy. The region is
the textbook case: every account (`dev`, `staging`, `prod`) deploys into the same set of regions, so the region context belongs in one reusable place.

:::tip Don't create a mixin for a single line
Earlier versions of this example also had `mixins/tenant/*` and `mixins/stage/*` files that each set a single context variable (`tenant: plat`,
`stage: dev`). That added an extra file to chase for no real reuse. A value that belongs to exactly one layer of the hierarchy should live **in that
layer's `_defaults.yaml`**, not in a one-line mixin. So `tenant` now lives in the tenant's `_defaults.yaml`, and `stage` lives in each account's
`_defaults.yaml`. Component settings stay in the top-level regional stacks that actually deploy those components.
:::

In `stacks/mixins/region/us-east-2.yaml`, add the following config:

**File:** `stacks/mixins/region/us-east-2.yaml`

```yaml
vars:
  region: us-east-2
  environment: ue2
```

In `stacks/mixins/region/us-west-2.yaml`, add the following config:

**File:** `stacks/mixins/region/us-west-2.yaml`

```yaml
vars:
  region: us-west-2
  environment: uw2
```

The region mixin defines the global context variables `region` and `environment`, which Atmos uses when searching for a component in a stack. The mixin
gets imported into the top-level stacks, so we don't repeat the region context in every account — keeping the configuration DRY. This is the
[multi-region](/design-patterns/stack-organization/multi-region-configuration) modeling pattern.

### Configure Defaults for Organization, OU and accounts

The `_defaults.yaml` stack manifests contain the default settings for the Organization(s), Organizational Units, and accounts. This is the
[`_defaults.yaml` design pattern](/design-patterns/stack-organization/defaults-pattern).

:::info
The `_defaults.yaml` stack manifests are not imported into other Atmos manifests automatically.
You need to explicitly import them using [imports](/stacks/imports).
:::

In `stacks/orgs/acme/_defaults.yaml`, define the `namespace` for the entire `acme` Organization (the real file also sets common tags and template
settings):

**File:** `stacks/orgs/acme/_defaults.yaml`

```yaml
vars:
  namespace: acme
```

In `stacks/orgs/acme/plat/_defaults.yaml`, configure the `plat` OU (tenant). The `tenant` context variable lives right here in the tenant's
`_defaults.yaml` (no separate one-line mixin to chase). Defaults files should hold context and truly common settings; deployable component catalogs are
imported by the top-level stacks that actually run them.

**File:** `stacks/orgs/acme/plat/_defaults.yaml`

```yaml
import:
  - orgs/acme/_defaults

vars:
  tenant: plat
```

When Atmos processes this stack config, it imports and deep-merges all the variables defined in the imported files and inline. Since the backend catalog
is not imported at this layer, helper stacks such as `global-region` can carry context without accidentally defining regional application resources.

In `stacks/orgs/acme/plat/dev/_defaults.yaml`, configure the `dev` account context. Keep this file focused on context and truly common defaults:

**File:** `stacks/orgs/acme/plat/dev/_defaults.yaml`

```yaml
import:
  - orgs/acme/plat/_defaults

vars:
  stage: dev
```

Configure the `prod` account the same way:

**File:** `stacks/orgs/acme/plat/prod/_defaults.yaml`

```yaml
import:
  - orgs/acme/plat/_defaults

vars:
  stage: prod
```

Add the `staging` account defaults the same way.

### Configure Top-level Stacks

After we've configured the catalog, the region mixins, and the defaults for the Organization, OU and accounts, the final step is to configure the Atmos
root (top-level) stacks. Each deployable regional leaf stack imports the backend catalog, its account defaults, and a region mixin. It also carries the
component settings for that stage and region.

This keeps component definitions in the stacks that actually deploy them, while defaults stay focused on shared context. This layering is the
[organizational hierarchy](/design-patterns/stack-organization/organizational-hierarchy-configuration) pattern in action.

In `stacks/orgs/acme/plat/dev/us-east-2.yaml`, define the dev backend in `us-east-2`. This is where you can see, at a glance, **what makes `dev`
different** from the catalog defaults — same services, deliberately cheaper and more ephemeral settings:

**File:** `stacks/orgs/acme/plat/dev/us-east-2.yaml`

```yaml
import:
  - catalog/backend
  - orgs/acme/plat/dev/_defaults
  - mixins/region/us-east-2

components:
  terraform:
    s3-bucket:
      vars:
        force_destroy: true        # easy teardown of non-empty dev buckets
        versioning_enabled: false  # not worth the storage for throwaway data
    kms-key:
      vars:
        deletion_window_in_days: 7 # shortest window AWS allows
        enable_key_rotation: false
    sqs-queue:
      vars:
        message_retention_seconds: 86400  # 1 day
```

Similarly, create the top-level Atmos stack for the `dev` account in `us-west-2` with the same dev component settings and a different region mixin:

**File:** `stacks/orgs/acme/plat/dev/us-west-2.yaml`

```yaml
import:
  - catalog/backend
  - orgs/acme/plat/dev/_defaults
  - mixins/region/us-west-2

components:
  terraform:
    s3-bucket:
      vars:
        force_destroy: true
        versioning_enabled: false
    kms-key:
      vars:
        deletion_window_in_days: 7
        enable_key_rotation: false
    sqs-queue:
      vars:
        message_retention_seconds: 86400
```

Repeat the pattern for the `staging` and `prod` accounts, importing the matching `_defaults`, region mixin, and component settings. For example,
`stacks/orgs/acme/plat/prod/us-east-2.yaml` uses the **opposite, hardened** choices:

**File:** `stacks/orgs/acme/plat/prod/us-east-2.yaml`

```yaml
import:
  - catalog/backend
  - orgs/acme/plat/prod/_defaults
  - mixins/region/us-east-2

components:
  terraform:
    s3-bucket:
      vars:
        force_destroy: false       # never silently delete a non-empty prod bucket
        versioning_enabled: true   # required in prod (enforced by the OPA policy)
    kms-key:
      vars:
        deletion_window_in_days: 30 # longest window AWS allows — recoverable
        enable_key_rotation: true
    sqs-queue:
      vars:
        message_retention_seconds: 1209600  # 14 days (AWS max)
```

These per-account, per-region leaf files are the top-level stacks Atmos resolves when you pass `-s plat-ue2-dev`, `-s plat-uw2-prod`, and so on.
The `global-region` stacks intentionally omit `catalog/backend`; they define context only, so regional resources like `sns-topic` and `sqs-queue` are not
accidentally instantiated in `plat-gbl-*`.

:::tip Component Inheritance
This is exactly what the regional top-level stack files do above: the catalog defines the defaults once, and each stage overrides only what differs via
[Component Inheritance](/design-patterns/inheritance-patterns/component-inheritance). `dev` turns `versioning_enabled` off while `prod` keeps it on —
which is exactly the difference the [OPA policy](/quick-start/advanced/configure-validation) enforces for `prod`. You can override per stage (as here) or
per region, at any layer of the hierarchy.
:::

---

**Next:** run scans on every plan and publish outputs on every apply → **[Configure Hooks →](/quick-start/advanced/configure-hooks)**
