Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
702 changes: 702 additions & 0 deletions docs/fixes/nested-terraform-state-auth-context-propagation.md

Large diffs are not rendered by default.

324 changes: 324 additions & 0 deletions docs/terraform-output-yaml-func-authentication-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,189 @@ execTerraformOutput()
└─ tf.Output() - Returns output values
```

## Nested Function Authentication

### What Are Nested Functions?

Nested functions occur when a component's configuration contains `!terraform.output` (or `!terraform.state`) functions that reference other components, which themselves also contain `!terraform.output` or `!terraform.state` functions in their configurations.

**Example:**

```yaml
# Component 1: api-gateway (being deployed)
components:
terraform:
api-gateway:
vars:
backends:
# Level 1: This references backend-service
- endpoint: !terraform.output backend-service alb_dns_name

# Component 2: backend-service (referenced by api-gateway)
components:
terraform:
backend-service:
vars:
# Level 2: This also has !terraform.output functions
vpc_id: !terraform.output vpc vpc_id
subnet_ids: !terraform.output vpc private_subnet_ids
```

When Atmos evaluates the Level 1 function, it needs to read the `backend-service` component configuration to execute `terraform output`. When that configuration is processed, it encounters the Level 2 functions, which must also be evaluated with proper authentication.

### How Authentication Propagates

Atmos propagates authentication through nested function evaluations using the `AuthManager`:

```text
Level 1: atmos terraform apply api-gateway -s production
├─ Creates AuthManager with prod-deploy identity
├─ Stores AuthManager in configAndStacksInfo.AuthManager
├─ Populates configAndStacksInfo.AuthContext from AuthManager
└─ Evaluates component configuration
Level 2: !terraform.output backend-service alb_dns_name
├─ Extracts authContext and authManager from stackInfo
├─ Calls GetTerraformOutput(authContext, authManager)
└─ ExecuteDescribeComponent(AuthManager: authManager) ✅ Passes AuthManager!
Level 3: Processing backend-service component config
├─ AuthManager propagated from Level 2
├─ Populates stackInfo.AuthContext from AuthManager
└─ Evaluates nested !terraform.output functions
Level 4: !terraform.output vpc vpc_id
├─ Extracts authContext from stackInfo ✅ AuthContext available!
├─ Converts AuthContext to environment variables
├─ Executes terraform output with authenticated credentials
└─ Successfully retrieves output value
```

**Key Points:**

1. **AuthManager is stored** in `configAndStacksInfo.AuthManager` at the top level
2. **AuthManager is passed** through `GetTerraformOutput()` to `ExecuteDescribeComponent()`
3. **AuthContext is populated** at each level from the AuthManager
4. **All nested levels** use the same authenticated session

This ensures that deeply nested component configurations can execute `terraform output` with proper credentials without requiring separate authentication at each level.

### Common Nested Function Scenarios

#### Scenario 1: Microservices Architecture

```yaml
# API Gateway aggregates endpoints from multiple services
api-gateway:
vars:
service_endpoints:
auth: !terraform.output auth-service endpoint_url
users: !terraform.output users-service endpoint_url
orders: !terraform.output orders-service endpoint_url
```

Each service component may have `!terraform.output` functions reading VPC, database, or cache configurations.

#### Scenario 2: Infrastructure Layering

```yaml
# Application tier references platform tier
app-component:
vars:
database_url: !terraform.output database connection_string
cache_endpoint: !terraform.output redis endpoint

# Platform tier references network tier
database:
vars:
subnet_ids: !terraform.output vpc database_subnet_ids
security_group_id: !terraform.output vpc database_sg_id
```

The `app-component` evaluation triggers `database` evaluation, which triggers `vpc` evaluation.

#### Scenario 3: Multi-Region Deployments

```yaml
# Global load balancer references regional backends
global-lb:
vars:
backends:
- endpoint: !terraform.output app us-east-1 lb_dns_name
- endpoint: !terraform.output app us-west-2 lb_dns_name
- endpoint: !terraform.output app eu-west-1 lb_dns_name
```

Each regional `app` component may have nested `!terraform.output` functions for regional resources.

### Authentication Behavior for Nested Functions

All nested function evaluations inherit the same AuthManager and credentials from the top-level command execution. This means:

- **Single authentication session** - All nested components use the same authenticated identity
- **Consistent credentials** - Environment variables set once and used by all nested terraform executions
- **Transitive permissions** - The identity must have access to all resources across nested components

If different components require different credentials, use component-level auth configuration to specify different default identities. Note that during a single command execution, all nested evaluations will use the top-level credentials.

### Performance Considerations

When using nested `!terraform.output` functions:

- Each `!terraform.output` spawns a terraform process (init + output)
- Consider using `!terraform.state` when possible (faster, no subprocess)
- Cache results are shared across nested evaluations
- Deep nesting (Level 4+) can impact performance
- Consider flattening dependencies for complex scenarios

### Best Practices for Nested Functions

1. **Prefer !terraform.state for Speed**
- Use `!terraform.state` instead of `!terraform.output` when possible
- Faster execution (no subprocess, no terraform init)
- Same authentication propagation behavior

2. **Configure Adequate Permissions**
- Ensure the identity has access to all resources needed across nested components
- Include transitive dependencies (if A calls B, and B calls C, identity needs permissions for all)

3. **Test Nested Configurations**
- Use `atmos describe component` to test nested function resolution
- Enable debug logging to verify authentication at each level:
```bash
atmos terraform plan component -s stack --logs-level=Debug
```

4. **Document Dependencies**
- Document which components reference others via `!terraform.output`
- Use `settings.depends_on` to declare explicit dependencies
- This helps with deployment ordering and troubleshooting

5. **Cache Optimization**
- Nested function results are cached to avoid redundant terraform executions
- Cache is per-component per-stack combination
- Use `skipCache: false` (default) for better performance

### Mixing !terraform.state and !terraform.output

You can mix both function types in nested scenarios:

```yaml
# Component using both function types
my-component:
vars:
# Fast state read (no subprocess)
vpc_id: !terraform.state vpc vpc_id

# Formatted output (uses terraform binary)
formatted_config: !terraform.output config-service json_config
```

**Authentication works the same way** for both function types when nested:
- Both receive `AuthManager` from parent context
- Both populate `AuthContext` for their own nested calls
- Mixed nesting works seamlessly (state can call output, output can call state)

## Identity Resolution

### Explicit Identity
Expand Down Expand Up @@ -217,6 +400,147 @@ components:
default: true
```

### Component-Level Auth Override in Nested Functions

When evaluating nested `!terraform.output` functions, Atmos checks each component's configuration for an `auth:` section at every nesting level:

1. **Component has `auth:` section** → Merges with global auth and creates component-specific AuthManager
2. **Component has no `auth:` section** → Inherits parent's AuthManager

This enables each nesting level to optionally override authentication while defaulting to the parent's credentials.

#### Example: Multi-Account Nested Functions

```yaml
# Global auth configuration
auth:
identities:
dev-account:
default: true
kind: aws/permission-set
via:
provider: aws-sso
account: "111111111111"
permission_set: "DevAccess"

# Component 1: Uses global auth (dev account)
components:
terraform:
api-gateway:
vars:
# Reads from backend-service which is in a different account
backend_url: !terraform.output backend-service endpoint

# Component 2: Overrides auth for prod account access
components:
terraform:
backend-service:
auth:
identities:
prod-account:
default: true
kind: aws/permission-set
via:
provider: aws-sso
account: "222222222222"
permission_set: "ProdReadOnly"
vars:
# This component's outputs are in prod account
database_url: !terraform.output database connection_string
```

**Authentication flow:**
1. `api-gateway` evaluated with dev-account credentials (global default)
2. Encounters `!terraform.output backend-service` → checks for `auth:` section
3. Finds `auth:` section in `backend-service` → creates new AuthManager with prod-account credentials
4. `backend-service` config evaluated with prod-account credentials
5. Nested `!terraform.output database` inherits prod-account credentials from parent

#### Component-Level Auth Resolution Algorithm

**File:** `internal/exec/terraform_nested_auth_helper.go`

The `resolveAuthManagerForNestedComponent()` function implements this logic:

```go
func resolveAuthManagerForNestedComponent(
atmosConfig *schema.AtmosConfiguration,
component string,
stack string,
parentAuthManager auth.AuthManager,
) (auth.AuthManager, error) {
// 1. Get component config WITHOUT processing templates/functions
// (avoids circular dependency)
componentConfig, err := ExecuteDescribeComponent(&ExecuteDescribeComponentParams{
Component: component,
Stack: stack,
ProcessTemplates: false,
ProcessYamlFunctions: false,
AuthManager: nil,
})

// 2. Check for auth section in component config
authSection, hasAuthSection := componentConfig[cfg.AuthSectionName].(map[string]any)
if !hasAuthSection || authSection == nil {
// No auth config → inherit parent
return parentAuthManager, nil
}

// 3. Merge component auth with global auth
mergedAuthConfig, err := auth.MergeComponentAuthFromConfig(
&atmosConfig.Auth,
componentConfig,
atmosConfig,
cfg.AuthSectionName,
)

// 4. Create and authenticate new AuthManager with merged config
componentAuthManager, err := auth.CreateAndAuthenticateManager(
"",
mergedAuthConfig,
cfg.IdentityFlagSelectValue,
)

return componentAuthManager, nil
}
```

#### Best Practices for Component-Level Auth in Nested Functions

1. **Minimize Auth Overrides**
- Use component-level auth only when necessary
- Prefer global defaults for simplicity
- Document why specific components need different credentials

2. **Test Cross-Account Access**
- Verify IAM permissions for cross-account output reads
- Test nested function resolution with component-level auth
- Use `atmos describe component` to verify auth resolution

3. **Cache Considerations**
- Each component-specific AuthManager creates a new authentication session
- Output reads are still cached per-component to avoid redundant terraform executions
- Authentication is performed once per unique component auth configuration

4. **Debug Component-Level Auth**
```bash
# Enable debug logging to see auth resolution
ATMOS_LOGS_LEVEL=Debug atmos describe component my-component -s stack
```

Look for log lines like:
```
Component has auth config, creating component-specific AuthManager
Created component-specific AuthManager identityChain=[prod-account]
```

#### Limitations and Considerations

- **Single top-level identity** - When using `--identity` flag, it applies at the top level only
- **Nested overrides respected** - Component-level auth in nested components is still respected
- **No re-prompting** - Interactive identity selection happens once at the top level
- **Transitive permissions** - Top-level identity must have permissions for initial component, nested components use their own auth

## How Credentials Flow

### 1. AuthManager Creation
Expand Down
Loading
Loading