diff --git a/README.md b/README.md index 4eb15ca..0abcb43 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,13 @@ CFN Teardown is a tool to delete matching CloudFormation stacks respecting stack - Stack name pattern matching for deletion. -- Generates stack dependencies in a file from which shows how loosely or tighly coupled the stacks are. +- Generates stack dependencies in a file from which shows how loosely or tighly coupled the stacks are. - Builds dependency tree for intelligent/faster teardown. - Multiple safety checks to prevent accidental deletion. - Supports slack notification for deletion status updates via webhook. - --- @@ -84,19 +83,14 @@ Configuration for this command can be set in three different ways in the precede ``` -See Available configurations via: +See available configurations via: `cfn-teardown --help` -```bash -cfn-teardown --help -cfn-teardown listDependencies --help -cfn-teardown deleteStacks --help -``` -### Algorithm +### Stack Teardown Strategy -1. Finds matching stacks based on the regex provided +1. Find matching stacks based on the regex provided -2. Prepares stack dependencies +2. Prepare stack dependencies
It looks something like this: @@ -137,31 +131,30 @@ cfn-teardown deleteStacks --help ```
-3. Alerts slack channel(if provided) and waits before initiating deletion. Starts deletion immediately if no wait time is provided. +3. Alert slack channel(if provided) and waits before initiating deletion. Starts deletion immediately if no wait time is provided. + +4. Select stacks which are eligible for deletion. A stack is eligible for deletion if it's exports are imported by no other stacks. In simple terms, it should have no dependencies. -4. Selects stacks which are eligible for deletion. A stack is eligible for deletion if it's exports are imported by no other stacks. In simple terms, it should have no dependencies. +5. Send delete requests for all selected stacks. -5. Initiates delete requests concurrently for all selected stacks. +6. Wait for 30 seconds(configurable) before scanning eligible stacks again. Checks If the stack has been already deleted and if deleted updates stack status in the dependency tree. -6. Waits for 30 seconds(configurable) before scanning eligible stacks again. Checks If the stack has been already deleted and if deleted updates stack status in the dependency tree. - 7. This process (sending delete requests, waiting, checking stack status) is repeated until all stacks have status `DELETE_COMPLETE`. - + 8. If a stack is not deleted even after exhausting all retries(default 5), teardown is halted and manual intervention is requested. - + ### Assume Role By default it tries to use the IAM role of environment it is currently running in. But we can also supply role arn if we want the script to assume a different role. - ### Safety Checks for Accidental Deletion - `DRY_RUN` flag must be explicitely set to `false` to activate delete functionality - `ABORT_WAIT_TIME_MINUTES` flag lets us to decide how much to wait before initiating delete as you might want to confirm the stacks that are about to get deleted -- `TARGET_ACCOUNT_ID` flag will check the supplied account id with aws session account id during runtime to confirm that we are deleting stacks in the desired aws account +- `TARGET_ACCOUNT_ID`: If provided, this flag confirms that the given aws account id matches with account id in the aws session during runtime to make sure that we are deleting stacks in the desired aws account ### Limitation diff --git a/utils/cloudformation.go b/utils/cloudformation.go index 97e685b..cb65d2d 100644 --- a/utils/cloudformation.go +++ b/utils/cloudformation.go @@ -245,13 +245,25 @@ func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) { Profile: dm.AWSProfile, })) - desiredAccount, err := dm.IsDesiredAWSAccount(sess) - if err != nil { - return nil, err + // validation for target account id + if dm.TargetAccountId != "" { + aID, err := dm.AWSSessionAccountID(sess) + if err != nil { + fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) + return nil, err + } + + if aID != dm.TargetAccountId { + return nil, fmt.Errorf( + "[CFN] Target account id (%v) did not match with account id (%v) in the current AWS session", + dm.TargetAccountId, + aID, + ) + } } - // to make things easy while running this script locally - if desiredAccount { + if dm.NukeRoleARN == "" { + // this means, we are using given aws profile return cloudformation.New(sess), nil } else { // Create the credentials from AssumeRoleProvider if nuke role arn is provided @@ -261,16 +273,13 @@ func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) { } } -func (dm CFNManager) IsDesiredAWSAccount(sess *session.Session) (bool, error) { +func (dm CFNManager) AWSSessionAccountID(sess *session.Session) (acID string, err error) { svc := sts.New(sess) result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) if err != nil { fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) - return false, err - } - - if *result.Account == dm.TargetAccountId { - return true, err + return } - return false, err + acID = *result.Account + return } diff --git a/utils/s3.go b/utils/s3.go index fba89d7..ac469cf 100644 --- a/utils/s3.go +++ b/utils/s3.go @@ -74,32 +74,41 @@ func (sm S3Manager) Session() (*s3.S3, error) { Profile: sm.AWSProfile, })) - desiredAccount, err := sm.IsDesiredAWSAccount(sess) - if err != nil { - return nil, err + // validation for target account id + if sm.TargetAccountId != "" { + aID, err := sm.AWSSessionAccountID(sess) + if err != nil { + fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) + return nil, err + } + + if aID != sm.TargetAccountId { + return nil, fmt.Errorf( + "[S3] Target account id (%v) did not match with account id (%v) in the current AWS session", + sm.TargetAccountId, + aID, + ) + } } - // to make things easy while running this script locally - if desiredAccount { - return s3.New(sess), err + if sm.NukeRoleARN == "" { + // this means, we are using given aws profile + return s3.New(sess), nil } else { // Create the credentials from AssumeRoleProvider if nuke role arn is provided creds := stscreds.NewCredentials(sess, sm.NukeRoleARN) // Create service client value configured for credentials from assumed role - return s3.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), err + return s3.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), nil } } -func (sm S3Manager) IsDesiredAWSAccount(sess *session.Session) (bool, error) { +func (sm S3Manager) AWSSessionAccountID(sess *session.Session) (acID string, err error) { svc := sts.New(sess) result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) if err != nil { fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) - return false, err - } - - if *result.Account == sm.TargetAccountId { - return true, err + return } - return false, err + acID = *result.Account + return }