pulse-aws 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.3
2
+ Name: pulse-aws
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Erwin Kuhn
6
+ Author-email: Erwin Kuhn <erwin.kuhn@protonmail.com>
7
+ Requires-Dist: aws-cdk-lib>=2.160.0,<3.0
8
+ Requires-Dist: constructs>=10.0.0,<11.0.0
9
+ Requires-Dist: boto3>=1.35.0,<2.0.0
10
+ Requires-Dist: botocore>=1.35.0,<2.0.0
11
+ Requires-Dist: httpx>=0.27.0,<1.0.0
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+
15
+ # pulse-aws
16
+
17
+ AWS deployment utilities for Pulse applications on ECS Fargate.
18
+
19
+ ## Features
20
+
21
+ - **Zero-downtime deployments** with header-based sticky sessions
22
+ - **Automatic ACM certificate management** with DNS validation
23
+ - **DNS configuration detection** - automatically detects and guides you through DNS setup
24
+ - **Baseline infrastructure** as code using AWS CDK
25
+ - **Multi-version support** - run multiple deployments simultaneously
26
+
27
+ ## Quick Start
28
+
29
+ ```bash
30
+ # Install
31
+ uv add pulse-aws
32
+
33
+ # Deploy
34
+ uv run scripts/deploy.py
35
+ ```
36
+
37
+ ## Architecture
38
+
39
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for a detailed overview of:
40
+
41
+ - Infrastructure resources and how they relate
42
+ - Traffic routing with sticky sessions
43
+ - Zero-downtime deployment workflow
44
+ - Security architecture
45
+
46
+ ## Deployment Workflow
47
+
48
+ The deployment script orchestrates the full workflow:
49
+
50
+ ### 1. ACM Certificate
51
+
52
+ ```python
53
+ from pulse_aws import ensure_acm_certificate
54
+
55
+ cert = await ensure_acm_certificate("api.example.com")
56
+ ```
57
+
58
+ - Creates or retrieves an ACM certificate
59
+ - Provides DNS validation records if needed
60
+ - Waits for certificate to be ISSUED
61
+
62
+ ### 2. Baseline Infrastructure
63
+
64
+ ```python
65
+ from pulse_aws import ensure_baseline_stack
66
+
67
+ outputs = await ensure_baseline_stack(
68
+ "prod",
69
+ certificate_arn=cert.arn,
70
+ )
71
+ ```
72
+
73
+ Creates shared infrastructure:
74
+
75
+ - VPC with public/private subnets
76
+ - Application Load Balancer with HTTPS listener
77
+ - ECS Fargate cluster
78
+ - ECR repository
79
+ - CloudWatch log group
80
+ - Security groups
81
+
82
+ ### 3. DNS Configuration Check
83
+
84
+ ```python
85
+ from pulse_aws import check_domain_dns
86
+
87
+ dns_config = check_domain_dns(domain, alb_dns_name)
88
+ if dns_config:
89
+ print(dns_config.format_for_display())
90
+ ```
91
+
92
+ Automatically checks if your domain resolves to the ALB:
93
+
94
+ - ✅ **Already configured**: Silent success
95
+ - ✅ **Proxied through Cloudflare**: Treated as configured once records point to Cloudflare
96
+ - ⚠️ **Not configured**: Shows exact DNS record to add
97
+
98
+ Example output:
99
+
100
+ ```
101
+ ⚠️ Domain DNS Configuration Required
102
+ ============================================================
103
+
104
+ 🔗 Configure DNS for test.stoneware.rocks
105
+
106
+ Add the following records to your DNS provider:
107
+
108
+ • Type: CNAME
109
+ Name: test.stoneware.rocks
110
+ Value: test-alb-514905529.us-east-2.elb.amazonaws.com
111
+ (Route traffic to Application Load Balancer)
112
+
113
+ Once the records are added, your domain will be live within a few minutes.
114
+ ```
115
+
116
+ ### 4. Deploy Application
117
+
118
+ ```python
119
+ from pulse_aws import (
120
+ generate_deployment_id,
121
+ build_and_push_image,
122
+ register_task_definition,
123
+ create_service_and_target_group,
124
+ install_listener_rules_and_switch_traffic,
125
+ )
126
+
127
+ deployment_id = generate_deployment_id("prod")
128
+ image_uri = await build_and_push_image(...)
129
+ task_def_arn = await register_task_definition(...)
130
+ service_arn, tg_arn = await create_service_and_target_group(...)
131
+ await install_listener_rules_and_switch_traffic(...) # Waits for health checks
132
+ ```
133
+
134
+ - Builds and pushes Docker image to ECR (with correct x86_64 architecture)
135
+ - Registers ECS task definition with IAM roles
136
+ - Creates target group and attaches to ALB listener
137
+ - Creates ECS service with 2 Fargate tasks
138
+ - **Waits for targets to pass health checks (zero-downtime)**
139
+ - Switches default traffic to new deployment
140
+
141
+ ## Zero-Downtime Deployments
142
+
143
+ Each deployment gets a unique ID (e.g., `prod-20251027-122112Z`):
144
+
145
+ 1. **New deployment starts** - New tasks spin up alongside old tasks
146
+ 2. **Header-based routing** - ALB creates a rule: `X-Pulse-Render-Affinity: <deployment-id>` → target group
147
+ 3. **Default action switches** - New users get new version
148
+ 4. **Old sessions continue** - Existing users stick to old version via header
149
+ 5. **Drain old deployment** - When ready, call drain endpoint to shut down gracefully
150
+
151
+ ```bash
152
+ # Drain an old deployment
153
+ curl -X POST \
154
+ -H "Authorization: Bearer <drain-secret>" \
155
+ https://api.example.com/drain
156
+ ```
157
+
158
+ ## Configuration
159
+
160
+ ### Environment Variables
161
+
162
+ - `AWS_PROFILE` - AWS profile to use
163
+ - `AWS_REGION` - AWS region (or set in `~/.aws/config`)
164
+
165
+ ### Deployment Settings
166
+
167
+ ```python
168
+ # In your deploy script
169
+ deployment_name = "prod" # Used for resource naming
170
+ domain = "api.example.com" # Your domain
171
+ dockerfile_path = Path("Dockerfile") # Path to Dockerfile
172
+ ```
173
+
174
+ ## Security
175
+
176
+ **Defense in depth:**
177
+
178
+ - ALB in public subnets (internet-facing)
179
+ - ECS tasks in private subnets (no direct internet access)
180
+ - NAT gateway for task outbound internet
181
+ - ALB security group: Only 80/443 from internet
182
+ - Service security group: Only 8000 from ALB
183
+ - IAM roles with least privilege
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ # Run tests
189
+ uv run pytest packages/pulse-aws/tests/
190
+
191
+ # Deploy test environment
192
+ AWS_PROFILE=your-profile uv run packages/pulse-aws/scripts/deploy.py
193
+ ```
194
+
195
+ ## Troubleshooting
196
+
197
+ ### Certificate validation stuck
198
+
199
+ If certificate stays in `PENDING_VALIDATION`:
200
+
201
+ 1. Check DNS validation records are added correctly
202
+ 2. Wait 5-10 minutes for DNS propagation
203
+ 3. Use `dig` to verify: `dig _xxx.yourdomain.com CNAME`
204
+
205
+ ### Domain not accessible after deployment
206
+
207
+ 1. Check DNS record points to ALB: `dig yourdomain.com`
208
+ 2. Wait for DNS propagation (can take 5-60 minutes)
209
+ 3. Verify ALB is healthy: Visit ALB DNS directly
210
+
211
+ ### Tasks failing health checks
212
+
213
+ 1. Check logs: `aws logs tail /aws/pulse/{env}/app --follow`
214
+ 2. Verify tasks are listening on port 8000
215
+ 3. Check `/_health` endpoint returns 200
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,205 @@
1
+ # pulse-aws
2
+
3
+ AWS deployment utilities for Pulse applications on ECS Fargate.
4
+
5
+ ## Features
6
+
7
+ - **Zero-downtime deployments** with header-based sticky sessions
8
+ - **Automatic ACM certificate management** with DNS validation
9
+ - **DNS configuration detection** - automatically detects and guides you through DNS setup
10
+ - **Baseline infrastructure** as code using AWS CDK
11
+ - **Multi-version support** - run multiple deployments simultaneously
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Install
17
+ uv add pulse-aws
18
+
19
+ # Deploy
20
+ uv run scripts/deploy.py
21
+ ```
22
+
23
+ ## Architecture
24
+
25
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for a detailed overview of:
26
+
27
+ - Infrastructure resources and how they relate
28
+ - Traffic routing with sticky sessions
29
+ - Zero-downtime deployment workflow
30
+ - Security architecture
31
+
32
+ ## Deployment Workflow
33
+
34
+ The deployment script orchestrates the full workflow:
35
+
36
+ ### 1. ACM Certificate
37
+
38
+ ```python
39
+ from pulse_aws import ensure_acm_certificate
40
+
41
+ cert = await ensure_acm_certificate("api.example.com")
42
+ ```
43
+
44
+ - Creates or retrieves an ACM certificate
45
+ - Provides DNS validation records if needed
46
+ - Waits for certificate to be ISSUED
47
+
48
+ ### 2. Baseline Infrastructure
49
+
50
+ ```python
51
+ from pulse_aws import ensure_baseline_stack
52
+
53
+ outputs = await ensure_baseline_stack(
54
+ "prod",
55
+ certificate_arn=cert.arn,
56
+ )
57
+ ```
58
+
59
+ Creates shared infrastructure:
60
+
61
+ - VPC with public/private subnets
62
+ - Application Load Balancer with HTTPS listener
63
+ - ECS Fargate cluster
64
+ - ECR repository
65
+ - CloudWatch log group
66
+ - Security groups
67
+
68
+ ### 3. DNS Configuration Check
69
+
70
+ ```python
71
+ from pulse_aws import check_domain_dns
72
+
73
+ dns_config = check_domain_dns(domain, alb_dns_name)
74
+ if dns_config:
75
+ print(dns_config.format_for_display())
76
+ ```
77
+
78
+ Automatically checks if your domain resolves to the ALB:
79
+
80
+ - ✅ **Already configured**: Silent success
81
+ - ✅ **Proxied through Cloudflare**: Treated as configured once records point to Cloudflare
82
+ - ⚠️ **Not configured**: Shows exact DNS record to add
83
+
84
+ Example output:
85
+
86
+ ```
87
+ ⚠️ Domain DNS Configuration Required
88
+ ============================================================
89
+
90
+ 🔗 Configure DNS for test.stoneware.rocks
91
+
92
+ Add the following records to your DNS provider:
93
+
94
+ • Type: CNAME
95
+ Name: test.stoneware.rocks
96
+ Value: test-alb-514905529.us-east-2.elb.amazonaws.com
97
+ (Route traffic to Application Load Balancer)
98
+
99
+ Once the records are added, your domain will be live within a few minutes.
100
+ ```
101
+
102
+ ### 4. Deploy Application
103
+
104
+ ```python
105
+ from pulse_aws import (
106
+ generate_deployment_id,
107
+ build_and_push_image,
108
+ register_task_definition,
109
+ create_service_and_target_group,
110
+ install_listener_rules_and_switch_traffic,
111
+ )
112
+
113
+ deployment_id = generate_deployment_id("prod")
114
+ image_uri = await build_and_push_image(...)
115
+ task_def_arn = await register_task_definition(...)
116
+ service_arn, tg_arn = await create_service_and_target_group(...)
117
+ await install_listener_rules_and_switch_traffic(...) # Waits for health checks
118
+ ```
119
+
120
+ - Builds and pushes Docker image to ECR (with correct x86_64 architecture)
121
+ - Registers ECS task definition with IAM roles
122
+ - Creates target group and attaches to ALB listener
123
+ - Creates ECS service with 2 Fargate tasks
124
+ - **Waits for targets to pass health checks (zero-downtime)**
125
+ - Switches default traffic to new deployment
126
+
127
+ ## Zero-Downtime Deployments
128
+
129
+ Each deployment gets a unique ID (e.g., `prod-20251027-122112Z`):
130
+
131
+ 1. **New deployment starts** - New tasks spin up alongside old tasks
132
+ 2. **Header-based routing** - ALB creates a rule: `X-Pulse-Render-Affinity: <deployment-id>` → target group
133
+ 3. **Default action switches** - New users get new version
134
+ 4. **Old sessions continue** - Existing users stick to old version via header
135
+ 5. **Drain old deployment** - When ready, call drain endpoint to shut down gracefully
136
+
137
+ ```bash
138
+ # Drain an old deployment
139
+ curl -X POST \
140
+ -H "Authorization: Bearer <drain-secret>" \
141
+ https://api.example.com/drain
142
+ ```
143
+
144
+ ## Configuration
145
+
146
+ ### Environment Variables
147
+
148
+ - `AWS_PROFILE` - AWS profile to use
149
+ - `AWS_REGION` - AWS region (or set in `~/.aws/config`)
150
+
151
+ ### Deployment Settings
152
+
153
+ ```python
154
+ # In your deploy script
155
+ deployment_name = "prod" # Used for resource naming
156
+ domain = "api.example.com" # Your domain
157
+ dockerfile_path = Path("Dockerfile") # Path to Dockerfile
158
+ ```
159
+
160
+ ## Security
161
+
162
+ **Defense in depth:**
163
+
164
+ - ALB in public subnets (internet-facing)
165
+ - ECS tasks in private subnets (no direct internet access)
166
+ - NAT gateway for task outbound internet
167
+ - ALB security group: Only 80/443 from internet
168
+ - Service security group: Only 8000 from ALB
169
+ - IAM roles with least privilege
170
+
171
+ ## Development
172
+
173
+ ```bash
174
+ # Run tests
175
+ uv run pytest packages/pulse-aws/tests/
176
+
177
+ # Deploy test environment
178
+ AWS_PROFILE=your-profile uv run packages/pulse-aws/scripts/deploy.py
179
+ ```
180
+
181
+ ## Troubleshooting
182
+
183
+ ### Certificate validation stuck
184
+
185
+ If certificate stays in `PENDING_VALIDATION`:
186
+
187
+ 1. Check DNS validation records are added correctly
188
+ 2. Wait 5-10 minutes for DNS propagation
189
+ 3. Use `dig` to verify: `dig _xxx.yourdomain.com CNAME`
190
+
191
+ ### Domain not accessible after deployment
192
+
193
+ 1. Check DNS record points to ALB: `dig yourdomain.com`
194
+ 2. Wait for DNS propagation (can take 5-60 minutes)
195
+ 3. Verify ALB is healthy: Visit ALB DNS directly
196
+
197
+ ### Tasks failing health checks
198
+
199
+ 1. Check logs: `aws logs tail /aws/pulse/{env}/app --follow`
200
+ 2. Verify tasks are listening on port 8000
201
+ 3. Check `/_health` endpoint returns 200
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "pulse-aws"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [{ name = "Erwin Kuhn", email = "erwin.kuhn@protonmail.com" }]
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "aws-cdk-lib>=2.160.0,<3.0",
10
+ "constructs>=10.0.0,<11.0.0",
11
+ "boto3>=1.35.0,<2.0.0",
12
+ "botocore>=1.35.0,<2.0.0",
13
+ "httpx>=0.27.0,<1.0.0",
14
+ ]
15
+
16
+ [tool.uv]
17
+ package = true
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.9.4,<0.10.0"]
21
+ build-backend = "uv_build"
Binary file
@@ -0,0 +1,104 @@
1
+ # ########################
2
+ # ##### NOTES ON IMPORT FORMAT
3
+ # ########################
4
+ #
5
+ # This file defines Pulse's public API. Imports need to be structured/formatted so as to to ensure
6
+ # that the broadest possible set of static analyzers understand Pulse's public API as intended.
7
+ # The below guidelines ensure this is the case.
8
+ #
9
+ # (1) All imports in this module intended to define exported symbols should be of the form `from
10
+ # pulse.foo import X as X`. This is because imported symbols are not by default considered public
11
+ # by static analyzers. The redundant alias form `import X as X` overwrites the private imported `X`
12
+ # with a public `X` bound to the same value. It is also possible to expose `X` as public by listing
13
+ # it inside `__all__`, but the redundant alias form is preferred here due to easier maintainability.
14
+
15
+ # (2) All imports should target the module in which a symbol is actually defined, rather than a
16
+ # container module where it is imported.
17
+
18
+ from .baseline import (
19
+ BaselineStackError as BaselineStackError,
20
+ )
21
+ from .baseline import (
22
+ BaselineStackOutputs as BaselineStackOutputs,
23
+ )
24
+ from .baseline import (
25
+ check_domain_dns as check_domain_dns,
26
+ )
27
+ from .baseline import (
28
+ ensure_baseline_stack as ensure_baseline_stack,
29
+ )
30
+ from .certificate import (
31
+ AcmCertificate as AcmCertificate,
32
+ )
33
+ from .certificate import (
34
+ CertificateError as CertificateError,
35
+ )
36
+ from .certificate import (
37
+ DnsConfiguration as DnsConfiguration,
38
+ )
39
+ from .certificate import (
40
+ DnsRecord as DnsRecord,
41
+ )
42
+ from .certificate import (
43
+ domain_uses_cloudflare_proxy as domain_uses_cloudflare_proxy,
44
+ )
45
+ from .certificate import (
46
+ ensure_acm_certificate as ensure_acm_certificate,
47
+ )
48
+ from .config import (
49
+ DockerBuild as DockerBuild,
50
+ )
51
+ from .config import (
52
+ HealthCheckConfig as HealthCheckConfig,
53
+ )
54
+ from .config import (
55
+ ReaperConfig as ReaperConfig,
56
+ )
57
+ from .config import (
58
+ TaskConfig as TaskConfig,
59
+ )
60
+ from .deployment import (
61
+ DeploymentError as DeploymentError,
62
+ )
63
+ from .deployment import (
64
+ build_and_push_image as build_and_push_image,
65
+ )
66
+ from .deployment import (
67
+ create_service_and_target_group as create_service_and_target_group,
68
+ )
69
+ from .deployment import (
70
+ deploy as deploy,
71
+ )
72
+ from .deployment import (
73
+ generate_deployment_id as generate_deployment_id,
74
+ )
75
+ from .deployment import (
76
+ install_listener_rules_and_switch_traffic as install_listener_rules_and_switch_traffic,
77
+ )
78
+ from .deployment import (
79
+ register_task_definition as register_task_definition,
80
+ )
81
+ from .deployment import (
82
+ wait_for_healthy_targets as wait_for_healthy_targets,
83
+ )
84
+ from .plugin import (
85
+ AWSECSPlugin as AWSECSPlugin,
86
+ )
87
+ from .reporting import (
88
+ CiReporter as CiReporter,
89
+ )
90
+ from .reporting import (
91
+ CliReporter as CliReporter,
92
+ )
93
+ from .reporting import (
94
+ DeploymentContext as DeploymentContext,
95
+ )
96
+ from .reporting import (
97
+ Reporter as Reporter,
98
+ )
99
+ from .reporting import (
100
+ create_context as create_context,
101
+ )
102
+ from .teardown import (
103
+ teardown_baseline_stack as teardown_baseline_stack,
104
+ )