serverless-ircd 0.3.0 → 0.4.0
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.
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +174 -18
- package/README.md +63 -30
- package/apps/aws-stack/README.md +2 -2
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +20 -3
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +6 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +35 -2
- package/packages/aws-adapter/src/handlers/index.ts +69 -3
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +6 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +154 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +2 -4
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +111 -46
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +33 -9
- package/packages/cf-adapter/src/index.ts +9 -8
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/worker/main.ts +8 -1
- package/packages/cf-adapter/wrangler.test.toml +8 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +9 -0
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +58 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +181 -12
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +38 -1
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +158 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +12 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +400 -3
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.test.ts +4 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +132 -2
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +9 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -443
package/docs/AWS-Deployment.md
DELETED
|
@@ -1,1186 +0,0 @@
|
|
|
1
|
-
# AWS Deployment Guide
|
|
2
|
-
|
|
3
|
-
End-to-end guide for deploying ServerlessIRCd to AWS (Phase 4 of the
|
|
4
|
-
project plan). Covers prerequisites, first-time AWS setup, local
|
|
5
|
-
development, staging and production deploys, configuration reference,
|
|
6
|
-
DynamoDB capacity planning, region strategy, cost notes, CI/CD, and
|
|
7
|
-
troubleshooting.
|
|
8
|
-
|
|
9
|
-
Cross-reference: the AWS Adapter Architecture doc
|
|
10
|
-
(`../../ServerlessIRCd/docs/AWS-Adapter-Architecture.md`) for the
|
|
11
|
-
diagrams referenced throughout, and the Cloudflare Deployment Guide
|
|
12
|
-
(`../../ServerlessIRCd/docs/Cloudflare-Deployment-Guide.md`) for the
|
|
13
|
-
equivalent flow on the other supported platform. The shared
|
|
14
|
-
`irc-core`/`irc-server` packages and the contract test suite are
|
|
15
|
-
identical between platforms; only the I/O shell (this stack) differs.
|
|
16
|
-
|
|
17
|
-
Key in-repo paths cited below:
|
|
18
|
-
|
|
19
|
-
| Path | What |
|
|
20
|
-
|---------------------------------------------------|-----------------------------------------------|
|
|
21
|
-
| `apps/aws-stack/src/aws-stack.ts` | CDK stack — tables, Lambdas, APIGW, schedules.|
|
|
22
|
-
| `apps/aws-stack/bin/aws.ts` | CDK app entry. Stack id is `IrcAwsStack`. |
|
|
23
|
-
| `apps/aws-stack/cdk.json` | `app = tsx bin/aws.ts`. |
|
|
24
|
-
| `apps/aws-stack/scripts/smoke.mjs` | CI + local smoke e2e. |
|
|
25
|
-
| `apps/aws-stack/tests/stack.test.ts` | Synth-time assertions (cross-check for facts).|
|
|
26
|
-
| `packages/aws-adapter/src/cdk-table-defs.ts` | Five DynamoDB table shapes. |
|
|
27
|
-
| `packages/aws-adapter/src/config-loader.ts` | Lambda env → `ServerConfig` mapping. |
|
|
28
|
-
| `packages/aws-adapter/src/handlers/index.ts` | Lambda entry (`handler`, `sweeperHandler`, `pingCheckerHandler`). |
|
|
29
|
-
| `.github/workflows/deploy-aws.yml` | Staging deploy + smoke e2e CI. |
|
|
30
|
-
|
|
31
|
-
**Acceptance criterion:** a new contributor can deploy their own
|
|
32
|
-
staging instance following only this doc.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 1. What gets deployed
|
|
37
|
-
|
|
38
|
-
A single CDK v2 stack (`IrcAwsStack`) provisions the entire AWS
|
|
39
|
-
substrate. There are no static assets — CloudFront and S3 are not part
|
|
40
|
-
of this deployment. Every resource is regional.
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
┌──────────────────────────────────────────────────────────────────┐
|
|
44
|
-
│ CDK stack: IrcAwsStack (apps/aws-stack/src/aws-stack.ts) │
|
|
45
|
-
│ │
|
|
46
|
-
│ ┌─ WebSocketApi (AWS::ApiGatewayV2::Api) ───────────────────┐ │
|
|
47
|
-
│ │ routes: $connect $disconnect $default │ │
|
|
48
|
-
│ │ → all three wired to IrcHandler │ │
|
|
49
|
-
│ │ stage: prod (auto-deploy) │ │
|
|
50
|
-
│ └────────────────────────────────────────────────────────────┘ │
|
|
51
|
-
│ │ │
|
|
52
|
-
│ ▼ WebSocket event per frame │
|
|
53
|
-
│ ┌─ Lambda: IrcHandler ───────────────────────────────────────┐ │
|
|
54
|
-
│ │ handler export `handler` (Node 20, esbuild bundle) │ │
|
|
55
|
-
│ │ entry: packages/aws-adapter/src/handlers/index.ts │ │
|
|
56
|
-
│ │ dispatches on event.requestContext.routeKey │ │
|
|
57
|
-
│ └────────────────────────────────────────────────────────────┘ │
|
|
58
|
-
│ │
|
|
59
|
-
│ ┌─ Lambda: IrcSweeper ───────────────────────────────────────┐ │
|
|
60
|
-
│ │ handler export `sweeperHandler` │ │
|
|
61
|
-
│ │ EventBridge schedule: rate(5 minutes) │ │
|
|
62
|
-
│ │ Scans Connections for rows past hard TTL; cleans up. │ │
|
|
63
|
-
│ └────────────────────────────────────────────────────────────┘ │
|
|
64
|
-
│ │
|
|
65
|
-
│ ┌─ Lambda: IrcPingChecker ───────────────────────────────────┐ │
|
|
66
|
-
│ │ handler export `pingCheckerHandler` │ │
|
|
67
|
-
│ │ EventBridge schedule: rate(1 minute) │ │
|
|
68
|
-
│ │ Sends PING to idle connections; disconnects no-PONG. │ │
|
|
69
|
-
│ └────────────────────────────────────────────────────────────┘ │
|
|
70
|
-
│ │
|
|
71
|
-
│ ┌─ DynamoDB (on-demand) ─────────────────────────────────────┐ │
|
|
72
|
-
│ │ Connections (PK connectionId, TTL idleSince) │ │
|
|
73
|
-
│ │ ChannelMeta (PK channelName) │ │
|
|
74
|
-
│ │ ChannelMembers (PK channelName, SK connectionId) │ │
|
|
75
|
-
│ │ Nicks (PK nickLower) │ │
|
|
76
|
-
│ │ Accounts (PK account) │ │
|
|
77
|
-
│ └────────────────────────────────────────────────────────────┘ │
|
|
78
|
-
│ │
|
|
79
|
-
│ CfnOutput: ConnectUrl = stage.url (wss://…) │
|
|
80
|
-
│ CfnOutput: ManagementUrl = stage.callbackUrl (https://…) │
|
|
81
|
-
└──────────────────────────────────────────────────────────────────┘
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
All IRC protocol logic lives in the shared `irc-core` reducers and the
|
|
85
|
-
`irc-server` `ConnectionActor`. The Lambdas are a thin I/O shell: they
|
|
86
|
-
forward WebSocket events into the actor and interpret the resulting
|
|
87
|
-
`Effect[]` against DynamoDB and the API Gateway Management API. No IRC
|
|
88
|
-
state is held in the Lambda process between invocations.
|
|
89
|
-
|
|
90
|
-
The three Lambda functions share a single bundle (esbuild tree-shakes
|
|
91
|
-
per handler export at synth time). They run on `nodejs20.x` and exclude
|
|
92
|
-
`@aws-sdk/*` from the bundle because the AWS SDK v3 ships with the
|
|
93
|
-
Lambda Node 20 runtime.
|
|
94
|
-
|
|
95
|
-
Stack outputs (`CfnOutput`):
|
|
96
|
-
|
|
97
|
-
| Output | Value | Purpose |
|
|
98
|
-
|-----------------|-----------------------------|----------------------------------------------------|
|
|
99
|
-
| `ConnectUrl` | `wss://<id>.execute-api.<region>.amazonaws.com/prod` | WebSocket endpoint IRC clients dial. |
|
|
100
|
-
| `ManagementUrl` | `https://<id>.execute-api.<region>.amazonaws.com/prod` | HTTPS endpoint for `ApiGatewayManagementApi.postToConnection`. |
|
|
101
|
-
|
|
102
|
-
The same CloudFormation template serves both staging and production.
|
|
103
|
-
The staging/production distinction today is which AWS account and
|
|
104
|
-
region the stack is deployed into — there is no per-environment stack
|
|
105
|
-
parameter (see §6 and §7.4 for the implications).
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
## 2. Prerequisites
|
|
110
|
-
|
|
111
|
-
| Requirement | Version / detail |
|
|
112
|
-
|---------------------|---------------------------------------------------------------|
|
|
113
|
-
| Node.js | ≥ 20 (matches CI; `engines.node` in root `package.json` and in `apps/aws-stack/package.json`). |
|
|
114
|
-
| pnpm | 9.x (`packageManager: pnpm@9.15.9` in root `package.json`). |
|
|
115
|
-
| AWS account | IAM-permissioned to create CloudFormation stacks, IAM roles, API Gateway v2, Lambda, DynamoDB tables, and EventBridge rules. |
|
|
116
|
-
| AWS credentials | `aws configure`, `AWS_PROFILE`, or SSO. CDK reads the standard SDK chain. |
|
|
117
|
-
| CDK v2 | Comes from `apps/aws-stack/devDependencies` (`aws-cdk ^2.160.0`) — no global install needed. Use `pnpm cdk` (or `pnpm cdk:synth`) so the local binary wins over any globally-installed CDK. |
|
|
118
|
-
| CDK bootstrap | `cdk bootstrap` must have been run **once** in the target account/region (see §3.3). |
|
|
119
|
-
| Git checkout | Clean working tree on `main` for production deploys. |
|
|
120
|
-
|
|
121
|
-
Confirm the local environment:
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
node --version # v20.x or newer
|
|
125
|
-
pnpm --version # 9.x
|
|
126
|
-
aws --version # aws-cli/2.x (any 2.x is fine)
|
|
127
|
-
pnpm install # from repo root, installs the whole workspace
|
|
128
|
-
pnpm build # builds irc-core, irc-server, aws-adapter, aws-stack
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
`pnpm build` must succeed before `cdk deploy`: the `NodejsFunction`
|
|
132
|
-
construct transpiles the handler entry with esbuild at synth time, and
|
|
133
|
-
esbuild resolves `@serverless-ircd/*` workspace packages via their
|
|
134
|
-
compiled `dist/` outputs.
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## 3. First-time AWS setup
|
|
139
|
-
|
|
140
|
-
These steps are done once per AWS account.
|
|
141
|
-
|
|
142
|
-
### 3.1 Pick a region
|
|
143
|
-
|
|
144
|
-
API Gateway WebSocket endpoints, DynamoDB, EventBridge Scheduler, and
|
|
145
|
-
Lambda are all regional. WebSocket clients pay round-trip time to the
|
|
146
|
-
region, so pick the one closest to your user base (see §10 for the
|
|
147
|
-
full region discussion). Set it as your default:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
export AWS_REGION=us-east-1 # or your nearest region
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
CDK picks the region up via `CDK_DEFAULT_REGION` (set automatically by
|
|
154
|
-
the CDK CLI from the SDK chain).
|
|
155
|
-
|
|
156
|
-
### 3.2 Configure credentials
|
|
157
|
-
|
|
158
|
-
Three equivalent options:
|
|
159
|
-
|
|
160
|
-
```bash
|
|
161
|
-
# Static access key (simplest):
|
|
162
|
-
aws configure
|
|
163
|
-
# → prompts for Access Key ID, Secret Key, region, output format.
|
|
164
|
-
|
|
165
|
-
# Named profile:
|
|
166
|
-
export AWS_PROFILE=my-profile
|
|
167
|
-
|
|
168
|
-
# SSO:
|
|
169
|
-
aws sso login --profile my-sso-profile
|
|
170
|
-
export AWS_PROFILE=my-sso-profile
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Verify:
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
aws sts get-caller-identity
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### 3.3 Bootstrap CDK
|
|
180
|
-
|
|
181
|
-
CDK needs an S3 bucket + a few IAM roles per account/region to hold
|
|
182
|
-
synthesized CloudFormation assets. Bootstrap once:
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
cd apps/aws-stack
|
|
186
|
-
pnpm cdk bootstrap
|
|
187
|
-
# → creates a CFN stack named CDKToolkit with the bootstrap bucket.
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
If you skip this, the first `cdk deploy` fails with a bucket-not-found
|
|
191
|
-
error (see §13.1). Bootstrap is idempotent — re-running on an
|
|
192
|
-
already-bootstrapped account is a no-op.
|
|
193
|
-
|
|
194
|
-
### 3.4 Optional: set up OIDC for CI
|
|
195
|
-
|
|
196
|
-
If you intend to use the GitHub Actions deploy workflow (§12) without
|
|
197
|
-
long-lived access keys, configure GitHub OIDC:
|
|
198
|
-
|
|
199
|
-
1. Create an IAM identity provider for `token.actions.githubusercontent.com`.
|
|
200
|
-
2. Create a role with a trust policy granting
|
|
201
|
-
`sts:AssumeRoleWithWebIdentity` to your repo's `ref:refs/heads/main`.
|
|
202
|
-
3. Attach a permission policy broad enough to manage the stack (IAM,
|
|
203
|
-
API Gateway v2, Lambda, DynamoDB, EventBridge, CloudFormation).
|
|
204
|
-
4. Note the role ARN — it becomes the `AWS_DEPLOY_ROLE_ARN` secret in
|
|
205
|
-
CI. Static keys (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`) are
|
|
206
|
-
the fallback; the workflow supports both.
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
## 4. Local development
|
|
211
|
-
|
|
212
|
-
Unlike the Cloudflare adapter (which has `wrangler dev` against the
|
|
213
|
-
real `workerd` runtime), the AWS adapter does not yet ship a
|
|
214
|
-
hot-reload local Lambda/API Gateway emulator. The local dev loop is:
|
|
215
|
-
|
|
216
|
-
1. **Synth-time assertions** (default `pnpm test`) — fast, no Docker.
|
|
217
|
-
2. **Unit tests** for the handler, runtime, and access patterns under
|
|
218
|
-
`packages/aws-adapter/tests/`.
|
|
219
|
-
3. **Optional localstack validation** — gated, requires Docker.
|
|
220
|
-
|
|
221
|
-
### 4.1 Synth-time tests
|
|
222
|
-
|
|
223
|
-
From the repo root:
|
|
224
|
-
|
|
225
|
-
```bash
|
|
226
|
-
pnpm --filter @serverless-ircd/aws-stack test
|
|
227
|
-
# or, equivalently:
|
|
228
|
-
pnpm test # runs the whole workspace including aws-stack
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
These execute `Template.fromStack(...)` in-process — no AWS
|
|
232
|
-
credentials, no Docker. Every assertion in
|
|
233
|
-
`apps/aws-stack/tests/stack.test.ts` corresponds to a fact this guide
|
|
234
|
-
states: five on-demand tables, three Node 20 Lambdas, the
|
|
235
|
-
`$connect`/`$disconnect`/`$default` routes, the `prod` stage, the two
|
|
236
|
-
`rate(...)` schedules, the two CfnOutputs, and least-privilege IAM
|
|
237
|
-
(no `Action: "*"` or `Resource: "*"`).
|
|
238
|
-
|
|
239
|
-
### 4.2 Synthesize the CloudFormation template
|
|
240
|
-
|
|
241
|
-
```bash
|
|
242
|
-
pnpm --filter @serverless-ircd/aws-stack cdk:synth
|
|
243
|
-
# → writes cdk.out/<stack>.template.json
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Inspecting `cdk.out/` is the fastest way to see exactly what will be
|
|
247
|
-
deployed without touching AWS.
|
|
248
|
-
|
|
249
|
-
### 4.3 Optional: validate against localstack
|
|
250
|
-
|
|
251
|
-
A gated test (`apps/aws-stack/tests/localstack.test.ts`) runs
|
|
252
|
-
`cdklocal synth` against a real localstack container and re-asserts
|
|
253
|
-
the five-table shape. It is **skipped unless `LOCALSTACK=1` is set** so
|
|
254
|
-
CI never depends on Docker. To run it locally:
|
|
255
|
-
|
|
256
|
-
```bash
|
|
257
|
-
# 1. Start localstack (requires Docker):
|
|
258
|
-
docker run --rm -d -p 4566:4566 localstack/localstack
|
|
259
|
-
|
|
260
|
-
# 2. Install the localstack-aware CDK CLI globally:
|
|
261
|
-
npm install -g aws-cdk-local
|
|
262
|
-
|
|
263
|
-
# 3. Point the AWS SDK at localstack (cdklocal reads these):
|
|
264
|
-
export AWS_ACCESS_KEY_ID=test
|
|
265
|
-
export AWS_SECRET_ACCESS_KEY=test
|
|
266
|
-
export AWS_DEFAULT_REGION=us-east-1
|
|
267
|
-
|
|
268
|
-
# 4. Run the gated test:
|
|
269
|
-
LOCALSTACK=1 pnpm --filter @serverless-ircd/aws-stack test
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### 4.4 Running the smoke script locally
|
|
273
|
-
|
|
274
|
-
The smoke e2e (`apps/aws-stack/scripts/smoke.mjs`) defaults to
|
|
275
|
-
`ws://localhost:8787`, inherited from the Cloudflare adapter's dev
|
|
276
|
-
port. To smoke-test the AWS adapter locally you would need to stand up
|
|
277
|
-
an API Gateway WebSocket emulator in front of the Lambda handler
|
|
278
|
-
(sam-cli + localstack can do this); that wiring is not preconfigured.
|
|
279
|
-
In practice, local iteration uses the unit + synth tests, and the
|
|
280
|
-
smoke script is run against a deployed staging URL (§5.2) or against
|
|
281
|
-
the Cloudflare adapter's local dev server when you're iterating on the
|
|
282
|
-
shared protocol layer.
|
|
283
|
-
|
|
284
|
-
---
|
|
285
|
-
|
|
286
|
-
## 5. Deploy staging
|
|
287
|
-
|
|
288
|
-
Staging is the environment CI deploys on every push to `main`; you can
|
|
289
|
-
also deploy it manually from a clean checkout.
|
|
290
|
-
|
|
291
|
-
### 5.1 Deploy from your machine
|
|
292
|
-
|
|
293
|
-
```bash
|
|
294
|
-
# From the repo root:
|
|
295
|
-
pnpm deploy:aws:staging
|
|
296
|
-
# Equivalent to:
|
|
297
|
-
# pnpm --filter @serverless-ircd/aws-stack deploy:staging
|
|
298
|
-
# → cdk deploy --all --require-approval never
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
The first deploy of a given account/region will:
|
|
302
|
-
|
|
303
|
-
1. Upload the esbuild-bundled handler asset to the CDK bootstrap S3
|
|
304
|
-
bucket.
|
|
305
|
-
2. Create the `IrcAwsStack` CloudFormation stack.
|
|
306
|
-
3. Provision five DynamoDB tables, three Lambda functions, one API
|
|
307
|
-
Gateway WebSocket API, one `prod` stage, two EventBridge rules,
|
|
308
|
-
least-privilege IAM roles, and the two stack outputs.
|
|
309
|
-
4. Print the deployed URL on success — look for the `ConnectUrl`
|
|
310
|
-
output, e.g.
|
|
311
|
-
`wss://abc123.execute-api.us-east-1.amazonaws.com/prod`.
|
|
312
|
-
|
|
313
|
-
A typical first deploy takes 2–4 minutes (DynamoDB table creation and
|
|
314
|
-
API Gateway stage auto-deploy dominate).
|
|
315
|
-
|
|
316
|
-
### 5.2 Verify with the smoke script
|
|
317
|
-
|
|
318
|
-
```bash
|
|
319
|
-
node apps/aws-stack/scripts/smoke.mjs \
|
|
320
|
-
--url wss://abc123.execute-api.us-east-1.amazonaws.com/prod
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
The script replays CONNECT → NICK/USER → JOIN #smoke → PRIVMSG → QUIT
|
|
324
|
-
and asserts the server emits `001`, `376`, `353`, `366`, and closes
|
|
325
|
-
the socket. Expected output on success:
|
|
326
|
-
|
|
327
|
-
```json
|
|
328
|
-
{"ts":"...","level":"info","msg":"aws-stack smoke e2e passed","url":"wss://..."}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
On failure the script writes a `level: "error"` JSON line and exits
|
|
332
|
-
non-zero, which is what the CI workflow gates on.
|
|
333
|
-
|
|
334
|
-
### 5.3 Connect a real IRC client
|
|
335
|
-
|
|
336
|
-
Point any RFC-compliant WebSocket-aware IRC client at the deployed
|
|
337
|
-
`wss://` URL. For WeeChat:
|
|
338
|
-
|
|
339
|
-
```
|
|
340
|
-
/server add ircd abc123.execute-api.us-east-1.amazonaws.com/443
|
|
341
|
-
/set irc.server.ircd.ssl on
|
|
342
|
-
/connect ircd
|
|
343
|
-
/join #test
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
For a quick text-only check, the `apps/local-cli` and
|
|
347
|
-
`tools/tcp-ws-forwarder` packages in the parent repo bridge a plain
|
|
348
|
-
TCP IRC client to a WebSocket endpoint.
|
|
349
|
-
|
|
350
|
-
---
|
|
351
|
-
|
|
352
|
-
## 6. Deploy production
|
|
353
|
-
|
|
354
|
-
The repo-level shortcut is:
|
|
355
|
-
|
|
356
|
-
```bash
|
|
357
|
-
pnpm deploy:aws:prod
|
|
358
|
-
# Equivalent to:
|
|
359
|
-
# pnpm --filter @serverless-ircd/aws-stack deploy:prod
|
|
360
|
-
# → cdk deploy --all --require-approval never
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
**There is no separate production stack.** `deploy:prod` and
|
|
364
|
-
`deploy:staging` run the identical CDK command — both deploy a stack
|
|
365
|
-
named `IrcAwsStack` (hardcoded in `apps/aws-stack/bin/aws.ts`). The
|
|
366
|
-
staging/production distinction is **which AWS account and region the
|
|
367
|
-
stack is deployed into**, controlled by the credentials and
|
|
368
|
-
`AWS_REGION` in the environment.
|
|
369
|
-
|
|
370
|
-
For a real production deployment, before your first `pnpm deploy:aws:prod`:
|
|
371
|
-
|
|
372
|
-
1. **Use a dedicated AWS account** (or at least a dedicated region) for
|
|
373
|
-
production. Do not point production credentials at the same stack
|
|
374
|
-
CI is deploying to on every push.
|
|
375
|
-
2. **Change `RemovalPolicy.DESTROY` to `RemovalPolicy.RETAIN`** on the
|
|
376
|
-
five DynamoDB tables (see §9.4). The committed stack deletes all
|
|
377
|
-
data on `cdk destroy` — intentional for staging, dangerous for
|
|
378
|
-
production.
|
|
379
|
-
3. **Override the server identity** (`SERVER_NAME`, `NETWORK_NAME`,
|
|
380
|
-
`MOTD`) — see §7.1. The committed defaults are placeholders.
|
|
381
|
-
4. **Pick a region close to your users** (see §10).
|
|
382
|
-
|
|
383
|
-
There is **no CI auto-deploy to production**. The deploy workflow
|
|
384
|
-
(`deploy-aws.yml`) only triggers on push to `main` and targets
|
|
385
|
-
staging. Production deploys are always manual from a clean checkout.
|
|
386
|
-
|
|
387
|
-
---
|
|
388
|
-
|
|
389
|
-
## 7. Configuration reference
|
|
390
|
-
|
|
391
|
-
Server identity (`SERVER_NAME`, `NETWORK_NAME`, `MOTD`) and the logical
|
|
392
|
-
environment name are CDK construct props on `IrcAwsStack`
|
|
393
|
-
(`apps/aws-stack/src/aws-stack.ts`); pass them when instantiating the
|
|
394
|
-
stack. All other deployment knobs still live in the stack source.
|
|
395
|
-
Unlike the Cloudflare adapter (where config lives in a declarative
|
|
396
|
-
`wrangler.toml`), AWS config is code — non-identity overrides require
|
|
397
|
-
editing the stack and re-deploying.
|
|
398
|
-
|
|
399
|
-
### 7.1 Server identity (construct props)
|
|
400
|
-
|
|
401
|
-
The three identity values are sourced from `IrcStackProps` and joined
|
|
402
|
-
to the env vars the Lambda config loader reads. `motdLines` is a
|
|
403
|
-
`string[]`; the stack joins it with `\n` so the loader splits it back
|
|
404
|
-
into one entry per IRC numeric (`375` / `372` / `376`):
|
|
405
|
-
|
|
406
|
-
```ts
|
|
407
|
-
// apps/aws-stack/src/aws-stack.ts (excerpt)
|
|
408
|
-
const motd = (props.motdLines ?? DEFAULT_MOTD_LINES).join('\n');
|
|
409
|
-
handler.addEnvironment('SERVER_NAME', serverName);
|
|
410
|
-
handler.addEnvironment('NETWORK_NAME', networkName);
|
|
411
|
-
handler.addEnvironment('MOTD', motd);
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
These three values are injected into the `IrcHandler` and
|
|
415
|
-
`IrcPingChecker` Lambda environments at synth time. To override them,
|
|
416
|
-
pass the construct props when creating the stack:
|
|
417
|
-
|
|
418
|
-
```ts
|
|
419
|
-
new IrcAwsStack(app, 'IrcAwsStack-production', {
|
|
420
|
-
environmentName: 'production',
|
|
421
|
-
serverName: 'irc.my.net',
|
|
422
|
-
networkName: 'MyNet',
|
|
423
|
-
motdLines: ['Welcome to my network.', 'Be excellent to each other.'],
|
|
424
|
-
});
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
| Env var | Purpose | Committed default |
|
|
428
|
-
|-----------------|-----------------------------------------------|------------------------------------------------|
|
|
429
|
-
| `SERVER_NAME` | Server name sent in `001`/`005` numerics. | `irc.example.com` (placeholder — override!) |
|
|
430
|
-
| `NETWORK_NAME` | Network label in `005 NETWORK=…`. | `ExampleNet` |
|
|
431
|
-
| `MOTD` | Message-of-the-day. **Delimiter: `\n`** — the loader splits the value on every newline into one MOTD line. | Two-line banner (see note below). |
|
|
432
|
-
|
|
433
|
-
> **MOTD delimiter contract:** the config loader
|
|
434
|
-
> (`packages/aws-adapter/src/config-loader.ts`) splits `MOTD` on `\n`
|
|
435
|
-
> to form the multi-line MOTD array. The CDK stack's `motdLines`
|
|
436
|
-
> construct prop is a `string[]` joined with `\n`, so each array entry
|
|
437
|
-
> becomes its own line and round-trips back through the loader into the
|
|
438
|
-
> expected number of `motdLines`. Do **not** use a comma (or any other
|
|
439
|
-
> character) as a separator — the loader treats it as literal line
|
|
440
|
-
> content, collapsing the MOTD into a single line.
|
|
441
|
-
|
|
442
|
-
### 7.2 Lambda runtime config (injected by CDK, do not edit)
|
|
443
|
-
|
|
444
|
-
These env vars are wired automatically by the stack at synth time. Do
|
|
445
|
-
not hand-set them in the console — the next `cdk deploy` will
|
|
446
|
-
overwrite them.
|
|
447
|
-
|
|
448
|
-
| Env var | Source | Consumed by |
|
|
449
|
-
|------------------------|-------------------------------------------|--------------------------------------|
|
|
450
|
-
| `MANAGEMENT_URL` | `stage.callbackUrl` (CfnOutput `ManagementUrl`) | `IrcHandler`, `IrcPingChecker` (builds `ApiGatewayManagementApi` for `postToConnection`). |
|
|
451
|
-
| `CONNECTIONS_TABLE` | `Connections` (physical table name) | All three Lambdas (`tablesConfigFromEnv`). |
|
|
452
|
-
| `CHANNELMETA_TABLE` | `ChannelMeta` | All three Lambdas. |
|
|
453
|
-
| `CHANNELMEMBERS_TABLE` | `ChannelMembers` | All three Lambdas. |
|
|
454
|
-
| `NICKS_TABLE` | `Nicks` | All three Lambdas. |
|
|
455
|
-
| `ACCOUNTS_TABLE` | `Accounts` | All three Lambdas. |
|
|
456
|
-
| `AWS_REGION` | Injected by the Lambda runtime, not CDK. | `buildDepsFromEnv` (constructs the DynamoDB client). |
|
|
457
|
-
|
|
458
|
-
The `tablesConfigFromEnv` helper
|
|
459
|
-
(`packages/aws-adapter/src/handlers/index.ts:226`) throws on cold
|
|
460
|
-
start if any of the five `<NAME>_TABLE` vars is missing. The smoke
|
|
461
|
-
timeout in §13.2 traces back to this.
|
|
462
|
-
|
|
463
|
-
### 7.3 Optional config-loader env vars (read if present)
|
|
464
|
-
|
|
465
|
-
The shared config loader (`packages/aws-adapter/src/config-loader.ts`)
|
|
466
|
-
also reads these optional env vars and feeds them through the shared
|
|
467
|
-
Zod `ServerConfig` schema. **The committed CDK stack does not inject
|
|
468
|
-
any of them** — to enable one, add an `addEnvironment(...)` line in
|
|
469
|
-
`aws-stack.ts` and re-deploy.
|
|
470
|
-
|
|
471
|
-
| Env var | Type | Schema field |
|
|
472
|
-
|---------------------------|---------|-----------------------|
|
|
473
|
-
| `MAX_CLIENTS` | int | `maxClients` |
|
|
474
|
-
| `CHANNEL_PREFIXES` | string | `channelPrefixes` |
|
|
475
|
-
| `OPER_USER` | string | `operCreds[0].user` |
|
|
476
|
-
| `OPER_PASSWORD` | string | `operCreds[0].password` |
|
|
477
|
-
| `MAX_CHANNELS_PER_USER` | int | `maxChannelsPerUser` |
|
|
478
|
-
| `MAX_TARGETS_PER_COMMAND` | int | `maxTargetsPerCommand`|
|
|
479
|
-
| `NICK_LEN` | int | `nickLen` |
|
|
480
|
-
| `CHANNEL_LEN` | int | `channelLen` |
|
|
481
|
-
| `TOPIC_LEN` | int | `topicLen` |
|
|
482
|
-
| `MAX_LIST_ENTRIES` | int | `maxListEntries` |
|
|
483
|
-
| `QUIT_MESSAGE` | string | `quitMessage` |
|
|
484
|
-
| `SASL_ACCOUNTS` | string | `saslAccounts` |
|
|
485
|
-
|
|
486
|
-
`SASL_ACCOUNTS` is the legacy/config fallback (newline-delimited
|
|
487
|
-
`username:password` pairs); it is used only when the `Accounts` table is
|
|
488
|
-
empty. When the table has rows it is authoritative — see §8.1.
|
|
489
|
-
|
|
490
|
-
Invalid values cause a readable boot-time error — the Lambda will
|
|
491
|
-
throw on the first invocation (cold start). Test changes locally with
|
|
492
|
-
`pnpm --filter @serverless-ircd/aws-adapter test`.
|
|
493
|
-
|
|
494
|
-
### 7.4 No per-environment stack parameter
|
|
495
|
-
|
|
496
|
-
There is no `stageName` or `environmentType` parameter on the CDK
|
|
497
|
-
stack. The stack name (`IrcAwsStack`), table names
|
|
498
|
-
(`Connections`, `ChannelMeta`, `ChannelMembers`, `Nicks`, `Accounts`),
|
|
499
|
-
API Gateway stage name (`prod`), and Lambda construct ids are all
|
|
500
|
-
hardcoded. To run staging and production in **the same AWS account**,
|
|
501
|
-
you would need to fork the stack to parameterize these names; the
|
|
502
|
-
recommended path is to use separate accounts (or separate regions) for
|
|
503
|
-
staging vs production. This is the same architectural choice the
|
|
504
|
-
Cloudflare adapter makes (staging and production are separate Worker
|
|
505
|
-
namespaces).
|
|
506
|
-
|
|
507
|
-
### 7.5 Lambda runtime and bundling
|
|
508
|
-
|
|
509
|
-
| Knob | Value |
|
|
510
|
-
|-------------------|------------------------------------------------------------|
|
|
511
|
-
| Runtime | `nodejs20.x` (`Runtime.NODEJS_20_X`). |
|
|
512
|
-
| Entry | `packages/aws-adapter/src/handlers/index.ts` (computed by `handlerEntry()` in `aws-stack.ts`). |
|
|
513
|
-
| Handler exports | `IrcHandler` → `handler`; `IrcSweeper` → `sweeperHandler`; `IrcPingChecker` → `pingCheckerHandler`. |
|
|
514
|
-
| Bundler | esbuild (via `NodejsFunction`). |
|
|
515
|
-
| External modules | `@aws-sdk/*` (ships with the runtime; keeps the bundle small). |
|
|
516
|
-
| Memory / timeout | CDK defaults (128 MB / 3 s). Override via `memorySize`/`timeout` props if you observe cold-start or fanout timeouts. |
|
|
517
|
-
|
|
518
|
-
### 7.6 API Gateway stage
|
|
519
|
-
|
|
520
|
-
| Knob | Value |
|
|
521
|
-
|-------------------|------------------------------------------------------------|
|
|
522
|
-
| Stage name | `prod` (hardcoded as `STAGE_NAME` in `aws-stack.ts`). |
|
|
523
|
-
| Auto-deploy | `true` (changes propagate without a manual deployment). |
|
|
524
|
-
| Auth on `$connect`| None today (no authorizer). See §13.5. |
|
|
525
|
-
| Route responses | Default (`$connect` returns 200 → upgrade; `$default` and `$disconnect` are fire-and-forget). |
|
|
526
|
-
|
|
527
|
-
---
|
|
528
|
-
|
|
529
|
-
## 8. Secrets
|
|
530
|
-
|
|
531
|
-
**As of v1 the Lambda consumes no Secrets-Manager/SSM bindings.** Server
|
|
532
|
-
password and oper creds are handled by later hardening work; when they
|
|
533
|
-
ship, the natural homes are AWS Secrets Manager or SSM Parameter Store
|
|
534
|
-
(the AWS adapter architecture doc calls both out as supporting
|
|
535
|
-
services). SASL account credentials, however, **are** persisted — in the
|
|
536
|
-
`Accounts` DynamoDB table (see §8.1).
|
|
537
|
-
|
|
538
|
-
When secrets land, the typical pattern will be:
|
|
539
|
-
|
|
540
|
-
1. Create the secret in Secrets Manager (or a `SecureString`
|
|
541
|
-
parameter in SSM).
|
|
542
|
-
2. Grant the Lambda role `secretsmanager:GetSecretValue` (or
|
|
543
|
-
`ssm:GetParameter`) scoped to that one secret ARN.
|
|
544
|
-
3. Read it inside `buildDepsFromEnv` (cold start) — never inside the
|
|
545
|
-
per-frame hot path.
|
|
546
|
-
|
|
547
|
-
```bash
|
|
548
|
-
aws secretsmanager create-secret \
|
|
549
|
-
--name serverless-ircd/staging/SERVER_PASSWORD \
|
|
550
|
-
--secret-string "$(openssl rand -base64 32)"
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
### 8.1 SASL accounts (`Accounts` table + seed tooling)
|
|
554
|
-
|
|
555
|
-
SASL PLAIN credentials live in the `Accounts` DynamoDB table as
|
|
556
|
-
`HashedAccountCredential` rows — `{ account, algorithm, salt, hash }`,
|
|
557
|
-
all strings, with scrypt hashes encoded as base64 (never plaintext).
|
|
558
|
-
At Lambda cold start, `buildDepsFromEnv` → `resolveAccountStore`
|
|
559
|
-
scans the table once and pre-loads every row into a
|
|
560
|
-
`DynamoAccountStore`; the synchronous `verify()` called from the SASL
|
|
561
|
-
reducer works against that in-memory snapshot. The snapshot is rebuilt
|
|
562
|
-
on every cold start, so account changes take effect within one cold
|
|
563
|
-
start (seconds to minutes).
|
|
564
|
-
|
|
565
|
-
**Precedence** (`resolveAccountStore`, in `account-store.ts`):
|
|
566
|
-
|
|
567
|
-
1. The `Accounts` table is **authoritative when it has ≥1 row** — the
|
|
568
|
-
table wins and the config seed is ignored.
|
|
569
|
-
2. When the table is empty, the adapter falls back to the
|
|
570
|
-
`SASL_ACCOUNTS` env var (newline-delimited `username:password` pairs,
|
|
571
|
-
see §7.3) so existing deployments keep working without migrating.
|
|
572
|
-
3. When neither source has accounts, no `AccountStore` is bound and
|
|
573
|
-
`AUTHENTICATE PLAIN` returns `904 ERR_SASLFAIL`.
|
|
574
|
-
|
|
575
|
-
**Seed / CRUD tooling.** Provision accounts by writing
|
|
576
|
-
`HashedAccountCredential` rows produced by `hashAccountCredential`
|
|
577
|
-
(scrypt, random salt, base64-encoded). The adapter exports
|
|
578
|
-
`putAccountCredential(docClient, tableName, username, password)`
|
|
579
|
-
for this — a repeat call for the same `username` overwrites the row
|
|
580
|
-
(the PK is `account`). A minimal seeding snippet (Node 20, from the repo
|
|
581
|
-
root):
|
|
582
|
-
|
|
583
|
-
```bash
|
|
584
|
-
node --input-type=module -e '
|
|
585
|
-
import { createDynamoDocumentClient, putAccountCredential } from "@serverless-ircd/aws-adapter";
|
|
586
|
-
const dynamo = createDynamoDocumentClient({ region: "us-east-1" });
|
|
587
|
-
await putAccountCredential(dynamo, "StagingAccounts", "alice", "change-me");
|
|
588
|
-
console.log("seeded alice");
|
|
589
|
-
'
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
A repo-local seed script (`tools/seed-aws-accounts.ts`) wraps the same
|
|
593
|
-
calls for batch provisioning:
|
|
594
|
-
|
|
595
|
-
```bash
|
|
596
|
-
# Seed against DynamoDB Local:
|
|
597
|
-
npx tsx tools/seed-aws-accounts.ts \
|
|
598
|
-
--table StagingAccounts \
|
|
599
|
-
--endpoint http://localhost:8000 \
|
|
600
|
-
--accounts alice:s3cret bob:password2
|
|
601
|
-
|
|
602
|
-
# Or from a file (newline-delimited username:password):
|
|
603
|
-
npx tsx tools/seed-aws-accounts.ts \
|
|
604
|
-
--table StagingAccounts \
|
|
605
|
-
--region us-east-1 \
|
|
606
|
-
--file accounts.txt
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
For a one-off `aws-cli` write, hash the password out-of-band and put the
|
|
610
|
-
row directly:
|
|
611
|
-
|
|
612
|
-
```bash
|
|
613
|
-
# Hash with the adapter, then write the row yourself:
|
|
614
|
-
aws dynamodb put-item \
|
|
615
|
-
--table-name StagingAccounts \
|
|
616
|
-
--item '{"account":{"S":"alice"},"algorithm":{"S":"scrypt"},"salt":{"S":"<base64-salt>"},"hash":{"S":"<base64-hash>"}}'
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
> The `Accounts` table is granted to all three Lambdas (`handler`,
|
|
620
|
-
> `sweeper`, `pingChecker`) via `grantReadWriteData`; only the handler
|
|
621
|
-
> reads it at cold start. The sweeper/ping-checker never touch SASL
|
|
622
|
-
> state, but sharing the grant keeps the IAM story uniform.
|
|
623
|
-
|
|
624
|
-
**Never** put credentials, password hashes, or API tokens in
|
|
625
|
-
`aws-stack.ts`, in Lambda env vars, or in committed files. Lambda env
|
|
626
|
-
vars are visible in the console and in CloudFormation describe output;
|
|
627
|
-
they are not secret.
|
|
628
|
-
|
|
629
|
-
The CI deploy uses **GitHub Actions secrets** plus an optional repo
|
|
630
|
-
variable (see §12):
|
|
631
|
-
|
|
632
|
-
| Secret | Used by | Purpose |
|
|
633
|
-
|-------------------------|-------------------|---------------------------------------------------------------|
|
|
634
|
-
| `AWS_ACCESS_KEY_ID` | `deploy-aws.yml` | Static access key for CDK deploy. Required unless OIDC is used. |
|
|
635
|
-
| `AWS_SECRET_ACCESS_KEY` | `deploy-aws.yml` | Companion secret key. Required unless OIDC is used. |
|
|
636
|
-
| `AWS_DEPLOY_ROLE_ARN` | `deploy-aws.yml` | Optional. When set, the job assumes this role via OIDC web identity instead of using static keys. |
|
|
637
|
-
| `AWS_REGION` | `deploy-aws.yml` | Region to deploy into (e.g. `us-east-1`). |
|
|
638
|
-
|
|
639
|
-
| Variable | Purpose |
|
|
640
|
-
|-----------------|---------------------------------------------------------------------|
|
|
641
|
-
| `AWS_SMOKE_URL` | Optional. `wss://` URL for the smoke e2e step. If unset, the workflow falls back to reading the stack's `ConnectUrl` CloudFormation output via the AWS CLI; if that also fails the smoke step is skipped with a warning. |
|
|
642
|
-
|
|
643
|
-
Configure all of these under repo **Settings → Secrets and variables
|
|
644
|
-
→ Actions**.
|
|
645
|
-
|
|
646
|
-
---
|
|
647
|
-
|
|
648
|
-
## 9. DynamoDB capacity planning
|
|
649
|
-
|
|
650
|
-
### 9.1 All five tables run on-demand
|
|
651
|
-
|
|
652
|
-
Every table is created with `BillingMode.PAY_PER_REQUEST` (asserted in
|
|
653
|
-
`apps/aws-stack/tests/stack.test.ts:45`). On-demand pricing charges
|
|
654
|
-
per request:
|
|
655
|
-
|
|
656
|
-
- **Write request units (WRU)** — one WRU = one write of up to 1 KB
|
|
657
|
-
(or 2 WRU for a strongly-consistent write of up to 1 KB). A
|
|
658
|
-
conditional write that fails (e.g. `attribute_not_exists` rejects a
|
|
659
|
-
nick collision) still costs 1 WRU.
|
|
660
|
-
- **Read request units (RRU)** — one RRU = one eventually-consistent
|
|
661
|
-
read of up to 4 KB (or 2 RRU for strongly consistent).
|
|
662
|
-
|
|
663
|
-
There is no minimum spend and no capacity to plan at deploy time.
|
|
664
|
-
This is intentional for v1 — it removes a class of
|
|
665
|
-
`ProvisionedThroughputExceededException` failure modes. See §11 for
|
|
666
|
-
the cost trade-off and when to consider provisioned capacity.
|
|
667
|
-
|
|
668
|
-
### 9.2 Schema and access patterns
|
|
669
|
-
|
|
670
|
-
Five tables (defined in
|
|
671
|
-
`packages/aws-adapter/src/cdk-table-defs.ts`):
|
|
672
|
-
|
|
673
|
-
| Table | PK (HASH) | SK (RANGE) | TTL | Hottest access pattern |
|
|
674
|
-
|-----------------|-------------------|------------------|-------------|----------------------------------------------------------|
|
|
675
|
-
| `Connections` | `connectionId` | — | `idleSince` | `GetItem`/`UpdateItem` on every frame; `Scan` by sweeper. |
|
|
676
|
-
| `ChannelMeta` | `channelName` | — | — | `GetItem`/`UpdateItem` on MODE/TOPIC/KICK. Low volume. |
|
|
677
|
-
| `ChannelMembers`| `channelName` | `connectionId` | — | **`Query` on every channel PRIVMSG for fanout.** Hottest. |
|
|
678
|
-
| `Nicks` | `nickLower` | — | — | `PutItem` (conditional) on NICK; `GetItem` on PRIVMSG to a nick. |
|
|
679
|
-
| `Accounts` | `account` | — | — | `GetItem` on SASL AUTHENTICATE. Cold most of the time. |
|
|
680
|
-
|
|
681
|
-
Uniqueness invariants are enforced structurally (not in application
|
|
682
|
-
code):
|
|
683
|
-
|
|
684
|
-
- `Nicks.PK = nickLower` — conditional `PutItem` with
|
|
685
|
-
`attribute_not_exists(PK)` makes nick reservation atomic across
|
|
686
|
-
concurrent Lambdas. Two clients racing on `Alice` both get evaluated
|
|
687
|
-
against the same row; exactly one wins.
|
|
688
|
-
- `ChannelMembers` PK+SK composite — `TransactWriteItems` for JOIN/PART
|
|
689
|
-
updates `Connections.joined` and `ChannelMembers` in one atomic
|
|
690
|
-
write (up to 100 items per transaction, the AWS limit).
|
|
691
|
-
- `Accounts` row schema — `{ account (PK), algorithm, salt, hash }`,
|
|
692
|
-
all strings. `algorithm` is `'scrypt'`; `salt` and `hash` are
|
|
693
|
-
base64-encoded. Never plaintext. SASL PLAIN verifies against this
|
|
694
|
-
hash server-side. Seed rows with `putAccountCredential` (see §8.1).
|
|
695
|
-
|
|
696
|
-
### 9.3 The hottest pattern: channel fanout
|
|
697
|
-
|
|
698
|
-
Channel PRIVMSG is the dominant cost driver. There is no long-lived
|
|
699
|
-
per-channel process (contrast with the Cloudflare adapter's
|
|
700
|
-
`ChannelDO`). Fanout is computed per message:
|
|
701
|
-
|
|
702
|
-
1. The reducer emits `Broadcast("#chan", [line], except=sender)`.
|
|
703
|
-
2. `AwsRuntime.broadcast()` issues a `Query` on `ChannelMembers` with
|
|
704
|
-
`PK = "#chan"` → returns one row per member.
|
|
705
|
-
3. For each member, the Lambda calls
|
|
706
|
-
`ApiGatewayManagementApi.postToConnection(member, line)`.
|
|
707
|
-
|
|
708
|
-
That's O(members) Management API calls per message. A 100-member
|
|
709
|
-
channel receiving one PRIVMSG per second produces ~100 `postToConnection`
|
|
710
|
-
calls/second and one `Query`/second. A 1k-member announcement channel
|
|
711
|
-
at the same rate produces ~1k calls/second. Mitigations (batched
|
|
712
|
-
postToConnection, per-channel send-list cache, a dedicated fanout
|
|
713
|
-
Lambda) are deferred to post-v1; the load-test ticket will surface
|
|
714
|
-
the ceiling.
|
|
715
|
-
|
|
716
|
-
### 9.4 Removal policy (IMPORTANT)
|
|
717
|
-
|
|
718
|
-
The stack sets `RemovalPolicy.DESTROY` on every table:
|
|
719
|
-
|
|
720
|
-
```ts
|
|
721
|
-
// apps/aws-stack/src/aws-stack.ts:48
|
|
722
|
-
const tables = Object.entries(TABLE_DEFS).map(
|
|
723
|
-
([logicalId, tableProps]) =>
|
|
724
|
-
new Table(this, logicalId, { ...tableProps, removalPolicy: RemovalPolicy.DESTROY }),
|
|
725
|
-
);
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
**This means `cdk destroy` deletes every table and all of its data.**
|
|
729
|
-
This is intentional for staging (you want a clean teardown between
|
|
730
|
-
experiments) and **dangerous for production**.
|
|
731
|
-
|
|
732
|
-
For production, fork the stack to use `RemovalPolicy.RETAIN` (or
|
|
733
|
-
`RETAIN_ON_UPDATE`): tables become orphans on stack deletion and keep
|
|
734
|
-
their data. There is no built-in knob for this today — it requires
|
|
735
|
-
editing `aws-stack.ts`. Recommendation: ship a `production: boolean`
|
|
736
|
-
stack prop in a future change that flips the removal policy. Flagged
|
|
737
|
-
in §14.
|
|
738
|
-
|
|
739
|
-
### 9.5 TTL on `Connections.idleSince`
|
|
740
|
-
|
|
741
|
-
The `Connections` table carries a DynamoDB TTL attribute (`idleSince`),
|
|
742
|
-
asserted at synth time (`stack.test.ts:52`). The Lambda updates
|
|
743
|
-
`idleSince` on every frame; DynamoDB silently deletes rows whose
|
|
744
|
-
`idleSince` is older than the TTL window (typically within 48 hours of
|
|
745
|
-
expiry, no SLA). This is a **safety net** for rows the sweeper misses.
|
|
746
|
-
|
|
747
|
-
### 9.6 The gone-connection sweeper
|
|
748
|
-
|
|
749
|
-
A second Lambda (`IrcSweeper`, handler export `sweeperHandler`) runs on
|
|
750
|
-
an EventBridge schedule at `rate(5 minutes)`:
|
|
751
|
-
|
|
752
|
-
```ts
|
|
753
|
-
new Rule(this, 'SweeperSchedule', {
|
|
754
|
-
schedule: Schedule.rate(Duration.minutes(5)),
|
|
755
|
-
targets: [new LambdaFunction(sweeper)],
|
|
756
|
-
});
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
Each tick `Scan`s the entire `Connections` table, identifies rows past
|
|
760
|
-
a hard stale threshold, and runs the same cleanup path as
|
|
761
|
-
`$disconnect` (delete memberships, release nicks, delete the row). The
|
|
762
|
-
sweeper catches connections that vanished without APIGW emitting
|
|
763
|
-
`$disconnect` (e.g. TCP RST that APIGW didn't observe).
|
|
764
|
-
|
|
765
|
-
**Cost note:** `Scan` reads the **entire** table every 5 minutes. At
|
|
766
|
-
1 KB per row, scanning 10k connections costs ~3 RRU (10k / 4 KB per
|
|
767
|
-
RRU) per tick, ~864 ticks/day, ~2.6k RRU/day — negligible. At 100k
|
|
768
|
-
connections it's ~26k RRU/day — still small but visible. A future
|
|
769
|
-
optimization is to make the sweeper `Query` a sparse GSI keyed by
|
|
770
|
-
`idleSince` instead of `Scan`.
|
|
771
|
-
|
|
772
|
-
### 9.7 When to consider provisioned capacity
|
|
773
|
-
|
|
774
|
-
Switch a table to `PROVISIONED` billing (with or without autoscaling)
|
|
775
|
-
when its traffic is predictable and steady. The clearest signal is
|
|
776
|
-
when the on-demand bill for a single table exceeds the cost of
|
|
777
|
-
provisioned capacity for the same throughput — DynamoDB's pricing page
|
|
778
|
-
breaks this out. In practice:
|
|
779
|
-
|
|
780
|
-
- `ChannelMembers` is the first candidate (channel fanout dominates).
|
|
781
|
-
- `Connections` is a poor candidate (spiky, cold-start-driven).
|
|
782
|
-
- `Accounts`/`ChannelMeta`/`Nicks` are unlikely to need provisioned
|
|
783
|
-
capacity in any v1-size deployment.
|
|
784
|
-
|
|
785
|
-
Switching billing modes is a no-downtime `UpdateTable` operation; the
|
|
786
|
-
stack change is `billingMode: BillingMode.PROVISIONED` plus
|
|
787
|
-
`readCapacity`/`writeCapacity` props.
|
|
788
|
-
|
|
789
|
-
---
|
|
790
|
-
|
|
791
|
-
## 10. Region strategy
|
|
792
|
-
|
|
793
|
-
### 10.1 Everything is regional
|
|
794
|
-
|
|
795
|
-
API Gateway WebSocket endpoints are regional. DynamoDB is regional.
|
|
796
|
-
EventBridge Scheduler is regional. Lambda is regional. The stack
|
|
797
|
-
deploys into exactly one region (the one your credentials resolve to
|
|
798
|
-
via `CDK_DEFAULT_REGION`).
|
|
799
|
-
|
|
800
|
-
WebSocket clients pay round-trip time to that region. A user in
|
|
801
|
-
Frankfurt connecting to a `us-east-1` deployment will see ~90 ms RTT
|
|
802
|
-
on every PRIVMSG roundtrip; the same user against `eu-central-1` will
|
|
803
|
-
see ~10 ms. **Pick the region closest to your user base.**
|
|
804
|
-
|
|
805
|
-
### 10.2 Recommendation: single-region for v1
|
|
806
|
-
|
|
807
|
-
For v1, deploy in one region. There are no static assets (CloudFront
|
|
808
|
-
and S3 are not part of this stack), so a CDN adds no value here.
|
|
809
|
-
Single-region keeps the data model simple: the cross-table
|
|
810
|
-
`TransactWriteItems` flows (§9.2) only work within one region.
|
|
811
|
-
|
|
812
|
-
### 10.3 Multi-region is out of scope for v1
|
|
813
|
-
|
|
814
|
-
If you ever need multi-region active-active, the considerations are:
|
|
815
|
-
|
|
816
|
-
- **DynamoDB global tables** — replicate all five tables. Eventual
|
|
817
|
-
consistency across regions complicates the conditional-write
|
|
818
|
-
invariants (nick uniqueness, membership transactions): a nick
|
|
819
|
-
reserved in region A may not be visible in region B for a second or
|
|
820
|
-
two, allowing duplicate reservations. The reducer logic would need
|
|
821
|
-
to tolerate this.
|
|
822
|
-
- **Per-region APIGW + Lambda** — each region runs the full stack;
|
|
823
|
-
clients are routed by DNS (Route 53 latency-based routing).
|
|
824
|
-
- **Session affinity** — a client's `connectionId` is regional. A
|
|
825
|
-
reconnect after the 2-hour APIGW cap must land on the same region
|
|
826
|
-
or treat the new connection as a fresh registration (re-NICK,
|
|
827
|
-
re-JOIN).
|
|
828
|
-
|
|
829
|
-
This is post-v1 work; the AWS adapter architecture doc calls it out
|
|
830
|
-
as an open question.
|
|
831
|
-
|
|
832
|
-
### 10.4 Region pick shortlist
|
|
833
|
-
|
|
834
|
-
For a globally distributed user base, the typical picks:
|
|
835
|
-
|
|
836
|
-
- **US-East-1 (N. Virginia)** — newest services, cheapest, ~100 ms to
|
|
837
|
-
EU and ~150 ms to APAC. Default for the CI workflow's `AWS_REGION`.
|
|
838
|
-
- **EU-West-1 (Ireland)** / **EU-Central-1 (Frankfurt)** — best for
|
|
839
|
-
EU users.
|
|
840
|
-
- **AP-Northeast-1 (Tokyo)** / **AP-Southeast-1 (Singapore)** — best
|
|
841
|
-
for APAC users.
|
|
842
|
-
|
|
843
|
-
---
|
|
844
|
-
|
|
845
|
-
## 11. Cost notes
|
|
846
|
-
|
|
847
|
-
All figures are the public AWS list price as of late 2024 — **always
|
|
848
|
-
verify against the current AWS pricing page** before budgeting:
|
|
849
|
-
|
|
850
|
-
- API Gateway WebSocket: <https://aws.amazon.com/apigateway/pricing/>
|
|
851
|
-
- Lambda: <https://aws.amazon.com/lambda/pricing/>
|
|
852
|
-
- DynamoDB: <https://aws.amazon.com/dynamodb/pricing/>
|
|
853
|
-
- EventBridge: <https://aws.amazon.com/eventbridge/pricing/>
|
|
854
|
-
|
|
855
|
-
### 11.1 Billable services
|
|
856
|
-
|
|
857
|
-
| Service | Unit | List price (late 2024) |
|
|
858
|
-
|--------------------------|------------------------------------------------|-------------------------------|
|
|
859
|
-
| API Gateway WebSocket | connection-minute | $0.80 / million |
|
|
860
|
-
| API Gateway WebSocket | message (inbound or outbound) | $1.00 / million |
|
|
861
|
-
| Lambda | request | $0.20 / million |
|
|
862
|
-
| Lambda | GB-second | $0.0000166667 (≈ $0.0167 / GB-hour) |
|
|
863
|
-
| DynamoDB on-demand | write request unit (WRU) | $1.25 / million |
|
|
864
|
-
| DynamoDB on-demand | read request unit (RRU) | $0.25 / million |
|
|
865
|
-
| EventBridge Scheduler | event published | $1.00 / million |
|
|
866
|
-
| CloudWatch Logs | GB ingested | $0.50 |
|
|
867
|
-
|
|
868
|
-
The Lambda free tier (1M requests + 400k GB-seconds per month) covers
|
|
869
|
-
a meaningful slice of a small staging deployment.
|
|
870
|
-
|
|
871
|
-
### 11.2 Back-of-envelope: 100 concurrent users
|
|
872
|
-
|
|
873
|
-
Assume a small staging deployment with 100 concurrent users, average
|
|
874
|
-
5 messages/sec across all channels, each connection active for 1 hour,
|
|
875
|
-
average Lambda invocation 50 ms at 128 MB:
|
|
876
|
-
|
|
877
|
-
| Dimension | Estimate | Monthly cost (approx) |
|
|
878
|
-
|------------------------------------------|---------------------------------------|-----------------------|
|
|
879
|
-
| APIGW connection-minutes | 100 conn × 60 min × 24 h × 30 d = 4.32M conn-min | ~$3.50 |
|
|
880
|
-
| APIGW messages (inbound + fanout) | 5 msg/s × 86400 × 30 = 13M inbound, ~3× fanout | ~$50 |
|
|
881
|
-
| Lambda invocations | ~1 per inbound + fanout ≈ 40M | ~$8.00 (free tier offsets some) |
|
|
882
|
-
| Lambda GB-seconds | 40M × 0.05 s × 0.125 GB = 250k GB-s | ~$4.20 |
|
|
883
|
-
| DynamoDB writes (Connections updates + ChannelMembers deltas) | ~5M WRU | ~$6.25 |
|
|
884
|
-
| DynamoDB reads (fanout Queries + Connections GetItem) | ~30M RRU | ~$7.50 |
|
|
885
|
-
| EventBridge events (sweeper + pingChecker) | 5-min + 1-min ticks ≈ 50k events | <$0.10 |
|
|
886
|
-
| CloudWatch Logs | ~5 GB | ~$2.50 |
|
|
887
|
-
|
|
888
|
-
**Rough total: ~$80/month** for a 100-concurrent-user staging
|
|
889
|
-
deployment. The dominant cost is **APIGW messages** (inbound + the
|
|
890
|
-
per-member fanout). The single biggest lever is channel size — a
|
|
891
|
-
single 1k-member announcement channel can multiply the fanout cost by
|
|
892
|
-
10×.
|
|
893
|
-
|
|
894
|
-
### 11.3 Cost-trim levers
|
|
895
|
-
|
|
896
|
-
- **Cap channel size** with `+l` mode (the existing MODE enforcement).
|
|
897
|
-
- **Consider provisioned capacity** for `ChannelMembers` once traffic
|
|
898
|
-
is steady (§9.7).
|
|
899
|
-
- **Shorten the sweeper cadence** if you can tolerate slower cleanup —
|
|
900
|
-
`rate(5 minutes)` is a balance; `rate(15 minutes)` cuts sweeper
|
|
901
|
-
cost 3× at the cost of slower gone-connection reaping.
|
|
902
|
-
- **Tune Lambda memory** — 128 MB is the default and cheapest per
|
|
903
|
-
invocation, but a higher memory allocation can reduce duration
|
|
904
|
-
enough to win on GB-seconds.
|
|
905
|
-
|
|
906
|
-
---
|
|
907
|
-
|
|
908
|
-
## 12. CI/CD
|
|
909
|
-
|
|
910
|
-
The GitHub Actions workflow at `.github/workflows/deploy-aws.yml`
|
|
911
|
-
deploys staging on every push to `main` and replays the smoke e2e. It
|
|
912
|
-
is the canonical deploy path — manual `pnpm deploy:aws:staging` is for
|
|
913
|
-
iteration only.
|
|
914
|
-
|
|
915
|
-
### 12.1 What the workflow does
|
|
916
|
-
|
|
917
|
-
Steps performed (in order):
|
|
918
|
-
|
|
919
|
-
1. Checkout.
|
|
920
|
-
2. Install pnpm 9 + Node 20 (`cache: pnpm`).
|
|
921
|
-
3. `pnpm install --frozen-lockfile`.
|
|
922
|
-
4. `pnpm build` — builds all workspace packages.
|
|
923
|
-
5. `pnpm typecheck` and `pnpm test` — gate the deploy.
|
|
924
|
-
6. Configure AWS credentials (OIDC if `AWS_DEPLOY_ROLE_ARN` is set,
|
|
925
|
-
otherwise static keys).
|
|
926
|
-
7. `pnpm deploy:aws:staging` → `cdk deploy --all --require-approval never`.
|
|
927
|
-
8. Resolve the smoke URL: repo variable `AWS_SMOKE_URL` first, else
|
|
928
|
-
the stack's `ConnectUrl` CloudFormation output via
|
|
929
|
-
`aws cloudformation describe-stacks --stack-name IrcAwsStack`, else
|
|
930
|
-
skip the smoke step with a warning.
|
|
931
|
-
9. `pnpm smoke:aws:staging -- --url "$SMOKE_URL"` — failure fails the
|
|
932
|
-
build.
|
|
933
|
-
|
|
934
|
-
Concurrency is serialized via `concurrency.group: aws-staging`,
|
|
935
|
-
`cancel-in-progress: false`, so two pushes cannot race the same
|
|
936
|
-
CloudFormation stack.
|
|
937
|
-
|
|
938
|
-
### 12.2 Required repo configuration
|
|
939
|
-
|
|
940
|
-
Under **Settings → Secrets and variables → Actions**:
|
|
941
|
-
|
|
942
|
-
- **Secret `AWS_ACCESS_KEY_ID`** — required unless OIDC.
|
|
943
|
-
- **Secret `AWS_SECRET_ACCESS_KEY`** — required unless OIDC.
|
|
944
|
-
- **Secret `AWS_DEPLOY_ROLE_ARN`** — optional; enables OIDC.
|
|
945
|
-
- **Secret `AWS_REGION`** — required (e.g. `us-east-1`).
|
|
946
|
-
- **Variable `AWS_SMOKE_URL`** — optional but recommended; the
|
|
947
|
-
workflow can fall back to the CFN output, but setting this variable
|
|
948
|
-
explicitly avoids the `describe-stacks` round-trip and the
|
|
949
|
-
possibility of a stale cached value after a redeploy.
|
|
950
|
-
|
|
951
|
-
### 12.3 OIDC vs static keys
|
|
952
|
-
|
|
953
|
-
The workflow supports both auth modes:
|
|
954
|
-
|
|
955
|
-
```yaml
|
|
956
|
-
- name: Configure AWS credentials
|
|
957
|
-
uses: aws-actions/configure-aws-credentials@v4
|
|
958
|
-
with:
|
|
959
|
-
aws-region: ${{ secrets.AWS_REGION }}
|
|
960
|
-
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
961
|
-
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
962
|
-
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
963
|
-
```
|
|
964
|
-
|
|
965
|
-
When `AWS_DEPLOY_ROLE_ARN` is set, the action assumes the role via
|
|
966
|
-
OIDC web identity (requires `permissions.id-token: write`, which the
|
|
967
|
-
workflow declares) and ignores the static keys. When unset, it falls
|
|
968
|
-
back to the static access-key pair. OIDC is the recommended path for
|
|
969
|
-
any deployment tied to a real AWS account — no long-lived credentials
|
|
970
|
-
to rotate.
|
|
971
|
-
|
|
972
|
-
### 12.4 No CI auto-deploy to production
|
|
973
|
-
|
|
974
|
-
Production deploys are intentionally manual (`pnpm deploy:aws:prod`
|
|
975
|
-
from a clean checkout on `main`, against separate credentials). The
|
|
976
|
-
workflow's `on:` block only triggers on `push` to `main` and
|
|
977
|
-
`workflow_dispatch`, both of which target staging.
|
|
978
|
-
|
|
979
|
-
---
|
|
980
|
-
|
|
981
|
-
## 13. Troubleshooting
|
|
982
|
-
|
|
983
|
-
### 13.1 `cdk deploy` fails with bucket-not-found
|
|
984
|
-
|
|
985
|
-
> `BUCKET_NAME_PARAMETER_<id> does not exist` or
|
|
986
|
-
> `NoSuchBucket: The specified bucket does not exist`
|
|
987
|
-
|
|
988
|
-
The CDK bootstrap stack hasn't been created in this account/region.
|
|
989
|
-
|
|
990
|
-
```bash
|
|
991
|
-
cd apps/aws-stack
|
|
992
|
-
pnpm cdk bootstrap
|
|
993
|
-
pnpm deploy:aws:staging
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
Bootstrap is idempotent and per-region — re-run it if you change
|
|
997
|
-
regions.
|
|
998
|
-
|
|
999
|
-
### 13.2 Lambda throws on cold start because of a missing env var
|
|
1000
|
-
|
|
1001
|
-
Symptom: smoke e2e times out waiting for `001`; CloudWatch Logs for
|
|
1002
|
-
`IrcHandler` show an `Error: Missing env var CONNECTIONS_TABLE (table
|
|
1003
|
-
name for Connections)` (or one of the other four `<NAME>_TABLE` vars).
|
|
1004
|
-
|
|
1005
|
-
The `tablesConfigFromEnv` helper throws on the first invocation if any
|
|
1006
|
-
of `CONNECTIONS_TABLE`, `CHANNELMETA_TABLE`, `CHANNELMEMBERS_TABLE`,
|
|
1007
|
-
`NICKS_TABLE`, `ACCOUNTS_TABLE` is missing. The CDK stack always
|
|
1008
|
-
injects these — if one is missing in the console, somebody hand-edited
|
|
1009
|
-
the Lambda config. Fix by re-deploying:
|
|
1010
|
-
|
|
1011
|
-
```bash
|
|
1012
|
-
pnpm deploy:aws:staging
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
If the error mentions `MANAGEMENT_URL` being absent: that env var is
|
|
1016
|
-
optional at the loader level (`buildDepsFromEnv` tolerates its absence
|
|
1017
|
-
by setting `managementApi = null`), but the handler's fanout path will
|
|
1018
|
-
fail at the first `postToConnection`. Verify both `IrcHandler` and
|
|
1019
|
-
`IrcPingChecker` carry `MANAGEMENT_URL` in the console.
|
|
1020
|
-
|
|
1021
|
-
### 13.3 Smoke e2e times out waiting for `001`
|
|
1022
|
-
|
|
1023
|
-
The connection upgrade succeeded (the WebSocket `open` fired) but
|
|
1024
|
-
registration didn't complete. Common causes:
|
|
1025
|
-
|
|
1026
|
-
- **The handler threw on the first frame.** Tail the logs:
|
|
1027
|
-
```bash
|
|
1028
|
-
aws logs tail /aws/lambda/IrcHandler --follow
|
|
1029
|
-
```
|
|
1030
|
-
and reproduce. Look for the cold-start `buildDepsFromEnv` throw
|
|
1031
|
-
(§13.2) or an unexpected exception in `handleDefault`.
|
|
1032
|
-
- **The `prod` stage didn't deploy.** The stage is `autoDeploy: true`,
|
|
1033
|
-
so this is rare, but verify the URL ends in `/prod` and that the
|
|
1034
|
-
route is wired:
|
|
1035
|
-
```bash
|
|
1036
|
-
aws apigatewayv2 get-routes --api-id <api-id>
|
|
1037
|
-
```
|
|
1038
|
-
You should see three routes: `$connect`, `$disconnect`, `$default`.
|
|
1039
|
-
- **Client connected over plain `ws://`.** APIGW WebSocket stages only
|
|
1040
|
-
accept `wss://`. Use the URL printed by `cdk deploy` (the
|
|
1041
|
-
`ConnectUrl` output).
|
|
1042
|
-
|
|
1043
|
-
### 13.4 `aws cloudformation describe-stacks` returns nothing in CI
|
|
1044
|
-
|
|
1045
|
-
The workflow uses:
|
|
1046
|
-
|
|
1047
|
-
```bash
|
|
1048
|
-
aws cloudformation describe-stacks \
|
|
1049
|
-
--stack-name IrcAwsStack \
|
|
1050
|
-
--query 'Stacks[0].Outputs[?OutputKey==`ConnectUrl`].OutputValue' \
|
|
1051
|
-
--output text
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
to resolve the smoke URL. If this returns nothing, either:
|
|
1055
|
-
|
|
1056
|
-
- **The stack name is wrong.** It's hardcoded as `IrcAwsStack` in
|
|
1057
|
-
`apps/aws-stack/bin/aws.ts`. If you forked the stack to a different
|
|
1058
|
-
name, the workflow's `--stack-name IrcAwsStack` won't find it.
|
|
1059
|
-
Update the workflow or rename back.
|
|
1060
|
-
- **The deploy failed silently.** Check the prior `pnpm
|
|
1061
|
-
deploy:aws:staging` step's exit code. `cdk deploy` with
|
|
1062
|
-
`--require-approval never` can still fail on IAM or
|
|
1063
|
-
CloudFormation errors.
|
|
1064
|
-
- **Wrong region.** The `describe-stacks` call uses `AWS_REGION` from
|
|
1065
|
-
the secrets; it must match the region the stack was deployed into.
|
|
1066
|
-
|
|
1067
|
-
The workflow prints a `::warning::` and skips the smoke step rather
|
|
1068
|
-
than failing the build — a silent skip is the failure mode to watch
|
|
1069
|
-
for.
|
|
1070
|
-
|
|
1071
|
-
### 13.5 WebSocket client can't connect
|
|
1072
|
-
|
|
1073
|
-
The `$connect` route is wired to `IrcHandler` and currently uses **no
|
|
1074
|
-
auth** (no authorizer). If the client gets a 4xx/5xx on the HTTP
|
|
1075
|
-
upgrade:
|
|
1076
|
-
|
|
1077
|
-
- Verify the URL is the `ConnectUrl` output, not `ManagementUrl` (the
|
|
1078
|
-
management endpoint rejects WebSocket upgrades).
|
|
1079
|
-
- Verify the `IrcHandler` Lambda has `execute-api:ManageConnections`
|
|
1080
|
-
on this stage (the stack grants this via
|
|
1081
|
-
`stage.grantManagementApiAccess(handler)`).
|
|
1082
|
-
- If you add an authorizer or IP allowlist later, the `$connect`
|
|
1083
|
-
Lambda is the natural place to gate by source IP (read
|
|
1084
|
-
`event.requestContext.connectionId` and
|
|
1085
|
-
`event.requestContext.sourceIp`).
|
|
1086
|
-
|
|
1087
|
-
### 13.6 DynamoDB table vanished after `cdk destroy`
|
|
1088
|
-
|
|
1089
|
-
By design. The stack uses `RemovalPolicy.DESTROY` on every table
|
|
1090
|
-
(§9.4) so staging teardowns are clean. **Do not run
|
|
1091
|
-
`cdk destroy` against a production stack.** For production, fork the
|
|
1092
|
-
stack to `RemovalPolicy.RETAIN` before the first deploy; flagged in
|
|
1093
|
-
§14.
|
|
1094
|
-
|
|
1095
|
-
### 13.7 High DynamoDB cost on `Connections`
|
|
1096
|
-
|
|
1097
|
-
The gone-connection sweeper `Scan`s the entire `Connections` table
|
|
1098
|
-
every 5 minutes (§9.6). At very large scale this dominates the read
|
|
1099
|
-
bill. Short-term mitigation: increase the sweeper's `rate(...)` to
|
|
1100
|
-
`rate(15 minutes)` or `rate(30 minutes)` in `aws-stack.ts`. Long-term:
|
|
1101
|
-
add a sparse GSI keyed by `idleSince` and switch the sweeper to a
|
|
1102
|
-
`Query` against it (post-v1 work).
|
|
1103
|
-
|
|
1104
|
-
### 13.8 Nick reservation always fails
|
|
1105
|
-
|
|
1106
|
-
A client's `NICK` always returns `433 ERR_NICKNAMEINUSE`, even for
|
|
1107
|
-
nicks that should be free. Two deployments sharing the same DynamoDB
|
|
1108
|
-
table name space is the usual cause — e.g. you deployed `IrcAwsStack`
|
|
1109
|
-
twice into the same account/region with a forked stack name. The
|
|
1110
|
-
`Nicks` table name is hardcoded as `Nicks` (physical name), so two
|
|
1111
|
-
stacks in the same region collide on the same table. Use separate
|
|
1112
|
-
regions or fork the table-name prefix.
|
|
1113
|
-
|
|
1114
|
-
---
|
|
1115
|
-
|
|
1116
|
-
## 14. Quick reference
|
|
1117
|
-
|
|
1118
|
-
```bash
|
|
1119
|
-
# One-time account setup (per region)
|
|
1120
|
-
aws configure # or: export AWS_PROFILE=...
|
|
1121
|
-
cd apps/aws-stack && pnpm cdk bootstrap
|
|
1122
|
-
|
|
1123
|
-
# Local dev
|
|
1124
|
-
pnpm install
|
|
1125
|
-
pnpm build
|
|
1126
|
-
pnpm --filter @serverless-ircd/aws-stack test # synth-time assertions
|
|
1127
|
-
pnpm --filter @serverless-ircd/aws-stack cdk:synth # writes cdk.out/
|
|
1128
|
-
|
|
1129
|
-
# Optional localstack validation (Docker required)
|
|
1130
|
-
docker run --rm -d -p 4566:4566 localstack/localstack
|
|
1131
|
-
npm install -g aws-cdk-local
|
|
1132
|
-
export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1
|
|
1133
|
-
LOCALSTACK=1 pnpm --filter @serverless-ircd/aws-stack test
|
|
1134
|
-
|
|
1135
|
-
# Staging
|
|
1136
|
-
pnpm deploy:aws:staging
|
|
1137
|
-
node apps/aws-stack/scripts/smoke.mjs \
|
|
1138
|
-
--url wss://<id>.execute-api.<region>.amazonaws.com/prod
|
|
1139
|
-
aws logs tail /aws/lambda/IrcHandler --follow # live logs
|
|
1140
|
-
|
|
1141
|
-
# Production (manual, separate credentials)
|
|
1142
|
-
pnpm deploy:aws:prod
|
|
1143
|
-
|
|
1144
|
-
# Tests / lint
|
|
1145
|
-
pnpm test # full workspace
|
|
1146
|
-
pnpm --filter @serverless-ircd/aws-adapter test
|
|
1147
|
-
pnpm --filter @serverless-ircd/aws-stack test
|
|
1148
|
-
pnpm lint
|
|
1149
|
-
```
|
|
1150
|
-
|
|
1151
|
-
Key files:
|
|
1152
|
-
|
|
1153
|
-
| Path | What |
|
|
1154
|
-
|---------------------------------------------------|-----------------------------------------------------|
|
|
1155
|
-
| `apps/aws-stack/src/aws-stack.ts` | CDK stack — all constructs and env-var wiring. |
|
|
1156
|
-
| `apps/aws-stack/bin/aws.ts` | App entry. Hardcodes stack name `IrcAwsStack`. |
|
|
1157
|
-
| `apps/aws-stack/cdk.json` | `app = tsx bin/aws.ts`. |
|
|
1158
|
-
| `apps/aws-stack/scripts/smoke.mjs` | CI + local smoke e2e. |
|
|
1159
|
-
| `apps/aws-stack/tests/stack.test.ts` | Synth-time assertions (cross-check for every fact). |
|
|
1160
|
-
| `packages/aws-adapter/src/cdk-table-defs.ts` | Five DynamoDB table shapes (keys, billing, TTL). |
|
|
1161
|
-
| `packages/aws-adapter/src/tables.ts` | Runtime table-name + key-column constants. |
|
|
1162
|
-
| `packages/aws-adapter/src/config-loader.ts` | Lambda env → `ServerConfig` schema mapping. |
|
|
1163
|
-
| `packages/aws-adapter/src/handlers/index.ts` | Lambda entry points (`handler`, `sweeperHandler`, `pingCheckerHandler`). |
|
|
1164
|
-
| `.github/workflows/deploy-aws.yml` | Staging deploy + smoke e2e CI. |
|
|
1165
|
-
|
|
1166
|
-
### Known gaps flagged for follow-up
|
|
1167
|
-
|
|
1168
|
-
These are facts the **code does not yet have an answer for**; the
|
|
1169
|
-
recommendations above are deployer guidance, not built-in knobs:
|
|
1170
|
-
|
|
1171
|
-
1. **`RemovalPolicy.DESTROY` is hardcoded.** There is no stack prop to
|
|
1172
|
-
flip it to `RETAIN` for production. Recommendation: a future change
|
|
1173
|
-
adds a `production: boolean` (or similar) construct prop that
|
|
1174
|
-
toggles the removal policy across all five tables.
|
|
1175
|
-
2. ~~**`SERVER_NAME` / `NETWORK_NAME` / `MOTD` are hardcoded in
|
|
1176
|
-
`aws-stack.ts`.**~~ **Resolved** — these are now CDK construct props
|
|
1177
|
-
(`serverName`, `networkName`, `motdLines`) on `IrcStackProps` (see
|
|
1178
|
-
§7.1).
|
|
1179
|
-
3. ~~**MOTD multiline parsing.**~~ **Resolved** — the stack's
|
|
1180
|
-
`motdLines` prop is a `string[]` joined with `\n`, which the config
|
|
1181
|
-
loader splits back into one line per entry. The delimiter is `\n`
|
|
1182
|
-
only; commas are treated as literal line content (see §7.1).
|
|
1183
|
-
4. **No per-environment stack parameter.** Staging and production
|
|
1184
|
-
cannot coexist in the same account/region without forking the
|
|
1185
|
-
stack to parameterize names. Recommendation: separate accounts for
|
|
1186
|
-
staging vs production for v1.
|