Skip to content

Commit

Permalink
Merge pull request #2 from nirdosh17/fixes-1
Browse files Browse the repository at this point in the history
Validation and Assume Role fixes
  • Loading branch information
nirdosh17 committed Aug 20, 2021
2 parents cbf0552 + 495c502 commit e4a47bc
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 46 deletions.
33 changes: 13 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


---

Expand Down Expand Up @@ -84,19 +83,14 @@ Configuration for this command can be set in three different ways in the precede
```
</details>

See Available configurations via:
See available configurations via: `cfn-teardown <command> --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
<details>
<summary><b>It looks something like this:</b></summary>

Expand Down Expand Up @@ -137,31 +131,30 @@ cfn-teardown deleteStacks --help
```
</details>

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
Expand Down
33 changes: 21 additions & 12 deletions utils/cloudformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
37 changes: 23 additions & 14 deletions utils/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit e4a47bc

Please sign in to comment.