Skip to content

Commit

Permalink
Change flag names to full caps.
Browse files Browse the repository at this point in the history
  • Loading branch information
nirdosh17 committed Aug 16, 2021
1 parent 726d006 commit ff5823c
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 110 deletions.
43 changes: 22 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CFN Teardown

CFN Teardown is a tool to delete CloudFormation stacks respecting the stack dependencies.
CFN Teardown is a tool to delete CloudFormation stacks respecting stack dependencies.

If you deploy all of you intrastructure using CloudFormation with a `consistent naming convention` for stacks, then you can use this tool to tear down the environment.

Expand Down Expand Up @@ -42,7 +42,7 @@ go get github.com/nirdosh17/cfn-teardown

## Using CFN Teardown

Required global flags for all commands: `stackPattern`, `awsRegion`, `awsProfile`
Required global flags for all commands: `STACK_PATTERN`, `AWS_REGION`, `AWS_PROFILE`

1. Run `cfn-teardown -h` and see available commands and needed parameters.

Expand All @@ -60,31 +60,31 @@ Required global flags for all commands: `stackPattern`, `awsRegion`, `awsProfile

Configuration for this command can be set in three different ways in the precedence order defined below:
1. Environment variables(same as flag name)
2. Flags e.g. `cfn-teardown deleteStacks --stackPattern=qaenv-`
2. Flags e.g. `cfn-teardown deleteStacks --STACK_PATTERN=qaenv-`
3. Supplied YAML Config file (default: ~/.cfn-teardown.yaml)
<details>
<summary><b>Minimal config file</b></summary>

```yaml
awsRegion: us-east-1
awsProfile: staging
stackPattern: qa-
AWS_REGION: us-east-1
AWS_PROFILE: staging
STACK_PATTERN: qa-
```
</details>
<details>
<summary><b>All configs present</b></summary>
```yaml
awsRegion: us-east-1
awsProfile: staging
awsAccountId: 121212121212
stackPattern: qa-
abortWaitTimeMinutes: 20
stackWaitTimeSeconds: 30
maxDeleteRetryCount: 5
notificationWebhookURL: https://hooks.slack.com/services/dummy/dummy/long_hash
roleARN: <arn>
dryRun: false
AWS_REGION: us-east-1
AWS_PROFILE: staging
TARGET_ACCOUNT_ID: 121212121212
STACK_PATTERN: qa-
ABORT_WAIT_TIME_MINUTES: 20
STACK_WAIT_TIME_SECONDS: 30
MAX_DELETE_RETRY_COUNT: 5
SLACK_WEBHOOK_URL: https://hooks.slack.com/services/dummy/dummy/long_hash
ROLE_ARN: "<arn>"
DRY_RUN: "false"
```
</details>
Expand Down Expand Up @@ -160,20 +160,21 @@ By default it tries to use the IAM role of environment it is being run. e.g. Cod

## Safety Checks for Accidental Deletion

- `dryRun` flag must be explicitely set to `false` to activate delete functionality
- `DRY_RUN` flag must be explicitely set to `false` to activate delete functionality

- `abortWaitTimeMinutes` 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
- `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

- `awsAccountId` flag will check the supplied account id with aws session account id during runtime to confirm that we are deleting stacks in the desired non production account
- `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


## Edge Case
If a stack can't be deleted from the AWS Console itself due to some dependencies, then it won't be deleted by this tool as well. In such case, manual intervention is required which is notified by this tool.
- If a stack can't be deleted from the AWS Console itself due to some dependencies or error, then it won't be deleted by this tool as well. In such case, manual intervention is required.
- To delete a stack with S3 bucket, this script empties the bucket first and then deletes the stack since CFN does not allow to delete stack with non-empty bucket.


## Caution :warning:
_With great power, comes great responsibility_

- Use this tool with great caution. **Don't ever** run this in production environment with the intention of deleting a subset of stacks.
- First try within small number of test stacks in dry run mode.
- Use redundant safety flags `dryRun`, `awsAccountId` and `abortWaitTimeMinutes`.
- Use redundant safety flags `DRY_RUN`, `TARGET_ACCOUNT_ID` and `ABORT_WAIT_TIME_MINUTES`.
30 changes: 17 additions & 13 deletions cmd/deleteStacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,38 +33,42 @@ Example:
If your stacks to be deleted follow this naming convention: qa-{{component name}}
Supply stack pattern as: 'qa-'
`,
Example: "cfn-teardown deleteStacks --stackPattern='qa-' --awsProfile='staging' --region=us-east-1",
Example: "cfn-teardown deleteStacks --STACK_PATTERN='^qa-' --AWS_PROFILE=staging --AWS_REGION=us-east-1",
Args: func(cmd *cobra.Command, args []string) error {
// validate your arguments here
return validateConfigs(config)
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Executing command: deleteStacks")
fmt.Println()
if config.DryRun != "false" {
fmt.Println("Running in dry run mode. Set dry run to 'false' to actually delete stacks.")
}

utils.InitiateTearDown(config)
},
}

func init() {
rootCmd.AddCommand(deleteStacksCmd)

deleteStacksCmd.Flags().Int("stackWaitTimeSeconds", 30, "Seconds to wait after delete requests are submitted to CFN")
viper.BindPFlag("stackWaitTimeSeconds", deleteStacksCmd.Flags().Lookup("stackWaitTimeSeconds"))
deleteStacksCmd.Flags().Int("STACK_WAIT_TIME_SECONDS", 30, "Seconds to wait after delete requests are submitted to CFN")
viper.BindPFlag("STACK_WAIT_TIME_SECONDS", deleteStacksCmd.Flags().Lookup("STACK_WAIT_TIME_SECONDS"))

deleteStacksCmd.Flags().String("awsAccountId", "", "[Safety Check] Validates against account id in current aws session and provided ID")
viper.BindPFlag("awsAccountId", deleteStacksCmd.Flags().Lookup("awsAccountId"))
deleteStacksCmd.Flags().String("TARGET_ACCOUNT_ID", "", "[Safety Check] Confirmes that account id from aws session and intented target aws account are the same")
viper.BindPFlag("TARGET_ACCOUNT_ID", deleteStacksCmd.Flags().Lookup("TARGET_ACCOUNT_ID"))

deleteStacksCmd.Flags().Int("maxDeleteRetryCount", 5, "Max stack delete attempts")
viper.BindPFlag("maxDeleteRetryCount", deleteStacksCmd.Flags().Lookup("maxDeleteRetryCount"))
deleteStacksCmd.Flags().Int("MAX_DELETE_RETRY_COUNT", 5, "Max stack delete attempts")
viper.BindPFlag("MAX_DELETE_RETRY_COUNT", deleteStacksCmd.Flags().Lookup("MAX_DELETE_RETRY_COUNT"))

deleteStacksCmd.Flags().Int("abortWaitTimeMinutes", 10, "[Safety Check] Minutes to wait before initiating deletion")
viper.BindPFlag("abortWaitTimeMinutes", deleteStacksCmd.Flags().Lookup("abortWaitTimeMinutes"))
deleteStacksCmd.Flags().Int("ABORT_WAIT_TIME_MINUTES", 10, "[Safety Check] Minutes to wait before initiating deletion")
viper.BindPFlag("ABORT_WAIT_TIME_MINUTES", deleteStacksCmd.Flags().Lookup("ABORT_WAIT_TIME_MINUTES"))

deleteStacksCmd.Flags().String("notificationWebhookURL", "", "Send status alerts to Slack channel")
viper.BindPFlag("notificationWebhookURL", deleteStacksCmd.Flags().Lookup("notificationWebhookURL"))
deleteStacksCmd.Flags().String("SLACK_WEBHOOK_URL", "", "Send status alerts to Slack channel")
viper.BindPFlag("SLACK_WEBHOOK_URL", deleteStacksCmd.Flags().Lookup("SLACK_WEBHOOK_URL"))

deleteStacksCmd.Flags().String("dryRun", "true", "[Safety Check] To delete stacks, it needs to be explicitely set to false")
viper.BindPFlag("dryRun", deleteStacksCmd.Flags().Lookup("dryRun"))
deleteStacksCmd.Flags().String("DRY_RUN", "true", "[Safety Check] To delete stacks, it needs to be explicitely set to false")
viper.BindPFlag("DRY_RUN", deleteStacksCmd.Flags().Lookup("DRY_RUN"))

// Here you will define your flags and configuration settings.

Expand Down
5 changes: 3 additions & 2 deletions cmd/listDependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,19 @@ Example:
If your stacks to be deleted follow this naming convention: qa-{{component name}}
Supply stack pattern as: 'qa-'
`,
Example: "cfn-teardown listDependencies --stackPattern='qa-' --awsProfile='staging' --region=us-east-1",
Example: "cfn-teardown listDependencies --STACK_PATTERN='qa-' --AWS_PROFILE='staging' --AWS_REGION=us-east-1",

Args: func(cmd *cobra.Command, args []string) error {
// validate your arguments here
return validateConfigs(config)
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println()
fmt.Println("Executing command: listDependencies")
fmt.Println()

// for safety
config.DryRun = "true"
fmt.Println("Running in dry run mode...")

utils.InitiateTearDown(config)
},
Expand Down
22 changes: 11 additions & 11 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ func validateConfigs(config models.Config) (err error) {
emptyFlags := []string{}

if config.StackPattern == "" {
emptyFlags = append(emptyFlags, "stackPattern")
emptyFlags = append(emptyFlags, "STACK_PATTERN")
}

if config.AWSProfile == "" {
emptyFlags = append(emptyFlags, "awsProfile")
emptyFlags = append(emptyFlags, "AWS_PROFILE")
}

if config.AWSRegion == "" {
emptyFlags = append(emptyFlags, "awsRegion")
emptyFlags = append(emptyFlags, "AWS_REGION")
}

if len(emptyFlags) > 0 {
Expand All @@ -98,17 +98,17 @@ func init() {
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

rootCmd.PersistentFlags().String("stackPattern", "", "Pattern to match stack name e.g. 'staging-'")
viper.BindPFlag("stackPattern", rootCmd.PersistentFlags().Lookup("stackPattern"))
rootCmd.PersistentFlags().String("STACK_PATTERN", "", "Pattern to match stack name e.g. 'staging-'")
viper.BindPFlag("STACK_PATTERN", rootCmd.PersistentFlags().Lookup("STACK_PATTERN"))

rootCmd.PersistentFlags().String("awsRegion", "", "AWS Region where the stacks are present")
viper.BindPFlag("awsRegion", rootCmd.PersistentFlags().Lookup("awsRegion"))
rootCmd.PersistentFlags().String("AWS_REGION", "", "AWS Region where the stacks are present")
viper.BindPFlag("AWS_REGION", rootCmd.PersistentFlags().Lookup("AWS_REGION"))

rootCmd.PersistentFlags().String("awsProfile", "", "AWS Profile")
viper.BindPFlag("awsProfile", rootCmd.PersistentFlags().Lookup("awsProfile"))
rootCmd.PersistentFlags().String("AWS_PROFILE", "", "AWS Profile")
viper.BindPFlag("AWS_PROFILE", rootCmd.PersistentFlags().Lookup("AWS_PROFILE"))

rootCmd.PersistentFlags().String("roleARN", "", "Assume this role to scan and delete stacks if provided")
viper.BindPFlag("roleARN", rootCmd.PersistentFlags().Lookup("roleARN"))
rootCmd.PersistentFlags().String("ROLE_ARN", "", "Assume this role to scan and delete stacks if provided")
viper.BindPFlag("ROLE_ARN", rootCmd.PersistentFlags().Lookup("ROLE_ARN"))

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cfn-teardown.yaml)")

Expand Down
20 changes: 10 additions & 10 deletions models/nuke.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ package models

// Config represents all the parameters supported by cfn-teardown
type Config struct {
AWSProfile string `mapstructure:"awsProfile"`
AWSRegion string `mapstructure:"awsRegion"`
AWSAccountId string `mapstructure:"awsAccountId"`
StackPattern string `mapstructure:"stackPattern"`
StackWaitTimeSeconds int16 `mapstructure:"stackWaitTimeSeconds"`
MaxDeleteRetryCount int16 `mapstructure:"maxDeleteRetryCount"`
AbortWaitTimeMinutes int16 `mapstructure:"abortWaitTimeMinutes"`
NotificationWebhookURL string `mapstructure:"notificationWebhookURL"`
RoleARN string `mapstructure:"roleARN"`
DryRun string `mapstructure:"dryRun"`
AWSProfile string `mapstructure:"AWS_PROFILE"`
AWSRegion string `mapstructure:"AWS_REGION"`
TargetAccountId string `mapstructure:"TARGET_ACCOUNT_ID"`
StackPattern string `mapstructure:"STACK_PATTERN"`
StackWaitTimeSeconds int16 `mapstructure:"STACK_WAIT_TIME_SECONDS"`
MaxDeleteRetryCount int16 `mapstructure:"MAX_DELETE_RETRY_COUNT"`
AbortWaitTimeMinutes int16 `mapstructure:"ABORT_WAIT_TIME_MINUTES"`
SlackWebhookURL string `mapstructure:"SLACK_WEBHOOK_URL"`
RoleARN string `mapstructure:"ROLE_ARN"`
DryRun string `mapstructure:"DRY_RUN"`
}
37 changes: 18 additions & 19 deletions utils/cloudformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ limitations under the License.
package utils

import (
"log"
"os"
"fmt"
"regexp"
"strings"

Expand All @@ -31,10 +30,11 @@ import (
)

type CFNManager struct {
ExpectedAccountID string
NukeRoleARN string
StackPattern string
AWSRegion string
TargetAccountId string
NukeRoleARN string
StackPattern string
AWSProfile string
AWSRegion string
}

func (dm CFNManager) DescribeStack(stackName string) (*cloudformation.Stack, error) {
Expand Down Expand Up @@ -75,7 +75,7 @@ func (dm CFNManager) ListStackResources(stackName string) ([]*cloudformation.Sta
}

if err != nil {
log.Printf("Error listing resources of stack '%v': %v\n", stackName, err)
fmt.Printf("Error listing resources of stack '%v': %v\n", stackName, err)
}

return resp.StackResourceSummaries, err
Expand Down Expand Up @@ -109,7 +109,7 @@ func (dm CFNManager) ListImports(exportNames []string) (map[string]struct{}, err
// No error means, delete request sent to cloudformation
// If the stack we are trying to delete has already been deleted, returns success
func (dm CFNManager) DeleteStack(stackName string) error {
log.Printf("Submitting delete request for stack: %v\n", stackName)
fmt.Printf("Submitting delete request for stack: %v\n", stackName)
cfn, err := dm.Session()
if err != nil {
return err
Expand Down Expand Up @@ -152,7 +152,7 @@ func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) {
}

if err != nil {
log.Printf("Failed listing stacks with pattern: '%v', Error: '%v'\n", dm.StackPattern, err)
fmt.Printf("Failed listing stacks with pattern: '%v', Error: '%v'\n", dm.StackPattern, err)
return envStacks, err
}

Expand Down Expand Up @@ -180,7 +180,7 @@ func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) {
}

if err != nil {
log.Printf("Error listing '%v' environment stacks: %v\n", dm.StackPattern, err)
fmt.Printf("Error listing '%v' environment stacks: %v\n", dm.StackPattern, err)
}
return envStacks, err
}
Expand All @@ -206,7 +206,7 @@ func (dm CFNManager) ListEnvironmentExports() (map[string][]string, error) {
}

if err != nil {
log.Printf("Error listing '%v' environment stack exports: %v\n", dm.StackPattern, err)
fmt.Printf("Error listing '%v' environment stack exports: %v\n", dm.StackPattern, err)
return exports, err
}

Expand Down Expand Up @@ -240,37 +240,36 @@ func (dm CFNManager) RegexMatch(stackName string) bool {
// assumes staging nuke role
func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) {
sess := session.Must(session.NewSessionWithOptions(session.Options{
Config: aws.Config{Region: aws.String(os.Getenv("AWS_REGION"))},
Config: aws.Config{Region: aws.String(dm.AWSRegion)},
SharedConfigState: session.SharedConfigEnable,
Profile: os.Getenv("AWS_PROFILE"),
Profile: dm.AWSProfile,
}))

isStaging, err := dm.IsDesiredAWSAccount(sess)
desiredAccount, err := dm.IsDesiredAWSAccount(sess)
if err != nil {
return nil, err
}

// to make things easy while running this script locally
if isStaging {
if desiredAccount {
return cloudformation.New(sess), nil
} else {
// Create the credentials from AssumeRoleProvider to assume the role referenced by the "NukeRoleARN" ARN.
// Create the credentials from AssumeRoleProvider if nuke role arn is provided
creds := stscreds.NewCredentials(sess, dm.NukeRoleARN)
// Create service client value configured for credentials from assumed role.
return cloudformation.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), nil
}

}

func (dm CFNManager) IsDesiredAWSAccount(sess *session.Session) (bool, error) {
svc := sts.New(sess)
result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{})
if err != nil {
log.Printf("Error requesting AWS caller identity: %v", err.Error())
fmt.Printf("Error requesting AWS caller identity: %v", err.Error())
return false, err
}

if *result.Account == dm.ExpectedAccountID {
if *result.Account == dm.TargetAccountId {
return true, err
}
return false, err
Expand Down
Loading

0 comments on commit ff5823c

Please sign in to comment.