Security Service Modules⚓︎
Overview⚓︎
Starting with LZA v1.16.0, security services like Amazon Macie are managed through modules — direct AWS API orchestration that replaces the previous CloudFormation custom resource approach. Modules provide faster execution, better error handling, and idempotent convergence across your AWS organization.
What changed?⚓︎
| Aspect | Previous (Custom Resources) | Current (Modules) |
|---|---|---|
| Execution | Lambda-backed CloudFormation custom resources | Direct AWS SDK API calls from the pipeline |
| Scope | One stack per account/region | Centralized orchestration across all accounts and regions |
| State | CloudFormation stack state | DynamoDB state table with config change detection |
| Logging | CloudWatch Logs (Lambda function logs) | Pipeline execution logs + optional verbose CloudWatch logging |
| Lifecycle | CloudFormation CREATE/UPDATE/DELETE events | Enable/disable with proper dependency ordering |
What happens to existing custom resources?⚓︎
When a module takes over management of a service, the corresponding CloudFormation custom resources are retained (not deleted). This means:
- The Lambda functions and CloudWatch Log Groups from previous custom resources remain in your accounts
- LZA does not clean up these retained resources — they are inert but still exist
- You can choose to manually clean up the retained Lambda functions and their log groups once you've verified the module is operating correctly
- The CloudWatch Log Groups contain historical execution logs that may be useful for auditing
Note
LZA intentionally does not delete retained resources. This is a safety measure to prevent accidental service disruption during the migration.
Module Execution State⚓︎
Modules track their execution state in a DynamoDB table. On each pipeline run, the module compares the current configuration against the last saved state. If nothing has changed, the module skips execution entirely.
Checking module state⚓︎
Module execution state is stored in a dedicated DynamoDB table: {Prefix}-Module-State-{AccountId}-{Region} (created by the Module Infrastructure stack). The {Prefix} is your configured accelerator prefix for standard deployments, or the qualifier prefix for external deployments. Each module entry contains:
- Configuration hash from the last successful execution
- Execution timestamp
- Result status (COMPLETED, FAILED, SKIPPED)
Forcing re-execution⚓︎
If you need to force a module to re-execute regardless of state (for example, after manual changes to the service):
- Set
overrideExisting: truein the module's configuration section insecurity-config.yaml - Run the pipeline
- Remove
overrideExisting: trueafter successful execution
centralSecurityServices:
macie:
enable: true
overrideExisting: true # Forces module to execute even if config unchanged
Verbose Logging⚓︎
Modules support verbose logging to CloudWatch for detailed troubleshooting. To enable:
- Set the
VERBOSE_LOG_GROUP_NAMEenvironment variable in your pipeline configuration - Module execution details will be written to the specified CloudWatch Log Group
- Logs include API calls, responses, timing, and error details
Troubleshooting Module Failures⚓︎
Reading module execution logs⚓︎
Module execution results appear in the LZA pipeline logs with structured output:
ℹ️ Module macie will be executed...
ℹ️ Checking if macie configuration has changed
ℹ️ Configuration changed for macie
🚀 Starting Macie configuration phase
✅ Macie configuration phase completed
✅ Module execution state saved successfully
✅ Completed module macie
If a module fails:
Common failure scenarios⚓︎
| Error | Cause | Resolution |
|---|---|---|
InvalidInputException: unrecognized service principal |
Service not available in partition (e.g., Macie in GovCloud) | Module auto-skips in unsupported partitions. If you see this on older versions, upgrade LZA. |
State management failure: DynamoDB table not found |
Prepare stack hasn't been deployed or table was deleted | Re-run the pipeline from the Prepare stage |
AccessDeniedException |
Insufficient permissions for cross-account operations | Verify the management account access role exists in all accounts |
TooManyRequestsException |
API rate limiting | Pipeline will retry automatically. If persistent, check for concurrent executions. |
Skipping a Module⚓︎
Use with caution
Skipping a module is an emergency measure for break/fix scenarios. It prevents the module from executing, which means the service will not be configured or updated by LZA. Only use this as a temporary workaround while investigating an issue.
If a module is causing pipeline failures and you need to unblock other deployments, you can skip it using an environment variable in the LZA pipeline:
Environment variable naming convention⚓︎
Where {MODULE_NAME} is the uppercase, underscore-separated module name:
| Module | Environment Variable |
|---|---|
| Amazon Macie | SKIP_MACIE_MODULE=true |
| TGW Associations & Propagations | SKIP_TGW_ASSOCIATIONS_AND_PROPAGATIONS_MODULE=true |
| Stack Resources Retention | SKIP_STACK_RESOURCES_RETENTION_MODULE=true |
How to set the environment variable⚓︎
Add the variable to your LZA CodeBuild/CodePipeline environment configuration. The exact method depends on your deployment:
- CodeBuild: Add to the buildspec environment variables or project configuration
- GitLab CI: Add as a CI/CD variable in project settings
- Manual execution: Export the variable before running the pipeline
What happens when a module is skipped⚓︎
- The module returns a
SKIPPEDstatus in the pipeline logs - No module API calls are made for that service
- The service continues to be managed by the existing CloudFormation custom resources (previous LZA behavior)
- Resource retention is also skipped, so custom resources remain active and functional
- Other modules continue executing normally
- Skipping
STACK_RESOURCES_RETENTIONalso causes dependent modules (like Macie) to skip, since custom resources remain active
Re-enabling a skipped module⚓︎
Remove the environment variable (or set it to false) and re-run the pipeline. The module will detect the configuration state and execute normally.
Partition Support⚓︎
Not all AWS services are available in all partitions (e.g., Amazon Macie is not available in GovCloud). LZA modules automatically detect partition availability and skip gracefully when a service is not supported:
ℹ️ Macie is not available in this partition: You specified an unrecognized service principal.
ℹ️ Skipping module macie as Amazon Macie is not available in this partition.
No action is needed — this is expected behavior in unsupported partitions.
TGW Associations & Propagations Module⚓︎
The TGW module manages Transit Gateway route table associations and propagations via direct API calls, replacing the previous CloudFormation custom resources. It uses ownership-based state tracking to safely coexist with externally-managed resources.
How ownership tracking works⚓︎
The module tracks which associations and propagations it creates. On each run, it only deletes resources that:
- Were previously created or adopted by LZA (recorded in state), AND
- Are no longer declared in
network-config.yaml
Resources created outside LZA (manually, via customization stacks, or other tooling) are never deleted by the module.
Behavior summary⚓︎
| Scenario | Module action |
|---|---|
| Resource in config, not in AWS | Creates it |
| Resource in config, already in AWS | Adopts it (records as owned, no change to AWS) |
| Resource not in config, owned by LZA | Deletes it |
| Resource not in config, NOT owned by LZA | Leaves it alone |
| First run (no previous state) | Creates from config, no deletions |
| Previous run failed | Creates from config, no deletions (create-only mode) |
Upgrading from a previous version⚓︎
On the first run after upgrading to a version with ownership tracking:
- The module operates in create-only mode (no deletions) because it has no historical ownership data
- All resources matching the current config are recorded as owned
- From the second run onwards, config-driven deletions work normally
Tip
On the first post-upgrade run, the module automatically detects the absence of ownership state and forces a seeding execution — even if your config hasn't changed. This is a one-time operation that records the current resources as owned. No manual steps are required.
Coexistence with customization stacks⚓︎
Resources created by customization stacks (which run after the TGW module) are safe:
- They are never recorded in the module's owned state
- The module will never delete them on subsequent runs
- They persist indefinitely alongside LZA-managed resources
Moving an attachment between route tables⚓︎
When you change an attachment's routeTableAssociations in config (e.g., move from core-rt to segregated-rt), the module:
- Disassociates from the current route table
- Associates to the new route table
This applies regardless of how the original association was created. Adding an attachment to network-config.yaml means LZA manages it — including moving it if the config specifies a different route table than the current state.
Recovery after failure⚓︎
If the module fails (e.g., due to API errors or timeouts), it enters create-only mode on the next run:
- No deletions are performed
- Resources from config are created or verified
- On successful completion, the owned state is re-established
- Subsequent runs resume normal operation (including config-driven deletions)
This ensures transient failures never lead to unintended resource deletion.
Important: Do not skip the module after migration⚓︎
Danger
Once the TGW module has executed successfully, do not set SKIP_TGW_ASSOCIATIONS_AND_PROPAGATIONS_MODULE=true. Skipping re-enables CloudFormation custom resources in the template, and CloudFormation will attempt to create associations/propagations that already exist in AWS — causing the deployment to fail. If you need to temporarily disable the module due to a pipeline issue, contact AWS Support for guidance.