diff --git a/.github/workflows/cdk-deploy-dev.yml b/.github/workflows/cdk-deploy-dev.yml index 0cf4e40..4fd7f6c 100644 --- a/.github/workflows/cdk-deploy-dev.yml +++ b/.github/workflows/cdk-deploy-dev.yml @@ -31,8 +31,8 @@ jobs: - name: Install dependencies run: uv sync - name: Install cdk cli - run: npm install -g aws-cdk - - name: Run CDK synth for the DEV environment - run: uv run projen dev:synth + run: npm install -g aws-cdk@2.1130.0 + - name: Validate CDK for the DEV environment + run: uv run projen dev:validate - name: Deploy CDK to the DEV environment on AWS account 987654321012 run: uv run projen dev:deploy diff --git a/.github/workflows/cdk-deploy-test.yml b/.github/workflows/cdk-deploy-test.yml index 8b1b1c8..943a594 100644 --- a/.github/workflows/cdk-deploy-test.yml +++ b/.github/workflows/cdk-deploy-test.yml @@ -31,8 +31,8 @@ jobs: - name: Install dependencies run: uv sync - name: Install cdk cli - run: npm install -g aws-cdk - - name: Run CDK synth for the TEST environment - run: uv run projen test:synth + run: npm install -g aws-cdk@2.1130.0 + - name: Validate CDK for the TEST environment + run: uv run projen test:validate - name: Deploy CDK to the TEST environment on AWS account 123456789012 run: uv run projen test:deploy diff --git a/.projen/deps.json b/.projen/deps.json index fe80ec2..511a81d 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -5,6 +5,11 @@ "version": "0.99.62", "type": "devenv" }, + { + "name": "pytest", + "version": "9.0.3", + "type": "devenv" + }, { "name": "ruff", "type": "devenv" @@ -19,7 +24,7 @@ }, { "name": "aws-cdk-lib", - "version": "2.254.0", + "version": "2.263.0", "type": "runtime" }, { diff --git a/.projen/tasks.json b/.projen/tasks.json index 4c5438b..0054fc5 100644 --- a/.projen/tasks.json +++ b/.projen/tasks.json @@ -141,6 +141,20 @@ } ] }, + "dev:validate": { + "name": "dev:validate", + "description": "Validate the stacks on the DEV account", + "env": { + "CDK_DEFAULT_ACCOUNT": "987654321012", + "ENVIRONMENT": "dev" + }, + "steps": [ + { + "exec": "cdk --unstable=validate validate", + "receiveArgs": true + } + ] + }, "diff": { "name": "diff", "description": "Diffs the currently deployed app against your code", @@ -241,6 +255,9 @@ "test": { "name": "test", "description": "Run tests", + "env": { + "PYTHONPATH": "src" + }, "steps": [ { "exec": "pytest" @@ -299,6 +316,30 @@ } ] }, + "test:validate": { + "name": "test:validate", + "description": "Validate the stacks on the TEST account", + "env": { + "CDK_DEFAULT_ACCOUNT": "123456789012", + "ENVIRONMENT": "test" + }, + "steps": [ + { + "exec": "cdk --unstable=validate validate", + "receiveArgs": true + } + ] + }, + "validate": { + "name": "validate", + "description": "Validate the CDK app offline against the default CloudFormation rules", + "steps": [ + { + "exec": "cdk --unstable=validate validate --no-online", + "receiveArgs": true + } + ] + }, "watch": { "name": "watch", "description": "Watches changes in your source code and rebuilds and deploys to the current account", diff --git a/.projenrc.py b/.projenrc.py index 8bff9bf..86847ef 100644 --- a/.projenrc.py +++ b/.projenrc.py @@ -17,6 +17,8 @@ python_version = "3.13" python_major, python_minor = map(int, python_version.split(".")) python_requires = f">={python_version},<{python_major}.{python_minor + 1}" +cdk_version = "2.263.0" +cdk_cli_version = "2.1130.0" # Define the AWS region for the CDK app and github workflows # Default to us-east-1 if AWS_REGION is not set in your environment variables @@ -26,8 +28,12 @@ author_email="danny@towardsthecloud.com", author_name="Danny Steenman", cdk_version_pinning=True, - cdk_version="2.254.0", # Find the latest CDK version here: https://pypi.org/project/aws-cdk-lib - cdk_cli_version="2.1117.0", # Find the latest CDK CLI version https://pypi.org/project/aws-cdk-cli/ + cdk_version=cdk_version, # Find the latest CDK version here: https://pypi.org/project/aws-cdk-lib + cdk_cli_version=cdk_cli_version, # Find the latest CDK CLI version https://pypi.org/project/aws-cdk-cli/ + context={ + "@aws-cdk/core:annotationsInValidationReport": True, + "@aws-cdk/core:validateAgainstDefaultRules": True, + }, module_name=python_module_name, name=project_name, projen_command="uv run projen", @@ -35,7 +41,12 @@ version="2.101.0", app_entrypoint=f"{python_module_name}/app.py", deps=["aws-cdk-github-oidc"], - dev_deps=["projen@0.99.62", "ruff", "ty"], # Find the latest projen version here: https://pypi.org/project/projen/ + dev_deps=[ + "projen@0.99.62", + "pytest@9.0.3", + "ruff", + "ty", + ], # Find the latest projen version here: https://pypi.org/project/projen/ uv=True, uv_options={ "python_exec": f"python{python_version}", @@ -47,7 +58,18 @@ github_options={ "pull_request_lint_options": { "semantic_title_options": { - "types": ["feat", "fix", "chore", "refactor", "perf", "docs", "style", "test", "build", "ci"], + "types": [ + "feat", + "fix", + "chore", + "refactor", + "perf", + "docs", + "style", + "test", + "build", + "ci", + ], }, }, }, @@ -73,6 +95,14 @@ # Set the CDK_DEFAULT_REGION environment variable for the projen tasks, # so the CDK CLI knows which region to use project.tasks.add_environment("CDK_DEFAULT_REGION", aws_region) +project.test_task.env("PYTHONPATH", python_module_name) + +project.add_task( + "validate", + description="Validate the CDK app offline against the default CloudFormation rules", + exec="cdk --unstable=validate validate --no-online", + receive_args=True, +) # Define the target AWS accounts for the different environments target_accounts = { @@ -113,7 +143,9 @@ ) # Add auto-merge step to the auto-approve workflow -auto_approve_workflow = project.try_find_object_file(".github/workflows/auto-approve.yml") +auto_approve_workflow = project.try_find_object_file( + ".github/workflows/auto-approve.yml" +) if auto_approve_workflow: auto_approve_workflow.add_override("jobs.approve.permissions.contents", "write") # Add checkout step before the merge step @@ -143,6 +175,6 @@ ) # Adds GitHub action workflows for deploying the CDK stacks to the target AWS account - github_cicd(gh, account, env, python_version, aws_region) + github_cicd(gh, account, env, python_version, aws_region, cdk_cli_version) project.synth() diff --git a/cdk.json b/cdk.json index 867c37f..ea37381 100644 --- a/cdk.json +++ b/cdk.json @@ -1,5 +1,9 @@ { "app": "python src/app.py", + "context": { + "@aws-cdk/core:annotationsInValidationReport": true, + "@aws-cdk/core:validateAgainstDefaultRules": true + }, "output": "cdk.out", "watch": { "include": [ diff --git a/pyproject.toml b/pyproject.toml index 0692cb8..add65b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.13,<3.14" dependencies = [ "aws-cdk-github-oidc", - "aws-cdk-lib==2.254.0", + "aws-cdk-lib==2.263.0", "constructs>=10.5.1,<11.0.0" ] @@ -19,4 +19,4 @@ dependencies = [ email = "danny@towardsthecloud.com" [dependency-groups] -dev = [ "projen==0.99.62", "ruff", "ty", "pytest==9.0.3" ] +dev = [ "projen==0.99.62", "pytest==9.0.3", "ruff", "ty" ] diff --git a/src/bin/cicd_helper.py b/src/bin/cicd_helper.py index b008a28..57454fd 100644 --- a/src/bin/cicd_helper.py +++ b/src/bin/cicd_helper.py @@ -1,10 +1,13 @@ from projen import github -def github_cicd(gh, account, env, python_version, aws_region): +def github_cicd(gh, account, env, python_version, aws_region, cdk_cli_version): # Add a GitHub workflow for deploying the CDK stacks to the AWS account cdk_deployment_workflow = github.GithubWorkflow(gh, f"cdk-deploy-{env}") - cdk_deployment_workflow.on(push={"branches": ["main"]} if env != "production" else None, workflow_dispatch={}) + cdk_deployment_workflow.on( + push={"branches": ["main"]} if env != "production" else None, + workflow_dispatch={}, + ) cdk_deployment_workflow.add_jobs( { @@ -46,11 +49,11 @@ def github_cicd(gh, account, env, python_version, aws_region): }, { "name": "Install cdk cli", - "run": "npm install -g aws-cdk", + "run": f"npm install -g aws-cdk@{cdk_cli_version}", }, { - "name": f"Run CDK synth for the {env.upper()} environment", - "run": f"uv run projen {env}:synth", + "name": f"Validate CDK for the {env.upper()} environment", + "run": f"uv run projen {env}:validate", }, { "name": f"Deploy CDK to the {env.upper()} environment on AWS account {account}", diff --git a/src/bin/env_helper.py b/src/bin/env_helper.py index 19e9c07..b887bdf 100644 --- a/src/bin/env_helper.py +++ b/src/bin/env_helper.py @@ -1,25 +1,26 @@ -from typing import Dict - from projen.awscdk import AwsCdkPythonApp -def cdk_action_task(project: AwsCdkPythonApp, target_account: Dict[str, str]): - task_actions = ["synth", "diff", "deploy", "destroy"] +def cdk_action_task(project: AwsCdkPythonApp, target_account: dict[str, str]): stack_name_pattern = f"*Stack-{target_account['ENVIRONMENT']}" + action_commands = { + "synth": ("cdk synth", False), + "validate": ("cdk --unstable=validate validate", True), + "diff": (f"cdk diff --require-approval never {stack_name_pattern}", False), + "deploy": (f"cdk deploy --require-approval never {stack_name_pattern}", False), + "destroy": (f"cdk destroy --force {stack_name_pattern}", False), + } - for action in task_actions: + for action, (exec_command, receive_args) in action_commands.items(): task_name = f"{target_account['ENVIRONMENT']}:{action}" task_description = f"{action.capitalize()} the stacks on the {target_account['ENVIRONMENT'].upper()} account" - exec_command = f"cdk {action} --require-approval never {stack_name_pattern}" - if action == "destroy": - exec_command = f"cdk destroy --force {stack_name_pattern}" - if action == "synth": - exec_command = "cdk synth" - - project.add_task( + task = project.add_task( task_name, description=task_description, env=target_account, - exec=exec_command, ) + if receive_args: + task.exec(exec_command, receive_args=True) + else: + task.exec(exec_command) diff --git a/uv.lock b/uv.lock index 175b801..b4adaa9 100644 --- a/uv.lock +++ b/uv.lock @@ -13,16 +13,16 @@ wheels = [ [[package]] name = "aws-cdk-asset-awscli-v1" -version = "2.2.273" +version = "2.2.282" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsii" }, { name = "publication" }, { name = "typeguard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/11/925a1ae8dd7c9fd7e775b405f357970bdf975f54550923c6c755ac24a00d/aws_cdk_asset_awscli_v1-2.2.273.tar.gz", hash = "sha256:6580dad3416e53712db434f81add6fb4a314e1a80f9c57cc42606df1f64c8e0f", size = 20370823, upload-time = "2026-03-30T16:58:42.105Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/30/90558d9e05c2b1a5a8d7d87332b4bed4a2e5d2eb04cc7064f6e9e0c8a2f2/aws_cdk_asset_awscli_v1-2.2.282.tar.gz", hash = "sha256:f86e61653927f77fb235b5ec148c955c610117430c869d182c8b20f7e07e2df2", size = 20740538, upload-time = "2026-05-25T21:33:20.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/eb/817ecd9e0987465114d17808bb8d10907ef64c9ea6029addfab0ab9aa055/aws_cdk_asset_awscli_v1-2.2.273-py3-none-any.whl", hash = "sha256:1a0994afa7b48f63b580603be64c7a99d19ed6777bdf81d3c2435d8b43cf0d71", size = 20369281, upload-time = "2026-03-30T16:58:39.474Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/dcb45969f1b34351cfd9afdb6b35bff38a3e929c8bc42b07036879f3a62e/aws_cdk_asset_awscli_v1-2.2.282-py3-none-any.whl", hash = "sha256:24139da0eb1be59f8cfda22c6343d51e8d50bb89b5052b049cfab92683de7790", size = 20738955, upload-time = "2026-05-25T21:33:17.673Z" }, ] [[package]] @@ -41,16 +41,15 @@ wheels = [ [[package]] name = "aws-cdk-cloud-assembly-schema" -version = "53.24.0" +version = "54.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsii" }, { name = "publication" }, - { name = "typeguard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/67/a386fee11c1d51ad57d2c045f228f6a57b0fc846eb8b441fc98fbdf1a4b1/aws_cdk_cloud_assembly_schema-53.24.0.tar.gz", hash = "sha256:f999f4c777deaca6631c61993bf5583022ff57c3a94a65930e2fc6b68cb7c407", size = 212924, upload-time = "2026-05-14T17:07:18.047Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/c9/732b7ca25bcc8a2bb80aeaa9c34ab2fd111a53477742d2f7b68937b7a180/aws_cdk_cloud_assembly_schema-54.17.0.tar.gz", hash = "sha256:ecf54cbd656c8c85a8df47291be791e60c92e8aca193757667024e7805adf55d", size = 282890, upload-time = "2026-08-07T13:29:27.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/34/b180fe10ae8c314d5f8c2e095a57669d54bcb036dc63b001eace16c88052/aws_cdk_cloud_assembly_schema-53.24.0-py3-none-any.whl", hash = "sha256:360c4804f3073601ac320d1773432bc45b34201d7c8fb85aff7ed536801efb0c", size = 212811, upload-time = "2026-05-14T17:07:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/72/e2/590fa96d944601c88eb2e6160071af413a0347ea4f561f043f14e0220744/aws_cdk_cloud_assembly_schema-54.17.0-py3-none-any.whl", hash = "sha256:d727b55da80b44ec02c6eb74b3f7271656e3a47f761b4d6b41784303cbf94308", size = 281957, upload-time = "2026-08-07T13:29:25.57Z" }, ] [[package]] @@ -71,7 +70,7 @@ wheels = [ [[package]] name = "aws-cdk-lib" -version = "2.254.0" +version = "2.263.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aws-cdk-asset-awscli-v1" }, @@ -80,11 +79,10 @@ dependencies = [ { name = "constructs" }, { name = "jsii" }, { name = "publication" }, - { name = "typeguard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/94/667be78bf5f39aef15ff34778eda0443bb4ad2498f33ccb6f7629f343f43/aws_cdk_lib-2.254.0.tar.gz", hash = "sha256:ddbef134cad91f8985444f77f052f0337af5f132101ad7aea2215b4775ff7828", size = 49658861, upload-time = "2026-05-13T21:59:39.35Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/62/41de6ea7bf3c10a18a3d70c42c0fe770c8e7c4b20cb9a7b4d95e295b50d6/aws_cdk_lib-2.263.0.tar.gz", hash = "sha256:fc87a36758c93c95845bb4889c21a899eb639b78e43d09d965d9d924233e60f6", size = 55074638, upload-time = "2026-07-31T16:47:16.594Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/3c/13c490cba548fde101663fb6d9050eeee67686a9730e74cba5711ba12068/aws_cdk_lib-2.254.0-py3-none-any.whl", hash = "sha256:626095eaa742e0b9d894c3a1bf06a6e7d1245a986165a72408871d41886c6d07", size = 50343718, upload-time = "2026-05-13T21:58:54.123Z" }, + { url = "https://files.pythonhosted.org/packages/10/c3/e8d1cc686828e8213be2929db1d21b95beb50fcde36b6ecff309eae75916/aws_cdk_lib-2.263.0-py3-none-any.whl", hash = "sha256:a8d7928c7a01c25f6cb85c98e465df6ffd2b9e696bdb256257eeca2157c80609", size = 55742744, upload-time = "2026-07-31T16:46:31.48Z" }, ] [[package]] @@ -108,7 +106,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aws-cdk-github-oidc" }, - { name = "aws-cdk-lib", specifier = "==2.254.0" }, + { name = "aws-cdk-lib", specifier = "==2.263.0" }, { name = "constructs", specifier = ">=10.5.1,<11.0.0" }, ] @@ -156,15 +154,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/38/6de46d122a2fcd275a5f1fa4ad1d8d2058afd6b72d8db5f0c125515cbb5c/constructs-10.6.0-py3-none-any.whl", hash = "sha256:ad4ffabdb53c17cde00fb94e441a1ba9fddac57c92ad49d263f8dbd416cec513", size = 66969, upload-time = "2026-03-23T09:00:53.041Z" }, ] -[[package]] -name = "importlib-resources" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -176,20 +165,19 @@ wheels = [ [[package]] name = "jsii" -version = "1.130.0" +version = "1.139.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "cattrs" }, - { name = "importlib-resources" }, { name = "publication" }, { name = "python-dateutil" }, { name = "typeguard" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/36/d84bb41b72b5a830dc87f1daea521ff4da024a08c1bd0b9b34989fe84bcf/jsii-1.130.0.tar.gz", hash = "sha256:7436ae382e2de27970b34a4ccfef953a45980c5070241c1bd610bf3af68a2d6b", size = 445207, upload-time = "2026-05-14T18:35:31.792Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/71/7c612c16291a6dc654bac00f56bef24c45f86e9a7c9668dc7d36f187e808/jsii-1.139.0.tar.gz", hash = "sha256:163c5d3ec00fd4ec89a33b9097f20a6f27626dd69d6875a8f7cc7577b8021ad0", size = 531387, upload-time = "2026-07-17T02:34:33.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/34/2dc16666ee53c1158fde17c2b713a85dc90eab029fb832b9564751f40eeb/jsii-1.130.0-py3-none-any.whl", hash = "sha256:ce50e11ea588fe6b2d0766d90edaf4c78b9e97e2e1f075fbd8bc29349c6503c8", size = 418405, upload-time = "2026-05-14T18:35:30.283Z" }, + { url = "https://files.pythonhosted.org/packages/33/3e/fce01768f79b2d5d0cf6258b55045f4b083ee86fd5de24c1af38168312ab/jsii-1.139.0-py3-none-any.whl", hash = "sha256:11468f767c6da698ed9888a04352850af33ccccfec7656475626caf36fca8bbb", size = 501693, upload-time = "2026-07-17T02:34:31.747Z" }, ] [[package]]