Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tools: Weathertop - Add Account Nuker #7203

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .tools/test/stacks/nuke/typescript/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cdk.out/
12 changes: 12 additions & 0 deletions .tools/test/stacks/nuke/typescript/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM ghcr.io/ekristen/aws-nuke:v3.42.0
ENV AWS_SDK_LOAD_CONFIG=1 \
AWS_DEBUG=true
USER root
RUN apk add --no-cache \
python3 \
py3-pip \
aws-cli
COPY nuke_generic_config.yaml /nuke_generic_config.yaml
COPY --chmod=755 run.sh /run.sh
USER aws-nuke
ENTRYPOINT ["/run.sh"]
52 changes: 52 additions & 0 deletions .tools/test/stacks/nuke/typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# aws-nuke for Weathertop

[aws-nuke](https://github.com/ekristen/aws-nuke) is an open-source tool that deletes non-default resources in a provided AWS account. It's implemented here in this directory using Cloud Development Kit (CDK) code that deploys the [official aws-nuke image](https://github.com/ekristen/aws-nuke/pkgs/container/aws-nuke) to an AWS Lambda function.

## ⚠ Important

This is a very destructive tool! It should not be deployed without fully understanding the impact it will have on your AWS accounts.
Please use caution and configure this tool to delete unused resources only in your lower test/sandbox environment accounts.

## Overview

This CDK stack is defined in [account_nuker.ts](account_nuker.ts). It includes:

- A Docker-based Lambda function with ARM64 architecture and 1GB memory
- An IAM role with administrative permissions for the Lambda's nuking function
- An EventBridge rule that triggers the function every Sunday at midnight

More specifically, this Lambda function is built from a [Dockerfile](Dockerfile) and runs with a 15-minute timeout. It contains a [nuke_generic_config.yml](nuke_generic_config.yaml) config and executes a [run.sh](run.sh) when invoked every Sunday at midnight UTC.

![infrastructure-overview](nuke-overview.png)

## Prerequisites

1. **Non-Prod AWS Account Alias**: A non-prod account alias must exist in target account. Set the alias by running `python create_account_alias.py weathertop-test` or following [these instructions](https://docs.aws.amazon.com/IAM/latest/UserGuide/account-alias-create.html).

## Setup and Installation

For multi-account deployments, please use the [deploy.py](../../../DEPLOYMENT.md#option-1-using-deploypy) script.

For single-account deployment, you can just run:

```sh
cdk bootstrap && cdk deploy
```

Note a successful stack creation, e.g.:

```bash
NukeStack: success: Published 956fbd116734e79edb987e767fe7f45d0b97e2123456789109103f80ba4c1:123456789101-us-east-1
Stack undefined
NukeStack: deploying... [1/1]
NukeStack: creating CloudFormation changeset...

✅ NukeStack

✨ Deployment time: 27.93s

Stack ARN:
arn:aws:cloudformation:us-east-1:123456789101:stack/NukeStack/9835cc20-d358-11ef-bccf-123407dc82dd

✨ Total time: 33.24s
```
65 changes: 65 additions & 0 deletions .tools/test/stacks/nuke/typescript/account_nuker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import * as cdk from "aws-cdk-lib";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as iam from "aws-cdk-lib/aws-iam";
import * as path from "path";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { Duration, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { DockerImageCode, DockerImageFunction } from "aws-cdk-lib/aws-lambda";

export interface NukeStackProps extends cdk.StackProps {
awsNukeDryRunFlag?: string;
awsNukeVersion?: string;
owner?: string;
}

class NukeStack extends cdk.Stack {
private readonly nukeLambdaRole: iam.Role;

constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

// Lambda Function role
this.nukeLambdaRole = new iam.Role(this, "NukeLambdaRole", {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"),
],
});

// Create the Lambda function
const lambdaFunction = new DockerImageFunction(
this,
"docker-lambda-function",
{
functionName: "docker-lambda-fn",
code: DockerImageCode.fromImageAsset(path.join(__dirname)),
memorySize: 1024,
timeout: Duration.minutes(15),
architecture: lambda.Architecture.ARM_64,
description: "This is dockerized AWS Lambda function",
role: this.nukeLambdaRole,
},
);

// Create EventBridge rule to trigger the Lambda function weekly
const rule = new events.Rule(this, "WeeklyTriggerRule", {
schedule: events.Schedule.expression("cron(0 0 ? * SUN *)"), // Runs at 00:00 every Sunday
});

// Add the Lambda function as a target for the EventBridge rule
rule.addTarget(new targets.LambdaFunction(lambdaFunction));
}
}

const app = new cdk.App();
new NukeStack(app, "NukeStack", {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
terminationProtection: true,
});
33 changes: 33 additions & 0 deletions .tools/test/stacks/nuke/typescript/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"app": "npx ts-node --prefer-ts-exts account_nuker.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
"cdk-migrate": true
}
}
118 changes: 118 additions & 0 deletions .tools/test/stacks/nuke/typescript/create_account_alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""
This module is used to create an AWS account alias, which is required by the deploy.py script.

It provides a function to create an account alias using the AWS CLI, as this specific
operation is not supported by the AWS CDK.
"""

import logging
import re
import subprocess

logger = logging.getLogger(__name__)


def _is_valid_alias(alias_name: str) -> bool:
"""
Check if the provided alias name is valid according to AWS rules.

AWS account alias must be unique and must be between 3 and 63 characters long.
Valid characters are a-z, 0-9 and '-'.

Args:
alias_name (str): The alias name to validate.

Returns:
bool: True if the alias is valid, False otherwise.
"""
pattern = r"^[a-z0-9](([a-z0-9]|-){0,61}[a-z0-9])?$"
return bool(re.match(pattern, alias_name)) and 3 <= len(alias_name) <= 63


def _log_aws_cli_version() -> None:
"""
Log the version of the AWS CLI installed on the system.
"""
try:
result = subprocess.run(["aws", "--version"], capture_output=True, text=True)
logger.info(f"AWS CLI version: {result.stderr.strip()}")
except Exception as e:
logger.warning(f"Unable to determine AWS CLI version: {str(e)}")


def create_account_alias(alias_name: str) -> None:
"""
Create a new account alias with the given name.

This function exists because the CDK does not support the specific
CreateAccountAliases API call. It attempts to create an account alias
using the AWS CLI and logs the result.

If the account alias is created successfully, it logs a success message.
If the account alias already exists, it logs a message indicating that.
If there is any other error, it logs the error message.

Args:
alias_name (str): The desired name for the account alias.
"""
# Log AWS CLI version when the function is called
_log_aws_cli_version()

if not _is_valid_alias(alias_name):
logger.error(
f"Invalid alias name '{alias_name}'. It must be between 3 and 63 characters long and contain only lowercase letters, numbers, and hyphens."
)
return

command = ["aws", "iam", "create-account-alias", "--account-alias", alias_name]

try:
subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
logger.info(f"Account alias '{alias_name}' created successfully.")
except subprocess.CalledProcessError as e:
if "EntityAlreadyExists" in e.stderr:
logger.info(f"Account alias '{alias_name}' already exists.")
elif "AccessDenied" in e.stderr:
logger.error(
f"Access denied when creating account alias '{alias_name}'. Check your AWS credentials and permissions."
)
elif "ValidationError" in e.stderr:
logger.error(
f"Validation error when creating account alias '{alias_name}'. The alias might not meet AWS requirements."
)
else:
logger.error(f"Error creating account alias '{alias_name}': {e.stderr}")
except Exception as e:
logger.error(
f"Unexpected error occurred while creating account alias '{alias_name}': {str(e)}"
)


def main():
import argparse

# Set up logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

# Create argument parser
parser = argparse.ArgumentParser(description="Create an AWS account alias")
parser.add_argument("alias", help="The alias name for the AWS account")

# Parse arguments
args = parser.parse_args()

# Call the function with the provided alias
create_account_alias(args.alias)
ford-at-aws marked this conversation as resolved.
Show resolved Hide resolved

if __name__ == "__main__":
main()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading