Skip to main content

Custom Commands

Atmos can be easily extended to support any number of custom CLI commands. Custom commands are exposed through the atmos CLI when you run atmos help. It's a great way to centralize the way operational tools are run in order to improve DX.

For example, one great way to use custom commands is to tie all the miscellaneous scripts into one consistent CLI interface. Then we can kiss those ugly, inconsistent arguments to bash scripts goodbye! Just wire up the commands in atmos to call the script. Then developers can just run atmos help and discover all available commands.

Simple Example​

Here is an example to play around with to get started.

Adding the following to atmos.yaml will introduce a new hello command.

# Custom CLI commands
commands:
- name: hello
description: This command says Hello world
steps:
- "echo Hello world!"

We can run this example like this:

atmos hello

Positional Arguments​

Atmos also supports positional arguments. If a positional argument is required but not provided by the user, the command will failβ€”unless you define a default in your config.

For the example, adding the following to atmos.yaml will introduce a new greet command that accepts one name argument, but uses a default of "John Doe" if none is provided.

# subcommands
commands:
- name: greet
description: This command says hello to the provided name
arguments:
- name: name
description: Name to greet
required: true
default: John Doe
steps:
- "echo Hello {{ .Arguments.name }}!"

We can run this example like this:

atmos greet Alice

or defaulting to "John Doe"

atmos greet

Trailing Arguments​

Atmos supports trailing arguments after -- (a standalone double-dash). The -- itself is a delimiter that signals the end of Atmos-specific options. Anything after -- is passed directly to the underlying command without being interpreted by Atmos. The value of these trailing arguments is accessible in {{ .TrailingArgs }}.

For the example, adding the following to atmos.yaml will introduce a new echo command that accepts one name argument and also uses trailingArgs

- name: ansible run
description: "Runs an Ansible playbook, allowing extra arguments after --."
arguments:
- name: playbook
description: "The Ansible playbook to run"
default: site.yml
required: true
steps:
- "ansible-playbook {{ .Arguments.playbook }} {{ .TrailingArgs }}"

Output:

$ atmos ansible run -- --limit web
Running: ansible-playbook site.yml --limit web

PLAY [web] *********************************************************************

Passing Flags​

Passing flags works much like passing positional arguments, except for that they are passed using long or short flags. Flags can be optional (this is configured by setting the required attribute to false).

# subcommands
commands:
- name: hello
description: This command says hello to the provided name
flags:
- name: name
shorthand: n
description: Name to greet
required: true
steps:
- "echo Hello {{ .Flags.name }}!"

We can run this example like this, using the long flag:

atmos hello --name world

Or, using the shorthand, we can just write:

atmos hello -n world

Boolean Flags​

Flags can be defined as boolean type using type: bool. Boolean flags don't require a value to be passedβ€”when present, they are set to true.

commands:
- name: deploy
description: Deploy to environment
flags:
- name: dry-run
shorthand: d
description: Perform a dry run without making changes
type: bool
- name: verbose
shorthand: v
description: Enable verbose output
type: bool
default: false
- name: auto-approve
description: Auto-approve without prompting
type: bool
default: true
steps:
- |
{{ if .Flags.dry-run }}
echo "DRY RUN MODE"
{{ end }}
{{ if .Flags.verbose }}
echo "Verbose output enabled"
{{ end }}
{{ if .Flags.auto-approve }}
terraform apply -auto-approve
{{ else }}
terraform apply
{{ end }}

Usage:

# Enable dry-run (sets it to true)
atmos deploy --dry-run

# Use short flag
atmos deploy -d

# Boolean with explicit value
atmos deploy --auto-approve=false

Using Boolean Flags in Steps​

Boolean flags are available as Go template variables with values true or false. Here are common patterns for using them in bash:

commands:
- name: build
description: Build the project
flags:
- name: verbose
shorthand: v
type: bool
description: Enable verbose output
- name: clean
type: bool
default: true
description: Clean before building
steps:
# Pattern 1: Conditional command execution with if/else
- |
{{ if .Flags.verbose }}
echo "Verbose mode enabled"
set -x
{{ end }}

# Pattern 2: Inline conditional flag
- echo "Building{{ if .Flags.verbose }} with verbose output{{ end }}..."

# Pattern 3: Pass as flag to another command
- make build {{ if .Flags.verbose }}VERBOSE=1{{ end }}

# Pattern 4: Conditional step execution
- |
{{ if .Flags.clean }}
echo "Cleaning build directory..."
rm -rf ./build
{{ end }}

# Pattern 5: Negation check
- |
{{ if not .Flags.clean }}
echo "Skipping clean step"
{{ end }}

# Pattern 6: Convert to shell variable
- |
VERBOSE={{ .Flags.verbose }}
if [ "$VERBOSE" = "true" ]; then
echo "Verbose is on"
fi

# Pattern 7: Using printf for explicit string conversion
- |
VERBOSE={{ printf "%t" .Flags.verbose }}
echo "Verbose flag is: $VERBOSE"
tip

Boolean values automatically render as true or false (lowercase strings) when used in templates. You can reference them directly with {{ .Flags.name }}β€”no conversion needed!

Flag Defaults​

Both string and boolean flags support default values using the default attribute:

flags:
- name: environment
description: Target environment
default: "development"
- name: force
type: bool
description: Force the operation
default: false
- name: auto-approve
type: bool
description: Skip confirmation prompts
default: true

When a flag has a default value, users can omit it from the command line. The default value will be used unless explicitly overridden.

Advanced Examples​

Define a New Terraform Command​

# Custom CLI commands
commands:
- name: terraform
description: Execute 'terraform' commands
# subcommands
commands:
- name: provision
description: This command provisions terraform components
arguments:
- name: component
description: Name of the component
flags:
- name: stack
shorthand: s
description: Name of the stack
required: true
# ENV var values support Go templates
env:
- key: ATMOS_COMPONENT
value: "{{ .Arguments.component }}"
- key: ATMOS_STACK
value: "{{ .Flags.stack }}"
steps:
- atmos terraform plan $ATMOS_COMPONENT -s $ATMOS_STACK
- atmos terraform apply $ATMOS_COMPONENT -s $ATMOS_STACK

Override an Existing Terraform Command​

# Custom CLI commands
commands:
- name: terraform
description: Execute 'terraform' commands
# subcommands
commands:
- name: apply
description: This command executes 'terraform apply -auto-approve' on terraform components
arguments:
- name: component
description: Name of the component
flags:
- name: stack
shorthand: s
description: Name of the stack
required: true
steps:
- atmos terraform apply {{ .Arguments.component }} -s {{ .Flags.stack }} -auto-approve

Show Component Info​

# Custom CLI commands
commands:
- name: show
description: Execute 'show' commands
# subcommands
commands:
- name: component
description: Execute 'show component' command
arguments:
- name: component
description: Name of the component
flags:
- name: stack
shorthand: s
description: Name of the stack
required: true
# ENV var values support Go templates and have access to {{ .ComponentConfig.xxx.yyy.zzz }} Go template variables
env:
- key: ATMOS_COMPONENT
value: "{{ .Arguments.component }}"
- key: ATMOS_STACK
value: "{{ .Flags.stack }}"
- key: ATMOS_TENANT
value: "{{ .ComponentConfig.vars.tenant }}"
- key: ATMOS_STAGE
value: "{{ .ComponentConfig.vars.stage }}"
- key: ATMOS_ENVIRONMENT
value: "{{ .ComponentConfig.vars.environment }}"
# If a custom command defines 'component_config' section with 'component' and 'stack', 'atmos' generates the config for the component in the stack
# and makes it available in {{ .ComponentConfig.xxx.yyy.zzz }} Go template variables,
# exposing all the component sections (which are also shown by 'atmos describe component' command)
component_config:
component: "{{ .Arguments.component }}"
stack: "{{ .Flags.stack }}"
# Steps support using Go templates and can access all configuration settings (e.g. {{ .ComponentConfig.xxx.yyy.zzz }})
# Steps also have access to the ENV vars defined in the 'env' section of the 'command'
steps:
- 'echo Atmos component from argument: "{{ .Arguments.component }}"'
- 'echo ATMOS_COMPONENT: "$ATMOS_COMPONENT"'
- 'echo Atmos stack: "{{ .Flags.stack }}"'
- 'echo Terraform component: "{{ .ComponentConfig.component }}"'
- 'echo Backend S3 bucket: "{{ .ComponentConfig.backend.bucket }}"'
- 'echo Terraform workspace: "{{ .ComponentConfig.workspace }}"'
- 'echo Namespace: "{{ .ComponentConfig.vars.namespace }}"'
- 'echo Tenant: "{{ .ComponentConfig.vars.tenant }}"'
- 'echo Environment: "{{ .ComponentConfig.vars.environment }}"'
- 'echo Stage: "{{ .ComponentConfig.vars.stage }}"'
- 'echo Dependencies: "{{ .ComponentConfig.deps }}"'

Set EKS Cluster​

# Custom CLI commands
commands:
- name: set-eks-cluster
description: |
Download 'kubeconfig' and set EKS cluster.

Example usage:
atmos set-eks-cluster eks/cluster -s plat-ue1-dev -r admin
atmos set-eks-cluster eks/cluster -s plat-uw2-prod --role reader
verbose: false # Set to `true` to see verbose outputs
arguments:
- name: component
description: Name of the component
flags:
- name: stack
shorthand: s
description: Name of the stack
required: true
- name: role
shorthand: r
description: IAM role to use
required: true
# If a custom command defines 'component_config' section with 'component' and 'stack',
# Atmos generates the config for the component in the stack
# and makes it available in {{ .ComponentConfig.xxx.yyy.zzz }} Go template variables,
# exposing all the component sections (which are also shown by 'atmos describe component' command)
component_config:
component: "{{ .Arguments.component }}"
stack: "{{ .Flags.stack }}"
env:
- key: KUBECONFIG
value: /dev/shm/kubecfg.{{ .Flags.stack }}-{{ .Flags.role }}
steps:
- >
aws
--profile {{ .ComponentConfig.vars.namespace }}-{{ .ComponentConfig.vars.tenant }}-gbl-{{ .ComponentConfig.vars.stage }}-{{ .Flags.role }}
--region {{ .ComponentConfig.vars.region }}
eks update-kubeconfig
--name={{ .ComponentConfig.vars.namespace }}-{{ .Flags.stack }}-eks-cluster
--kubeconfig="${KUBECONFIG}"
> /dev/null
- chmod 600 ${KUBECONFIG}
- echo ${KUBECONFIG}

List Stacks and Components​

# Custom CLI commands
commands:
- name: list
description: Execute 'atmos list' commands
# subcommands
commands:
- name: stacks
description: |
List all Atmos stacks.
steps:
- >
atmos describe stacks --process-templates=false --sections none | grep -e "^\S" | sed s/://g
- name: components
description: |
List all Atmos components in all stacks or in a single stack.

Example usage:
atmos list components
atmos list components -s tenant1-ue1-dev
atmos list components --stack tenant2-uw2-prod
flags:
- name: stack
shorthand: s
description: Name of the stack
required: false
steps:
- >
{{ if .Flags.stack }}
atmos describe stacks --stack {{ .Flags.stack }} --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]"
{{ else }}
atmos describe stacks --format json --sections none | jq ".[].components.terraform" | jq -s add | jq -r "keys[]"
{{ end }}

Working Directory​

Custom commands can specify a working_directory field to control where the command steps execute. This is useful when commands need to run from a specific location, regardless of where atmos was invoked.

Path Resolution​

  • Absolute paths are used as-is (e.g., /tmp, /home/user/scripts)
  • Relative paths are resolved against the Atmos base_path
  • The !repo-root YAML function can be used to reference the git repository root

Example: Run from Repository Root​

commands:
- name: build
description: Build the project from repository root
working_directory: !repo-root .
steps:
- make build
- make test

This ensures the build commands run from the repository root, even if you invoke atmos build from a subdirectory.

Example: Run in Temp Directory​

commands:
- name: download-tools
description: Download and extract tools in /tmp
working_directory: /tmp
steps:
- wget https://example.com/tools.tar.gz
- tar -xzf tools.tar.gz

Example: Run in Component Directory​

commands:
- name: component-init
description: Initialize a component
working_directory: components/terraform/vpc
steps:
- terraform init
- terraform validate

Since components/terraform/vpc is a relative path, it will be resolved against base_path.

tip

Use working_directory: !repo-root . when defining commands in .atmos.d/ at the repository root. This ensures commands work correctly when invoked from any subdirectory in your project.

Using Authentication with Custom Commands​

Custom commands can specify an identity field to authenticate before execution. This is useful when commands need to interact with cloud resources that require specific credentials or elevated permissions.

When an identity is specified, Atmos will:

  1. Authenticate using the specified identity (prompting for MFA if required)
  2. Write temporary credentials to a file
  3. Set environment variables pointing to the credential files (AWS_SHARED_CREDENTIALS_FILE, AWS_CONFIG_FILE, AWS_PROFILE, etc.)
  4. Execute all command steps with these environment variables

Example: Custom Command with Authentication​

commands:
- name: deploy-infra
description: Deploy infrastructure with superadmin privileges
identity: superadmin # Authenticate as superadmin before running
arguments:
- name: component
description: Component to deploy
required: true
flags:
- name: stack
shorthand: s
description: Stack to deploy to
required: true
steps:
- atmos terraform plan {{ .Arguments.component }} -s {{ .Flags.stack }}
- atmos terraform apply {{ .Arguments.component }} -s {{ .Flags.stack }} -auto-approve

To execute this command:

atmos deploy-infra vpc -s plat-ue2-prod

Atmos will:

  1. Authenticate as superadmin (prompting for MFA if configured)
  2. Set up environment variables pointing to temporary credential files
  3. Execute both terraform plan and terraform apply with those credentials

Example: Multi-Step Command with Authentication​

commands:
- name: audit-resources
description: Audit cloud resources with auditor credentials
identity: auditor
flags:
- name: region
shorthand: r
description: AWS region
required: true
steps:
- |
echo "Running audit in {{ .Flags.region }}..."
aws sts get-caller-identity
- |
aws ec2 describe-instances --region {{ .Flags.region }} \
--query 'Reservations[].Instances[].[InstanceId,State.Name,Tags[?Key==`Name`].Value|[0]]' \
--output table
- |
aws s3 ls --region {{ .Flags.region }}

All steps in this command will execute with the auditor identity credentials.

Authentication with Component Config​

You can combine identity authentication with component configuration:

commands:
- name: provision-with-auth
description: Provision component with specific identity
identity: infrastructure-admin
arguments:
- name: component
description: Component to provision
flags:
- name: stack
shorthand: s
description: Stack name
required: true
component_config:
component: "{{ .Arguments.component }}"
stack: "{{ .Flags.stack }}"
env:
- key: COMPONENT_REGION
value: "{{ .ComponentConfig.vars.region }}"
steps:
- |
echo "Provisioning {{ .Arguments.component }} in {{ .ComponentConfig.vars.region }}"
echo "Using identity: infrastructure-admin"
aws sts get-caller-identity
- atmos terraform apply {{ .Arguments.component }} -s {{ .Flags.stack }}
tip

Configure identities in your atmos.yaml under the auth section. See the Authentication documentation for configuration details.

Overriding Identity at Runtime​

You can override the identity specified in the command configuration using the --identity flag:

# Use the identity from command config
atmos deploy-infra vpc -s plat-ue2-prod

# Override with a different identity
atmos deploy-infra vpc -s plat-ue2-prod --identity developer

# Use no identity (skip authentication even if configured)
atmos deploy-infra vpc -s plat-ue2-prod --identity ""

The --identity flag is automatically added to all custom commands and takes precedence over the identity field in the command configuration.

note

The identity field applies to all steps in the custom command. If you need different identities for different operations, consider using separate custom commands or workflows with per-step identity configuration.