Skip to main content

Migrating from Rain

Move the CloudFormation stacks you deploy with Rain to Atmos. Your templates, parameter files, and deployed stacks stay where they are. This guide maps every Rain directive, config file, and command to its Atmos-native equivalent, so you end up with Atmos conventions rather than a Rain emulation.

Using an AI Coding Assistant?

Install the atmos-migration skill so Claude Code, Cursor, GitHub Copilot, and other AI coding assistants can apply this guide directly to your repository:

atmos ai skill install atmos-migration

See AI Agent Skills for details.

Experimental

Why Migrate from Rain​

Rain was archived in July 2026. The existing releases still run, but nothing ships after them. If Rain is how your team deploys CloudFormation, the question is what replaces it, and Atmos answers it with a native aws/cloudformation component type. Atmos talks to the CloudFormation API through the AWS SDK, so there is no Rain, aws, or sam binary involved, and your stacks become components in the same stack manifests, inheritance, and CI workflows the rest of your infrastructure uses.

The migration is parity, not compatibility. Rain preprocessed templates and resolved !Rain:: directives before deployment. Atmos sends a file template to CloudFormation exactly as written and offers a native way to accomplish what each directive did. Templates that are plain CloudFormation need no changes at all.

Key Differences at a Glance​

ConcernRainAtmos
Stack nameCommand argumentstack_name on the component
Parameters--params, --config, RAIN_VAR_*parameters: map, or !include of your existing parameter file
Tags--tags, --config, RAIN_DEFAULT_TAG_*tags: map, or tags: !labels to carry Atmos labels onto the stack
Region and credentials--region, --profilesettings.aws_cloudformation.region and Atmos Auth or the SDK credential chain
Template directives!Rain::Env, !Rain::Include, !Rain::Embed, !Rain::S3YAML functions inside an included template, hooks for uploads
Artifact bucketrain-artifacts-<account>-<region>, created on first deployA kind: aws/s3 provision target, created by backend create or automatically with provision.backend.enabled: true
Asset packagingrain pkg rewrites the templatearchive and publish hook steps upload assets; the template references them through parameters
EnvironmentsOne invocation per environmentOne stack manifest per environment, sharing a catalog through imports and inheritance
Stack outputsConsole or aws cloudformation describe-stacksatmos aws cloudformation output, plus !aws.cloudformation.output from other components
DriftNot a Rain featuredrift detect and drift describe

Directory Structure Comparison​

.
├── templates/
│ ├── app.yaml # !Rain::Env, !Rain::Embed, !Rain::Include, !Rain::S3
│ └── fragments/tags.yaml
├── src/handler.py # embedded by !Rain::Embed
├── dist/worker/ # uploaded by !Rain::S3
├── templates/network.yaml # a second stack the app reads an output from
├── params/
│ ├── dev.yaml # {Parameters: {...}, Tags: {...}} per environment
│ └── staging.yaml
└── deploy.sh # loops environments, hand-orders network before app,
# copies the network output into the app parameters

Nothing forces this layout. Point components."aws/cloudformation".base_path at wherever your templates already live and keep the parameter files where they are.

Concept Mapping​

Rain Directives​

Rain resolved these directives by rewriting the template before it reached CloudFormation. Atmos never rewrites a file template, so a template that still contains a directive fails locally, before any AWS call, with the replacement named for each directive:

Error: aws/cloudformation template contains Rain directives, which Atmos does not preprocess

components/cloudformation/app/template.yaml uses Rain directives (Constant, Env, Embed, S3).

💡 Replace !Rain::Env with `!env NAME` in the component's parameters:, or inside the template
when it is included with template: !include template.yaml | eval
💡 Migration guide: https://atmos.tools/migration/rain
DirectiveWhat Rain didAtmos-native replacement
!Rain::Env NAMEInserted an environment variableA CloudFormation parameter fed by !env NAME in parameters:, or !env NAME inside the template when it is included with | eval
!Rain::Constant NAME, ${Rain::NAME}Inserted a value from the template's Rain: Constants: sectionA CloudFormation parameter fed from stack vars:, or {{ .vars.name }} in an inline template
!Rain::Include file.yamlMerged a YAML or JSON fragment!include file.yaml inside the template, included with | eval
!Rain::Embed fileInlined a file as a string!include file inside the template, included with | eval; a non-YAML file loads as a string
!Rain::S3 path, !Rain::S3Http pathUploaded a file or directory and inserted its S3 URI or URLarchive and publish hook steps upload the asset; the template reads the bucket and key from parameters
!Rain::ModuleClient-side template composition (experimental in Rain)No equivalent. Expand modules in your build step, or use AWS CDK, and point path at the output

Two of these replacements rely on the | eval option of !include. An included file is data by default, so its YAML functions are not evaluated. Adding | eval turns that on for the included template, and CloudFormation's own short-form intrinsics (!Ref, !Sub, !GetAtt) are preserved either way.

Environment Values and Constants​

Rain:
Constants:
MarkerPrefix: /acme/app
Resources:
Marker:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub "${Rain::MarkerPrefix}/${Stage}/owner"
Type: String
Value: !Rain::Env DEPLOY_OWNER

Turning a directive into a CloudFormation parameter keeps the template plain, which is the most portable outcome. An unset variable without a default resolves to an empty string and logs a warning, so give !env a default (!env DEPLOY_OWNER platform) when an empty value would be a mistake.

Included Fragments and Embedded Files​

When you would rather keep the directives' shape, include the template into template: with | eval and use Atmos functions in the same places Rain used directives:

Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
Tags: !Rain::Include fragments/tags.yaml
Function:
Type: AWS::Lambda::Function
Properties:
Role: !GetAtt Role.Arn
Code:
ZipFile: !Rain::Embed ../src/handler.py

A nested !include inside the template resolves ./ and ../ relative to the template file, exactly where Rain's !Rain::Embed ../src/handler.py looked; bare paths resolve relative to base_path.

Uploaded Assets​

!Rain::S3 uploaded a file or directory during deployment and wrote the location into the template. Atmos keeps uploads out of the template and in a hook, because YAML functions only read values and never change infrastructure. The hook runs before apply and before changeset create, and the template reads the bucket and key through parameters:

Resources:
Worker:
Type: AWS::Lambda::Function
Properties:
Code: !Rain::S3
Path: ../dist/worker
BucketProperty: S3Bucket
KeyProperty: S3Key
Zip: true

The key carries a release variable (set it per deployment, for example from a git tag or !env RELEASE), because CloudFormation only updates a Lambda function when the S3Key value changes; uploading new code under the same key leaves the function untouched. The publish step skips uploads whose content has not changed, so repeated deployments of the same release report 0 uploaded, 1 unchanged. See asset packaging hooks for the full pattern.

Parameter Files and Rain Config Files​

An AWS CLI parameter array is accepted as-is, and the Parameters and Tags sections of a Rain config file are selected with a YQ expression:

stacks/deploy/dev.yaml
components:
"aws/cloudformation":
app:
parameters: !include ../../params/dev.json
stacks/deploy/staging.yaml
components:
"aws/cloudformation":
app:
parameters: !include ../../params/rain-config.yaml .Parameters
tags: !include ../../params/rain-config.yaml .Tags

Including a whole Rain config file as parameters is an error, because Parameters and Tags would be read as two parameters with map values. Values Rain took from RAIN_VAR_* and RAIN_DEFAULT_TAG_* environment variables belong in the catalog as parameters and tags, where every environment inherits them.

Tags​

Stack tags are a map. An AWS CLI tags file is a list of Key/Value objects and is rejected rather than silently dropped. Reshape it with a YQ expression, or carry your Atmos metadata.labels onto the stack explicitly:

tags: !include '../../params/tags.json ".[] as $t ireduce ({}; .[$t.Key] = $t.Value)"'
# or
tags: !labels

Atmos labels and tags select components in Atmos commands; they never become CloudFormation tags unless you write tags: !labels.

The Artifact Bucket​

Rain created rain-artifacts-<account>-<region> the first time a deployment needed it. Atmos uses the kind: aws/s3 provision target you declare, and creates it in one of two explicit ways:

atmos aws cloudformation backend create app -s dev

or, for the same create-on-demand behavior Rain had, with provision.backend.enabled: true on the component, which makes apply and deploy create a missing bucket. Preview commands never create it; they fail with a hint naming the command that will. Templates over the 51,200-byte inline limit are uploaded to this bucket automatically on every command that submits a template.

CLI Command Comparison​

RainAtmosNotes
rain deploy <template> <stack>apply or deployapply previews the changeset and prompts; deploy auto-approves
rain deploy --no-execchangeset createPrints the changeset name and keeps it; fires the changeset-create hook events
rain deploy --changeset <stack> <name>changeset execute --changeset-name=<name>
rain deploy --keepdisable_rollback: trueComponent field
rain deploy --role-arnrole_arnComponent field
rain deploy --termination-protectiontermination_protection: truedelete then requires --disable-termination-protection
rain diff <from> <to>plan / diffDifferent operation: Rain compared two templates, Atmos previews a changeset against the deployed stack
rain fmtfmtFormats in place; --check is Rain's --verify. Blank lines between sections are removed
rain cat <stack>get template--original returns the submitted template, like rain cat --unformatted
rain lslistOne region per call; marks which stacks match an Atmos component
rain ls --changesetchangeset list
rain logs <stack>logs--chart renders a per-resource timeline in the terminal; --follow streams
rain watch <stack>watch
rain tree <template>treeDifferent operation: Rain graphed a local template, Atmos shows deployed nested stacks
rain rm <stack>deletePrompts; respects termination protection
rain rm --changesetchangeset delete
rain bootstrapbackend create
rain pkgHook stepsSee Uploaded Assets
rain stackset deploystackset create / stackset updateAccounts and regions come from a kind: aws/stackset target; delegated admin is not supported
rain stackset lsstackset instances
rain stackset rmstackset delete
rain consoleatmos auth console
rain infoatmos auth whoami, atmos describe config
rain forecast, rain build, rain merge, rain module, rain ccNo equivalentKeep a build step that emits plain CloudFormation
(none)output, drift detect, validateNew verbs Rain never had

Migration Steps​

The examples use components network and app, Atmos stacks dev and staging, and CloudFormation stacks acme-plat-<stage>-network and acme-plat-<stage>-app. Replace them with your names. Where a step references an existing deployment, match its stack name, account, region, and parameter values, because a different name or region selects a different stack.

Step 1: Install Atmos and Authenticate​

brew install atmos

See Installation for other platforms. Configure an Atmos identity for the account, or keep using the AWS SDK credential chain (AWS_PROFILE in the shell works). Every command below accepts --identity=<name> when more than one identity is configured.

Step 2: Create a Minimal atmos.yaml​

atmos.yaml
base_path: .

components:
"aws/cloudformation":
base_path: components/cloudformation

stacks:
base_path: stacks
included_paths:
- "deploy/**/*"
name_template: "{{ .vars.stage }}"

templates:
settings:
enabled: true

Step 3: Resolve Rain Directives​

Skip this step for plain CloudFormation templates. For each directive, pick its replacement from Rain Directives: a CloudFormation parameter fed from stack configuration, an included template with | eval, or a packaging hook. Any command that reads the template (render, validate, plan, apply) lists the directives that remain; --dry-run only checks the stack configuration and never opens the template file.

Step 4: Split Shared Config from Environment Config​

Rain repeated the same rain deploy per environment with a different --config file. In Atmos the shared part becomes an abstract base in a catalog, the per-environment part is each environment's stack manifest, and anything shared by a region or account becomes a mixin.

stacks/catalog/network.yaml
components:
"aws/cloudformation":
network:
metadata:
type: abstract
path: template.yaml
stack_name: "acme-plat-{{ .vars.stage }}-network"
parameters:
Stage: "{{ .vars.stage }}"
stacks/catalog/app.yaml
components:
"aws/cloudformation":
app:
metadata:
type: abstract
path: template.yaml
stack_name: "acme-plat-{{ .vars.stage }}-app"
capabilities:
- CAPABILITY_IAM
# deploy.sh ordered network before app by hand and copied its output into the
# app's parameters. Both become declarations.
dependencies:
components:
- name: network
parameters:
Stage: "{{ .vars.stage }}"
SharedBucket: !aws.cloudformation.output network SharedBucket
stacks/mixins/region/us-east-1.yaml
components:
"aws/cloudformation":
network:
settings:
aws_cloudformation:
region: us-east-1
app:
settings:
aws_cloudformation:
region: us-east-1
stacks/deploy/dev.yaml
import:
- mixins/region/us-east-1
- catalog/network
- catalog/app

vars:
stage: dev

components:
"aws/cloudformation":
network:
metadata:
type: real
tags: !include ../../params/dev.yaml .Tags
app:
metadata:
type: real
parameters: !include ../../params/dev.yaml .Parameters
tags: !include ../../params/dev.yaml .Tags

The environment stack includes the Rain config file for that environment, and its Parameters map deep-merges with the catalog's parameters, so Stage and SharedBucket come from the base and DeployOwner comes from the file. Keep the per-environment !include in the environment stack: an !include path is resolved when the manifest loads, before Go templates render, so !include ../../params/{{ .vars.stage }}.yaml in a catalog does not work. The staging stack is the same file with stage: staging and params/staging.yaml.

Add termination_protection, role_arn, stack_policy, or notification_arns to the base when the existing deployment uses them. See the CloudFormation component reference for every field.

Step 5: Preview, Apply, Verify​

Check the stack configuration without touching AWS, then render the template once so any remaining Rain directive is reported before a real changeset is attempted:

atmos aws cloudformation plan app -s dev --dry-run
atmos aws cloudformation render app -s dev

Then preview against the real stacks. Atmos creates a changeset per component, renders it, and removes it, in dependency order:

atmos aws cloudformation plan --all -s dev
Changes for acme-plat-dev-network: 0 resources
acme-plat-dev-app: no changes (changeset would be a no-op)

For an existing stack, no changes is the result you want. Review any replacement or deletion before you apply. This single command replaces the whole deploy.sh loop: network deploys first, the SharedBucket output is read from the deployed stack and passed to app, and delete --all later runs in reverse order:

atmos aws cloudformation deploy --all -s dev
atmos aws cloudformation output app -s dev
INFO Processing components component_type=aws/cloudformation order=dependency count=2
...
Output Value
SharedBucketUsed acme-net-dev-shared

Use apply instead of deploy to review and confirm each changeset interactively. In a non-interactive shell without --auto-approve, apply stops with an error before changing anything. Repeat for staging by pointing at its stack; the catalog and mixin are shared.

Migration Checklist​

  • Install Atmos and configure credentials for the account
  • Match each existing stack's name, account, region, and parameter values
  • Resolve every Rain directive, or let the local error list the ones that remain
  • Reuse parameter files and Rain config sections with !include
  • Declare the artifact bucket as a provision target and create it, or enable automatic provisioning
  • Move rain pkg uploads into an archive + publish hook subscribed to the apply and changeset-create events
  • Run a dry run, then a real plan, and expect no changes for an existing stack
  • Apply, then verify outputs and tags
  • Update scripts for the new verbs, flags, and confirmation behavior
  • Move shared config into catalog bases and mixins; keep each environment's Rain config !include in its own stack
  • Replace hand-ordered deploy loops with dependencies.components and !aws.cloudformation.output, then deploy --all

What Atmos Won't Do​

  • Preprocess !Rain:: directives or Rain modules. A template that still has them fails locally with the replacement named for each.
  • Upload assets from inside a template. Uploads happen in hook steps, and YAML functions never change infrastructure.
  • Reproduce rain forecast, rain build, rain merge, or Cloud Control passthrough.
  • Compare two local templates the way rain diff did, or graph a local template the way rain tree did. The Atmos verbs of the same name work against the deployed stack.
  • Deploy StackSets with delegated administration.

Common Questions​

Next Steps​