Skip to content
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
8 changes: 5 additions & 3 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Python 3.12
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.12
python-version: 3.14
- name: Install dependencies
run: make poetry install
- name: Format and lint
run: make lint
- name: Run tests
run: make test
run: make test
env:
AWS_DEFAULT_REGION: us-east-1
57 changes: 57 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Deploy

on:
# Uncomment one of these to deploy ALL stacks to AWS
# NOTE: Uncomment only one to avoid duplicate deployments on the same event
# release: # on publishing new release
# types: [published]
# push: # on push to main
# branches: [main]
# pull_request: # on merging pull requests to main (closed includes unmerged; deploy job guards with merged == true)
# branches: [main]
# types: [closed]
workflow_dispatch:
inputs:
stack:
description: "CDK stack to deploy"
required: true
type: choice
options:
- api
- stream
- all

permissions:
id-token: write
contents: read

jobs:
check:
uses: ./.github/workflows/check.yml

deploy:
needs: check
if: github.event_name != 'pull_request' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
strategy:
max-parallel: 1
matrix:
stack: [api, stream]
steps:
- uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: Install dependencies
run: make poetry install
- name: Install AWS CDK CLI
run: npm install -g aws-cdk
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: Deploy stack
if: github.event_name != 'workflow_dispatch' || inputs.stack == 'all' || contains(inputs.stack, matrix.stack)
run: make deploy STACK=${{ matrix.stack }}
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.12
python-version: 3.14
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
Expand Down
1 change: 1 addition & 0 deletions .kiro/specs/cdk-deploy-destroy-targets/.config.kiro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"specId": "c5a2e92e-2f4f-462d-8fd8-b32efccb2871", "workflowType": "requirements-first", "specType": "feature"}
152 changes: 152 additions & 0 deletions .kiro/specs/cdk-deploy-destroy-targets/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Design Document: cdk-deploy-destroy-targets

## Overview

This feature adds `deploy` and `destroy` Make targets to the project Makefile, and a CDK app entry point (`infra/app.py`) that selects a single stack based on a `STACK` environment variable. Developers can run `make deploy STACK=api` or `make destroy STACK=stream` without knowing CDK CLI syntax or full stack construct IDs.

The two moving parts are:

1. `infra/app.py` — a minimal CDK app that reads `STACK` from the environment and instantiates only the requested stack.
2. Two new Makefile targets (`deploy`, `destroy`) that validate the `STACK` argument, build the CDK CLI invocation, and forward an optional `AWS_PROFILE`.

## Architecture

```mermaid
sequenceDiagram
participant Dev as Developer
participant Make as Makefile
participant CDK as cdk CLI
participant App as infra/app.py
participant AWS as AWS

Dev->>Make: make deploy STACK=api
Make->>Make: validate STACK ∈ {api, stream}
Make->>CDK: cdk deploy --app "python infra/app.py" ApiGatewayDynamodbStack [--profile ...]
CDK->>App: python infra/app.py (STACK=api in env)
App->>App: read STACK, instantiate ApiGatewayDynamodbStack
CDK->>AWS: deploy synthesised CloudFormation template
AWS-->>Dev: stack outputs
```

The Makefile is the sole entry point for developers. It owns argument validation and CDK CLI flag construction. `infra/app.py` is a thin adapter that maps the `STACK` env var to a stack class.

## Components and Interfaces

### 1. `infra/app.py` — CDK App Entry Point

Reads `STACK` from `os.environ`, looks it up in a registry dict, and calls `app.synth()`.

```
STACK_REGISTRY: dict[str, type[Stack]] = {
"api": ApiGatewayDynamodbStack,
"stream": DynamodbStreamStack,
}
```

- If `STACK` is missing or not in the registry → `sys.exit(1)` with a descriptive message.
- If `STACK` is valid → instantiate the stack with a fixed construct ID derived from the class name, then synth.

The construct ID passed to each stack is the class name (e.g. `ApiGatewayDynamodbStack`). This is the identifier that `cdk deploy` / `cdk destroy` targets.

### 2. Makefile targets

#### Stack name → CDK construct ID mapping

A Makefile associative array (or conditional block) maps short names to CDK construct IDs:

```makefile
STACK_MAP_api = ApiGatewayDynamodbStack
STACK_MAP_stream = DynamodbStreamStack
CDK_STACK = $(STACK_MAP_$(STACK))
```

Adding a new stack requires one new `STACK_MAP_<name>` line.

#### `deploy` target

```
deploy:
@[ -n "$(STACK)" ] || { echo "Usage: make deploy STACK=<api|stream>"; exit 1; }
@[ -n "$(CDK_STACK)" ] || { echo "Error: unknown stack '$(STACK)'"; exit 1; }
STACK=$(STACK) cdk deploy --app "python infra/app.py" $(CDK_STACK) \
$(if $(AWS_PROFILE),--profile $(AWS_PROFILE),)
```

#### `destroy` target

Same shape as `deploy` but calls `cdk destroy --force`.

#### Profile forwarding

Both targets use `$(if $(AWS_PROFILE),--profile $(AWS_PROFILE),)` so the flag is only appended when the variable is non-empty.

## Data Models

No persistent data models are introduced. The only runtime data is:

| Name | Type | Source | Description |
|---|---|---|---|
| `STACK` | `str` | env var / Make variable | Short stack name (`api` or `stream`) |
| `AWS_PROFILE` | `str` (optional) | Make variable | Named AWS CLI profile |
| `CDK_STACK` | `str` | Makefile expansion | Full CDK construct ID derived from `STACK` |
| `STACK_REGISTRY` | `dict[str, type[Stack]]` | `infra/app.py` | Maps short name → stack class |

## Correctness Properties

*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*

After prework analysis, requirements 1.1, 2.1, 4.1, and 4.2 all express the same invariant (valid name → correct class instantiated) and are consolidated into Property 1. Requirements 1.2, 1.3, 2.2, 2.3, and 4.3 all express the same invariant (invalid/missing name → non-zero exit) and are consolidated into Property 2. Requirements 3.1 and 3.2 are registry membership checks consolidated into a single example.

### Property 1: Valid stack name instantiates exactly the correct stack class

*For any* `(name, cls)` pair in `STACK_REGISTRY`, running `infra/app.py` with `STACK=name` must produce a CDK `App` whose stack list contains exactly one stack and that stack is an instance of `cls`.

**Validates: Requirements 1.1, 2.1, 3.1, 3.2, 4.1, 4.2**

### Property 2: Invalid or missing STACK causes non-zero exit

*For any* string that is not a key in `STACK_REGISTRY` (including the empty string), running `infra/app.py` with that value as `STACK` must raise `SystemExit` with a non-zero code and emit a descriptive error message.

**Validates: Requirements 1.2, 1.3, 2.2, 2.3, 4.3**

## Error Handling

| Scenario | Location | Behaviour |
|---|---|---|
| `STACK` not provided to `make deploy/destroy` | Makefile | Print usage message, `exit 1` |
| `STACK` value not in Makefile map | Makefile | Print "unknown stack" error, `exit 1` |
| `STACK` env var missing in `infra/app.py` | `infra/app.py` | `sys.exit(1)` with message |
| `STACK` env var unrecognised in `infra/app.py` | `infra/app.py` | `sys.exit(1)` with message listing valid names |
| `cdk deploy/destroy` fails | CDK CLI / shell | Non-zero exit propagates naturally (no `|| true`) |

The Makefile and the CDK app both validate `STACK` independently. The Makefile check is the first line of defence (fast, no Python startup cost); the app check is the safety net when the app is invoked directly.

## Testing Strategy

### Unit tests (`tests/infra/test_app.py`)

Focus on concrete examples and edge cases using `pytest` and `monkeypatch`:

- Example: `STACK=api` → app contains exactly one stack, instance of `ApiGatewayDynamodbStack`.
- Example: `STACK=stream` → app contains exactly one stack, instance of `DynamodbStreamStack`.
- Example: `STACK` unset → `SystemExit` raised with non-zero code.
- Example: `STACK=unknown` → `SystemExit` raised with non-zero code.
- Example: registry keys are exactly `{"api", "stream"}` (validates 3.1, 3.2).

### Property-based tests (`tests/infra/test_app_properties.py`)

Use **Hypothesis** (already a dev dependency). Each property test runs a minimum of 100 iterations.

Each test is tagged with a comment:
`# Feature: cdk-deploy-destroy-targets, Property <N>: <property_text>`

- **Property 1** — `@given(st.sampled_from(list(STACK_REGISTRY.items())))`: for any `(name, cls)` pair in the registry, the app produces exactly one stack that is an instance of `cls`.
`# Feature: cdk-deploy-destroy-targets, Property 1: valid stack name instantiates exactly the correct stack class`

- **Property 2** — `@given(st.text().filter(lambda s: s not in STACK_REGISTRY))`: for any string not in the registry, the app raises `SystemExit` with a non-zero code.
`# Feature: cdk-deploy-destroy-targets, Property 2: invalid or missing STACK causes non-zero exit`

### What is not tested here

- Actual CDK synthesis against AWS (integration concern, out of scope).
- Makefile shell logic — `--profile` forwarding and exit-code propagation are validated manually or via shell integration tests.
62 changes: 62 additions & 0 deletions .kiro/specs/cdk-deploy-destroy-targets/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Requirements Document

## Introduction

This feature adds `deploy` and `destroy` Make targets to the project's Makefile, enabling developers to deploy and tear down AWS Lambda stacks using AWS CDK from a single, consistent interface. The targets must support selecting which CDK stack to operate on (api or stream), accept an optional AWS profile, and integrate cleanly with the existing `make` workflow.

## Glossary

- **Makefile**: The project's top-level `Makefile` used to automate common developer workflows.
- **CDK**: AWS Cloud Development Kit — the infrastructure-as-code framework used under `infra/`.
- **Stack**: A deployable CDK unit. The project currently defines two stacks: `api` (ApiGatewayDynamodbStack) and `stream` (DynamodbStreamStack).
- **Stack_Name**: The logical identifier used to select a stack, corresponding to the stack module name under `infra/stacks/` (e.g., `api`, `stream`).
- **AWS_Profile**: An optional named AWS CLI profile used to authenticate CDK commands.
- **Deploy_Target**: The `deploy` Make target that synthesises and deploys a CDK stack to AWS.
- **Destroy_Target**: The `destroy` Make target that tears down a previously deployed CDK stack from AWS.

## Requirements

### Requirement 1: Deploy a CDK Stack

**User Story:** As a developer, I want to run `make deploy STACK=<stack>` to deploy a Lambda stack to AWS, so that I can provision infrastructure without remembering CDK CLI syntax.

#### Acceptance Criteria

1. WHEN the `deploy` target is invoked with a valid `STACK` value, THE Deploy_Target SHALL execute `cdk deploy` for the corresponding CDK stack.
2. WHEN the `deploy` target is invoked without a `STACK` value, THE Deploy_Target SHALL print a usage error message and exit with a non-zero status code.
3. WHEN the `deploy` target is invoked with a `STACK` value that does not match any defined stack, THE Deploy_Target SHALL print an error message identifying the invalid stack name and exit with a non-zero status code.
4. WHERE an `AWS_PROFILE` variable is provided, THE Deploy_Target SHALL pass the profile to the CDK CLI via the `--profile` flag.
5. WHEN `cdk deploy` fails, THE Deploy_Target SHALL propagate the non-zero exit code to the calling shell.

### Requirement 2: Destroy a CDK Stack

**User Story:** As a developer, I want to run `make destroy STACK=<stack>` to tear down a deployed Lambda stack, so that I can clean up AWS resources without remembering CDK CLI syntax.

#### Acceptance Criteria

1. WHEN the `destroy` target is invoked with a valid `STACK` value, THE Destroy_Target SHALL execute `cdk destroy` with the `--force` flag for the corresponding CDK stack.
2. WHEN the `destroy` target is invoked without a `STACK` value, THE Destroy_Target SHALL print a usage error message and exit with a non-zero status code.
3. WHEN the `destroy` target is invoked with a `STACK` value that does not match any defined stack, THE Destroy_Target SHALL print an error message identifying the invalid stack name and exit with a non-zero status code.
4. WHERE an `AWS_PROFILE` variable is provided, THE Destroy_Target SHALL pass the profile to the CDK CLI via the `--profile` flag.
5. WHEN `cdk destroy` fails, THE Destroy_Target SHALL propagate the non-zero exit code to the calling shell.

### Requirement 3: Stack Selection Mapping

**User Story:** As a developer, I want the Make targets to map short stack names (e.g., `api`, `stream`) to the correct CDK stack class names, so that I don't need to know the full CDK construct ID.

#### Acceptance Criteria

1. THE Makefile SHALL define a mapping from the `api` Stack_Name to the `ApiGatewayDynamodbStack` CDK stack identifier.
2. THE Makefile SHALL define a mapping from the `stream` Stack_Name to the `DynamodbStreamStack` CDK stack identifier.
3. WHEN a new stack module is added under `infra/stacks/`, THE Makefile SHALL require only a single-line addition to the mapping to support the new stack in the `deploy` and `destroy` targets.

### Requirement 4: CDK App Entry Point

**User Story:** As a developer, I want a CDK app entry point that instantiates the selected stack, so that `cdk deploy` and `cdk destroy` can target individual stacks without deploying all stacks at once.

#### Acceptance Criteria

1. THE CDK_App SHALL accept a `STACK` environment variable to determine which stack to instantiate.
2. WHEN the `STACK` environment variable is set to a valid Stack_Name, THE CDK_App SHALL instantiate only the corresponding CDK stack.
3. WHEN the `STACK` environment variable is not set or is set to an unrecognised value, THE CDK_App SHALL exit with a descriptive error message and a non-zero status code.
4. THE Makefile SHALL pass the `STACK` variable to the CDK app via the `--context` flag or environment variable so that the CDK app can resolve the correct stack.
52 changes: 52 additions & 0 deletions .kiro/specs/cdk-deploy-destroy-targets/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Implementation Plan: cdk-deploy-destroy-targets

## Overview

Create `infra/app.py` as the CDK app entry point and add `deploy`/`destroy` targets to the Makefile.

## Tasks

- [x] 1. Create `infra/app.py` CDK entry point
- Implement `STACK_REGISTRY` dict mapping `"api"` → `ApiGatewayDynamodbStack` and `"stream"` → `DynamodbStreamStack`
- Read `STACK` from `os.environ`; call `sys.exit(1)` with a descriptive message if missing or not in registry
- Instantiate the resolved stack class using the class name as the construct ID, then call `app.synth()`
- _Requirements: 4.1, 4.2, 4.3_

- [ ]* 1.1 Write unit tests for `infra/app.py` (`tests/infra/test_app.py`)
- Create `tests/infra/__init__.py` and `tests/infra/test_app.py`
- Test `STACK=api` → exactly one stack, instance of `ApiGatewayDynamodbStack`
- Test `STACK=stream` → exactly one stack, instance of `DynamodbStreamStack`
- Test `STACK` unset → `SystemExit` with non-zero code
- Test `STACK=unknown` → `SystemExit` with non-zero code
- Test registry keys are exactly `{"api", "stream"}` (validates 3.1, 3.2)
- End file with `if __name__ == "__main__": main()`
- _Requirements: 4.1, 4.2, 4.3, 3.1, 3.2_

- [ ]* 1.2 Write property test for `infra/app.py` (`tests/infra/test_app_properties.py`)
- **Property 1: Valid stack name instantiates exactly the correct stack class**
- `@given(st.sampled_from(list(STACK_REGISTRY.items())))` — for any `(name, cls)` pair, app produces exactly one stack that is an instance of `cls`
- **Validates: Requirements 1.1, 2.1, 3.1, 3.2, 4.1, 4.2**
- **Property 2: Invalid or missing STACK causes non-zero exit**
- `@given(st.text().filter(lambda s: s not in STACK_REGISTRY))` — for any string not in registry, app raises `SystemExit` with non-zero code
- **Validates: Requirements 1.2, 1.3, 2.2, 2.3, 4.3**
- Tag each test with the comment: `# Feature: cdk-deploy-destroy-targets, Property <N>: <property_text>`
- End file with `if __name__ == "__main__": main()`

- [x] 2. Checkpoint — Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.

- [x] 3. Add `deploy` and `destroy` targets to `Makefile`
- Add `STACK_MAP_api`, `STACK_MAP_stream`, and `CDK_STACK` variable definitions
- Add `deploy` target: validate `STACK` is set, validate `CDK_STACK` is non-empty, invoke `cdk deploy --app "python infra/app.py" $(CDK_STACK)` with optional `--profile $(AWS_PROFILE)`
- Add `destroy` target: same validation, invoke `cdk destroy --force --app "python infra/app.py" $(CDK_STACK)` with optional `--profile $(AWS_PROFILE)`
- Add `deploy` and `destroy` to `.PHONY`
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.1, 2.2, 2.3, 2.4, 2.5, 3.1, 3.2, 3.3_

- [x] 4. Final checkpoint — Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.

## Notes

- Tasks marked with `*` are optional and can be skipped for faster MVP
- Property tests use Hypothesis (already a dev dependency)
- The Makefile and `infra/app.py` both validate `STACK` independently — Makefile is the first line of defence
1 change: 1 addition & 0 deletions .kiro/specs/code-style-refactor/.config.kiro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"specId": "c5a2e92e-2f4f-462d-8fd8-b32efccb2871", "workflowType": "requirements-first", "specType": "feature"}
Loading