diff --git a/.cdkr.yaml b/.cdkr.yaml new file mode 100644 index 0000000..b7e4f27 --- /dev/null +++ b/.cdkr.yaml @@ -0,0 +1,11 @@ +build: + - aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws +apps: + - basedir: infrastructure + app: python3 app.py + stacks: + - '*' + context: + gaggle-cdk:account: {{account_id}} + gaggle-cdk:environment: {{environment}} + gaggle-cdk:deploy-it-role: 'true' \ No newline at end of file diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 0902ccf..ad618e2 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: 3.13 - name: install run: | python -m pip install --upgrade pip diff --git a/ecs_chargeback/requirements.txt b/ecs_chargeback/requirements.txt index 3d1c40a..dd3edd2 100644 --- a/ecs_chargeback/requirements.txt +++ b/ecs_chargeback/requirements.txt @@ -1,3 +1,3 @@ -boto3==1.26.52 -tabulate==0.9.0 +boto3 +tabulate datadog diff --git a/infrastructure/app.py b/infrastructure/app.py index 6add05d..513193f 100644 --- a/infrastructure/app.py +++ b/infrastructure/app.py @@ -1,93 +1,22 @@ #!/usr/bin/env python3 import os -from os.path import join, dirname, abspath -import aws_cdk as cdk -from constructs import Construct -from aws_cdk import ( - aws_lambda_python_alpha as aws_lambda_python, - aws_lambda, - aws_events, - aws_events_targets, - aws_s3, - aws_iam, -) -from gaggle_cdk.core import apply_permissions_boundary - -base_path = dirname(dirname(abspath(__file__))) - -class ChargebackStack(cdk.Stack): - def __init__( - self, - scope: Construct, - id: str, - cluster_tag: str, - run_frequency_mins: int, - cost_lookback_days: int, - datadog_metric_prefix: str, - dd_api_key_secret_id: str, - dd_api_key_secret_field: str = "api_key", - bucket_name: str = None, - **kwargs - ) -> None: - super().__init__(scope, id, **kwargs) - - cache_bucket = aws_s3.Bucket( - self, - "ChargebackCacheBucket", - bucket_name=bucket_name, - ) - - datadog_api_key = cdk.SecretValue.secrets_manager( - secret_id=dd_api_key_secret_id, - json_field=dd_api_key_secret_field, - ) +from aws_cdk import App, Environment +from os.path import join, dirname, abspath +from gaggle_cdk.core import apply_permissions_boundary, GaggleTags - chargeback = aws_lambda_python.PythonFunction( - self, - "ChargebackHandler", - entry=join(base_path, "ecs_chargeback"), - runtime=aws_lambda.Runtime.PYTHON_3_8, - index="lambda.py", - handler="handler", - environment={ - "CLUSTER_TAG": cluster_tag, - "CACHE_BUCKET": cache_bucket.bucket_name, - "UTILIZATION_LOOKBACK_MINS": str(run_frequency_mins), - "COST_LOOKBACK_DAYS": str(cost_lookback_days), - "DATADOG_API_KEY": datadog_api_key.to_string(), - "DATADOG_METRIC_PREFIX": datadog_metric_prefix, - }, - timeout=cdk.Duration.seconds(60), - ) - chargeback.add_to_role_policy( - aws_iam.PolicyStatement( - actions=[ - "ecs:ListClusters", - "ecs:ListServices", - "ecs:DescribeServices", - "ecs:DescribeTaskDefinition", - "ecs:ListTagsForResource", - "cloudwatch:GetMetricData", - "ec2:DescribeInstanceTypes", - "ce:GetCostAndUsage", - ], - resources=["*"], - ) - ) +from app_stack import ChargebackStack - cache_bucket.grant_read_write(chargeback.role) +app = App() - rule = aws_events.Rule( - self, - "ChargebackHandlerRule", - schedule=aws_events.Schedule.rate(cdk.Duration.minutes(run_frequency_mins)), - ) - rule.add_target(aws_events_targets.LambdaFunction(chargeback)) +base_path = dirname(dirname(abspath(__file__))) +app_name = "ecs-chargeback" +environment = "production" +account_id = app.node -app = cdk.App() -ChargebackStack( +## Production +app_stack = ChargebackStack( app, "ecs-chargeback", cluster_tag=app.node.try_get_context("chargeback:cluster-tag"), @@ -101,12 +30,17 @@ def __init__( "chargeback:datadog-api-key-secret-field" ), datadog_metric_prefix=app.node.try_get_context("chargeback:datadog-metric-prefix"), - env=cdk.Environment( + env=Environment( account=os.environ["CDK_DEFAULT_ACCOUNT"], region=os.environ["CDK_DEFAULT_REGION"], ), ) +GaggleTags( + application=app_name, + environment=environment, + team=GaggleTags.Team.DEVOPS, +).apply(app_stack) -apply_permissions_boundary(app) +apply_permissions_boundary(app_stack) app.synth() diff --git a/infrastructure/app_stack.py b/infrastructure/app_stack.py new file mode 100644 index 0000000..ab068cc --- /dev/null +++ b/infrastructure/app_stack.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +import os + +from aws_cdk import ( + aws_lambda, + aws_events, + aws_events_targets, + aws_s3, + aws_iam, + BundlingOptions, + Duration, + SecretValue, + Stack, +) +from os.path import join, dirname, abspath +from constructs import Construct +from gaggle_cdk.core import apply_permissions_boundary +from gaggle_cdk.core.tagging import GaggleTags +from gaggle_cdk.core.teams import GaggleTeam + +base_path = dirname(dirname(abspath(__file__))) + +app_name = "ecs-chargeback" + + +class ChargebackStack(Stack): + def __init__( + self, + scope: Construct, + id: str, + *, + cluster_tag: str, + run_frequency_mins: int, + cost_lookback_days: int, + datadog_metric_prefix: str, + dd_api_key_secret_id: str, + dd_api_key_secret_field: str = "api_key", + bucket_name: str = None, + **kwargs + ) -> None: + super().__init__(scope, id, **kwargs) + + cache_bucket = aws_s3.Bucket( + self, + "ChargebackCacheBucket", + bucket_name=bucket_name, + ) + + datadog_api_key = SecretValue.secrets_manager( + secret_id=dd_api_key_secret_id, + json_field=dd_api_key_secret_field, + ) + + iam_role = aws_iam.Role( + self, + "ChargebackLambdaRole", + assumed_by=aws_iam.ServicePrincipal("lambda.amazonaws.com"), + managed_policies=[ + aws_iam.ManagedPolicy.from_aws_managed_policy_name( + "service-role/AWSLambdaBasicExecutionRole" + ) + ], + inline_policies={ + "LambdaPolicy": aws_iam.PolicyDocument( + statements=[ + aws_iam.PolicyStatement( + effect=aws_iam.Effect.ALLOW, + actions=[ + "ecs:ListClusters", + "ecs:ListServices", + "ecs:DescribeServices", + "ecs:DescribeTaskDefinition", + "ecs:ListTagsForResource", + "cloudwatch:GetMetricData", + "ec2:DescribeInstanceTypes", + "ce:GetCostAndUsage", + ], + resources=["*"], + ) + ] + ) + }, + ) + + chargeback = aws_lambda.Function( + self, + "ChargebackHandler", + runtime=aws_lambda.Runtime.PYTHON_3_13, + function_name=app_name, + code=aws_lambda.Code.from_asset( + join(base_path, "ecs_chargeback"), + bundling=BundlingOptions( + image=aws_lambda.Runtime.PYTHON_3_13.bundling_image, + command=[ + "bash", + "-c", + "pip install --no-cache -r requirements.txt -t /asset-output && cp -au . /asset-output", + ], + ), + ), + handler="lambda.handler", + environment={ + "CLUSTER_TAG": cluster_tag, + "CACHE_BUCKET": cache_bucket.bucket_name, + "UTILIZATION_LOOKBACK_MINS": str(run_frequency_mins), + "COST_LOOKBACK_DAYS": str(cost_lookback_days), + "DATADOG_API_KEY": datadog_api_key.to_string(), + "DATADOG_METRIC_PREFIX": datadog_metric_prefix, + }, + timeout=Duration.seconds(60), + role=iam_role, + ) + + cache_bucket.grant_read_write(chargeback.role) + + rule = aws_events.Rule( + self, + "ChargebackHandlerRule", + schedule=aws_events.Schedule.rate(Duration.minutes(run_frequency_mins)), + ) + rule.add_target(aws_events_targets.LambdaFunction(chargeback)) diff --git a/infrastructure/requirements.txt b/infrastructure/requirements.txt index e3ebffe..595c1c8 100644 --- a/infrastructure/requirements.txt +++ b/infrastructure/requirements.txt @@ -1,4 +1,3 @@ -aws-cdk-lib==2.61.0 -aws_cdk.aws_lambda_python_alpha==2.61.0a0 -constructs>=10.0.0 -gaggle-cdk.core==2.1.164 +aws-cdk-lib +constructs +gaggle-cdk.core