@skillcdn/console 0.1.8 → 0.1.10
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/README.md +5 -5
- package/dist/api.d.ts +5 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +255 -10
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +9 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +80 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +62 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +31 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -3
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +27 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +75 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +34 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +96 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/categories.d.ts +13 -0
- package/dist/components/categories.d.ts.map +1 -0
- package/dist/components/categories.js +52 -0
- package/dist/components/categories.js.map +1 -0
- package/dist/components/decision-list.d.ts +10 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +14 -1
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +12 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +35 -0
- package/dist/components/goals.d.ts.map +1 -0
- package/dist/components/goals.js +45 -0
- package/dist/components/goals.js.map +1 -0
- package/dist/components/task-form.d.ts +5 -1
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +13 -1
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +16 -0
- package/dist/components/task-list.d.ts.map +1 -0
- package/dist/components/task-list.js +14 -0
- package/dist/components/task-list.js.map +1 -0
- package/dist/components/task-view.d.ts +9 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +10 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +30 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +60 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +10 -0
- package/dist/components/work.d.ts.map +1 -0
- package/dist/components/work.js +29 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +511 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +124 -8
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -2
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +39 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +120 -0
- package/dist/dates.js.map +1 -0
- package/dist/i18n/en.d.ts +162 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +174 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +169 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +5 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +5 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +50 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +74 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +9 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +366 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +112 -3
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +15 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +26 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +11 -4
- package/docs/specs/rest.md +33 -8
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +4 -4
- package/skills/building-a-console/references/the-default-console.md +1 -1
- package/skills/deploying-a-console/SKILL.md +11 -10
- package/skills/deploying-a-console/assets/deployer-policy.json +13 -12
- package/skills/deploying-a-console/assets/secrets.example.json +4 -4
- package/skills/deploying-a-console/references/access.md +10 -9
- package/skills/deploying-a-console/references/resources.md +107 -50
- package/skills/deploying-a-console/references/sizing.md +1 -1
- package/skills/deploying-a-console/scripts/store-secrets.mjs +1 -15
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +30 -0
- package/src/cli/cli.ts +311 -10
- package/src/cli/format.ts +88 -0
- package/src/cli/help.ts +69 -7
- package/src/client.ts +70 -2
- package/src/components/board.tsx +205 -12
- package/src/components/calendar.tsx +255 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +62 -5
- package/src/components/event-feed.tsx +12 -0
- package/src/components/goals.tsx +264 -0
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +61 -0
- package/src/components/task-view.tsx +74 -3
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +83 -0
- package/src/console.tsx +337 -4
- package/src/data.ts +63 -8
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +184 -1
- package/src/i18n/ko.ts +175 -1
- package/src/index.ts +58 -1
- package/src/limits.ts +5 -0
- package/src/router.ts +119 -6
- package/src/routes.ts +10 -0
- package/src/schemas.ts +141 -1
- package/src/styles/console.css +511 -0
- package/src/vocabulary.ts +33 -0
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"Resource": "*"
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
|
-
"Sid": "
|
|
72
|
+
"Sid": "RolesOfTheConsole",
|
|
73
73
|
"Effect": "Allow",
|
|
74
74
|
"Action": [
|
|
75
75
|
"iam:CreateRole",
|
|
@@ -83,18 +83,9 @@
|
|
|
83
83
|
"iam:ListAttachedRolePolicies",
|
|
84
84
|
"iam:ListRolePolicies",
|
|
85
85
|
"iam:PassRole",
|
|
86
|
-
"iam:TagRole"
|
|
87
|
-
"iam:CreateUser",
|
|
88
|
-
"iam:DeleteUser",
|
|
89
|
-
"iam:GetUser",
|
|
90
|
-
"iam:PutUserPolicy",
|
|
91
|
-
"iam:DeleteUserPolicy",
|
|
92
|
-
"iam:CreateAccessKey",
|
|
93
|
-
"iam:DeleteAccessKey",
|
|
94
|
-
"iam:ListAccessKeys",
|
|
95
|
-
"iam:TagUser"
|
|
86
|
+
"iam:TagRole"
|
|
96
87
|
],
|
|
97
|
-
"Resource":
|
|
88
|
+
"Resource": "arn:aws:iam::*:role/console-*"
|
|
98
89
|
},
|
|
99
90
|
{
|
|
100
91
|
"Sid": "ServiceLinkedRolesAndTheGitHostProvider",
|
|
@@ -108,6 +99,16 @@
|
|
|
108
99
|
],
|
|
109
100
|
"Resource": "*"
|
|
110
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"Sid": "Gauges",
|
|
104
|
+
"Effect": "Allow",
|
|
105
|
+
"Action": [
|
|
106
|
+
"cloudwatch:GetMetricData",
|
|
107
|
+
"cloudwatch:GetMetricStatistics",
|
|
108
|
+
"cloudwatch:ListMetrics"
|
|
109
|
+
],
|
|
110
|
+
"Resource": "*"
|
|
111
|
+
},
|
|
111
112
|
{
|
|
112
113
|
"Sid": "BudgetAlert",
|
|
113
114
|
"Effect": "Allow",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "The shape of the secrets file the run keeps outside any repository and deletes, on the person's word, once every value is in the parameter store. A value is what the run generated or derived, what the person gave, or the word later; a provider the person did not choose has no line.",
|
|
2
|
+
"_": "The shape of the secrets file the run keeps outside any repository and deletes, on the person's word, once every value is in the parameter store. A value is what the run generated or derived, what the person gave, or the word later; a provider the person did not choose has no line. The bucket on AWS needs no line: the task's own role reads and writes it.",
|
|
3
3
|
"AUTH_SECRET": "generated: 48 random characters, never shown",
|
|
4
4
|
"DATABASE_URL": "derived: postgres://console:<generated password>@<database endpoint>:5432/console",
|
|
5
5
|
"DATABASE_CA": "derived: the regional certificate bundle of the database service, as PEM",
|
|
6
|
-
"S3_ACCESS_KEY_ID": "derived: the access key id of the files user",
|
|
7
|
-
"S3_SECRET_ACCESS_KEY": "derived: its secret",
|
|
8
6
|
"TUNNEL_TOKEN": "derived: the connector token of the tunnel",
|
|
9
7
|
"GITHUB_CLIENT_SECRET": "asked: the client secret of the OAuth app, or later",
|
|
10
|
-
"GOOGLE_CLIENT_SECRET": "asked: the client secret of the OAuth client, or later"
|
|
8
|
+
"GOOGLE_CLIENT_SECRET": "asked: the client secret of the OAuth client, or later",
|
|
9
|
+
"S3_ACCESS_KEY_ID": "only with the bucket on R2 (sizing.md, Options): asked, the key made in the Cloudflare dashboard",
|
|
10
|
+
"S3_SECRET_ACCESS_KEY": "only with the bucket on R2: its secret"
|
|
11
11
|
}
|
|
@@ -9,7 +9,7 @@ What the agent needs to act on the organization's accounts, the exact steps of t
|
|
|
9
9
|
**The deployer user and its policy.** Signed in to the account, in the IAM console (checked 2026-10):
|
|
10
10
|
|
|
11
11
|
1. Users, Create user. Name `console-deployer`. Leave console access off: this user is for the agent's commands only. Next.
|
|
12
|
-
2. Permissions: Attach policies directly. In a dedicated account, `AdministratorAccess`, which is the honest choice when nobody else is in the account. In a shared account, first create a policy from [deployer-policy.json](../assets/deployer-policy.json) (Policies, Create policy, JSON, paste, name it `console-deployer`), then attach that one. The scoped policy is broad where names cannot be known in advance and tight where they can (`console-` prefixed roles
|
|
12
|
+
2. Permissions: Attach policies directly. In a dedicated account, `AdministratorAccess`, which is the honest choice when nobody else is in the account. In a shared account, first create a policy from [deployer-policy.json](../assets/deployer-policy.json) (Policies, Create policy, JSON, paste, name it `console-deployer`), then attach that one. The scoped policy is broad where names cannot be known in advance and tight where they can (`console-` prefixed roles and buckets, `/console/` parameters); a step the policy does not cover fails with `AccessDenied` naming the action, and the fix is to add that action.
|
|
13
13
|
3. Create user. Open it, Security credentials, Access keys, Create access key. Use case: Command Line Interface (and tick the confirmation), or Other. Next, Create access key. **Download .csv file.** The secret is shown once; the file is the one copy.
|
|
14
14
|
|
|
15
15
|
**How the key reaches the agent.** Save the downloaded file as `.aws-key.csv` in the run's folder, and tell the agent. The agent reads the two values from the file with a script, writes them as the profile `console` into the AWS CLI's own credentials file (`~/.aws/credentials`, the person's file, readable by them alone where the system can say so), sets the region on the profile, and deletes `.aws-key.csv`; neither value is ever written in a command line, a message or a log. A key pasted into the chat instead is written to the profile the same way and never repeated, and the person is told to make a new key once the run is over, since the chat kept a copy.
|
|
@@ -27,23 +27,24 @@ The first answers with the account id the plan names; the second with the defaul
|
|
|
27
27
|
|
|
28
28
|
## The Cloudflare account and the token
|
|
29
29
|
|
|
30
|
-
**The domain.** The console's address is a hostname of a zone on Cloudflare: a domain the organization already has, moved to Cloudflare's nameservers (the zone is made first in the run, and the two nameservers it answers go to the person to set at their registrar), or one bought through Cloudflare's Registrar in the run with its price shown first. There is no free hostname for a tunnel; a domain is needed. The account is free (`dash.cloudflare.com/sign-up`).
|
|
30
|
+
**The domain.** The console's address is a hostname of a zone on Cloudflare: a domain the organization already has, moved to Cloudflare's nameservers (the zone is made first in the run, and the two nameservers it answers go to the person to set at their registrar), or one bought through Cloudflare's Registrar in the run with its price shown first. There is no free hostname for a tunnel; a domain is needed. The account is free (`dash.cloudflare.com/sign-up`). A domain already on Cloudflare and serving the organization's site is the usual case and needs nothing moved: the run adds the console's hostname to that zone and touches nothing else of it (resources.md "A zone that serves other hosts").
|
|
31
31
|
|
|
32
|
-
**The nameservers, at the registrar.** The person named the registrar with the domain; give its steps where they are known (the registrar's own help page for "nameservers" or "DNS settings", read at run time), and otherwise the general ones: sign in at the registrar, open the domain, find the nameserver (NS) setting, replace what is there with the two names Cloudflare gave, save; the change takes minutes to hours to reach the world, and the run checks the zone's status until it is `active`. Everything else of the domain's DNS (mail records, other hosts)
|
|
32
|
+
**The nameservers, at the registrar.** The person named the registrar with the domain; give its steps where they are known (the registrar's own help page for "nameservers" or "DNS settings", read at run time), and otherwise the general ones: sign in at the registrar, open the domain, find the nameserver (NS) setting, replace what is there with the two names Cloudflare gave, save; the change takes minutes to hours to reach the world, and the run checks the zone's status until it is `active`. Everything else of the domain's DNS (mail records, other hosts) must be in the Cloudflare zone before the switch, so that nothing stops working: the run has Cloudflare scan for the common records, shows the person what it found, and asks them to compare with the registrar's DNS page and send what is missing (resources.md "The zone, first").
|
|
33
33
|
|
|
34
34
|
**The token.** My Profile, API Tokens (`dash.cloudflare.com/profile/api-tokens`), Create Token, **Create Custom Token** (checked 2026-10). Name it `console deploy`. Permissions, one row each:
|
|
35
35
|
|
|
36
36
|
| Scope | Permission | Level | For |
|
|
37
37
|
|---|---|---|---|
|
|
38
|
-
| Account | Cloudflare
|
|
39
|
-
| Account | Account Settings | Read |
|
|
38
|
+
| Account | Cloudflare One Connector: cloudflared | Edit | The tunnel the task connects out through, and its routing. The row was called "Cloudflare Tunnel" before and is listed as "Argo Tunnel (Legacy)" beside the new name, which still works; the API reports it as "Cloudflare Tunnel Write" (checked 2026-10) |
|
|
39
|
+
| Account | Account Settings | Read | The account id, only when the zone is made or a domain bought in the run; a zone that exists names its account itself |
|
|
40
40
|
| Zone | DNS | Edit | The console's hostname |
|
|
41
41
|
| Zone | Zone | Edit | Making the zone for a domain the organization owns, finding it, its status and nameservers |
|
|
42
|
-
| Zone | Zone Settings | Edit | Always use HTTPS |
|
|
42
|
+
| Zone | Zone Settings | Edit | Always use HTTPS, on a zone of the console's own |
|
|
43
|
+
| Zone | Dynamic URL Redirects | Edit | Always HTTPS for the console's hostname alone, on a zone that serves other hosts too (resources.md "A zone that serves other hosts") |
|
|
43
44
|
| Zone | Zone WAF | Edit | The rate limiting rule |
|
|
44
45
|
| Account | Registrar | Edit | Only when a domain is to be bought in the run |
|
|
45
46
|
|
|
46
|
-
Zone Resources: Include, All zones, so that a zone made in the run is covered. Account Resources: the one account. Client IP filtering: none. TTL: none, or a year. Continue to summary, Create Token, copy the secret once.
|
|
47
|
+
Zone Resources: Include, All zones, so that a zone made in the run is covered. Account Resources: the one account. Client IP filtering: none. TTL: none, or a year. Continue to summary, Create Token, copy the secret once. Read where Edit is needed is the mistake to look for: a token with Read lists tunnels and rules and fails only at the first creation, with `403` and code `10000` (resources.md "When a step fails").
|
|
47
48
|
|
|
48
49
|
**How it reaches the agent.** Save it in a file named `.cf-token` in the run's folder (a file without an extension: in Notepad, "All files" as the type, so that it is not `.cf-token.txt`), and tell the agent; the agent reads it into the environment for each call and never prints it. The file is listed in the deployment repository's `.gitignore` from the first commit, and deleted at the end of the run on the person's word. A token pasted into the chat is written into the file with the agent's file tool and never repeated, and the person is told to make a new token once the run is over, since the chat kept a copy.
|
|
49
50
|
|
|
@@ -56,7 +57,7 @@ curl -s https://api.cloudflare.com/client/v4/zones -H "Authorization: Bearer $CL
|
|
|
56
57
|
curl -s https://api.cloudflare.com/client/v4/accounts -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
`"status": "active"`, the zones the token sees (the domain among them, or none yet), and the account with its id. The file carries a carriage return on Windows; the `tr` strips it.
|
|
60
|
+
`"status": "active"`, the zones the token sees (the domain among them, or none yet), and the account with its id; the last call is refused without Account Settings Read, which is fine when the zone exists and carries the id. The file carries a carriage return on Windows; the `tr` strips it. These calls prove the token is live, not what it may change: Read lists exactly what Edit lists.
|
|
60
61
|
|
|
61
62
|
## The git host
|
|
62
63
|
|
|
@@ -75,4 +76,4 @@ An organization-owned app is the same form under the organization's settings, De
|
|
|
75
76
|
|
|
76
77
|
## The secrets file
|
|
77
78
|
|
|
78
|
-
What the person holds (a client secret) is written to a file outside any repository, in the shape of [secrets.example.json](../assets/secrets.example.json): `console-secrets.json` in the folder above the run's folder, with the generated and derived values the run adds to it. The run reads it to put each value in the parameter store, and deletes it at the end on the person's word. A value the person says `later` to leaves its provider out until it is set; the record names it. A provider the person did not choose has no line in it.
|
|
79
|
+
What the person holds (a client secret) is written to a file outside any repository, in the shape of [secrets.example.json](../assets/secrets.example.json): `console-secrets.json` in the folder above the run's folder, with the generated and derived values the run adds to it. The file starts empty: the script's `set` makes it with the first value, and the example is its shape, never a template to copy, since `store` would write a copied placeholder as a parameter. The run reads it to put each value in the parameter store, and deletes it at the end on the person's word. A value the person says `later` to leaves its provider out until it is set; the record names it. A provider the person did not choose has no line in it.
|
|
@@ -4,17 +4,19 @@ What each phase of the skill does, command by command, with its checks and what
|
|
|
4
4
|
|
|
5
5
|
## Conventions
|
|
6
6
|
|
|
7
|
-
The run's folder is the deployment repository to be; `scratch/` in it holds the scripts, the request bodies and the notes, and never enters git. Every AWS command runs as the deployer, in the region of the plan; every Cloudflare call carries the token from its file. Set once per shell, in the run's folder:
|
|
7
|
+
The run's folder is the deployment repository to be; `scratch/` in it holds the scripts, the request bodies and the notes, and never enters git. Every AWS command runs as the deployer, in the region of the plan; every Cloudflare call carries the token from its file. Set once per shell, in the run's folder (the shell is bash: Git Bash on Windows, bash or zsh elsewhere):
|
|
8
8
|
|
|
9
|
-
```
|
|
9
|
+
```bash
|
|
10
10
|
export MSYS_NO_PATHCONV=1 # Git Bash on Windows: keep /console and the like as written
|
|
11
11
|
export AWS_PROFILE=console AWS_REGION=<region>
|
|
12
12
|
export CLOUDFLARE_API_TOKEN=$(tr -d '\r\n ' < .cf-token)
|
|
13
13
|
cf() { curl -sS "https://api.cloudflare.com/client/v4$1" -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" "${@:2}"; }
|
|
14
14
|
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
|
|
15
|
-
SECRETS=../console-secrets.json # outside the repository
|
|
15
|
+
SECRETS=../console-secrets.json # outside the repository; made by the first `set` below, never a copy of assets/secrets.example.json
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Every id the run makes or finds (`CF_ACCOUNT`, `ZONE`, `VPC`, `SUBNETS`, `TASKS_SG`, `DB_SG`, `ENDPOINT`, `BUCKET`, `TUNNEL`, the rules' ids) goes into `scratch/notes.md` as it is made, as a line `NAME=value`, and a new shell sets them again from there (`set -a; . scratch/notes.md; set +a` when the file holds nothing but such lines): phases 5 to 7 come after a ten-minute watch, or after a pause of hours, almost always in a new shell.
|
|
19
|
+
|
|
18
20
|
A value that must not be seen goes through a file or a pipe, never a command line: `aws ... --cli-input-json file://scratch/<file>` for a request that carries one, and the two scripts of this skill ([import-aws-key.mjs](../scripts/import-aws-key.mjs), [store-secrets.mjs](../scripts/store-secrets.mjs)), copied to `scratch/`, for the key and the parameters. A response that carries one is piped straight into the secrets file:
|
|
19
21
|
|
|
20
22
|
```sh
|
|
@@ -24,12 +26,12 @@ node -e "process.stdout.write(require('node:crypto').randomBytes(36).toString('b
|
|
|
24
26
|
|
|
25
27
|
## Phase 1: Access
|
|
26
28
|
|
|
27
|
-
1. The folder: `mkdir -p <folder>/scratch && cd <folder> && git init -b main`, and `.gitignore` with `.cf-token`, `.aws-key.csv`, `scratch/`, `*.pem`, `console-secrets.json`, `node_modules/`, `console-ui/dist/`, `.env`, `.env.*`, before anything else is written.
|
|
29
|
+
1. The folder: `mkdir -p <folder>/scratch && cd <folder> && git init -b main`, and `.gitignore` with `.cf-token`, `.aws-key.csv`, `scratch/`, `*.pem`, `console-secrets.json`, `node_modules/`, `console-ui/dist/`, `.env`, `.env.*`, `.skillcdn-console.json` (which `console use` writes in phase 6, and which belongs to a checkout people work in, not to the deployment), before anything else is written.
|
|
28
30
|
2. The command and the scripts: `npm install -g @skillcdn/console`, then copy `$(npm root -g)/@skillcdn/console/skills/deploying-a-console/scripts/*.mjs` into `scratch/` (or write them there from the connection's copy of the skill).
|
|
29
31
|
3. The key: the person saved `.aws-key.csv` in the folder ([access.md](access.md)). `node scratch/import-aws-key.mjs .aws-key.csv console` writes the profile and deletes the file. Then `aws configure set region <region> --profile console`.
|
|
30
32
|
4. `aws sts get-caller-identity`: the account id and `user/console-deployer`. A refusal means the key is wrong or not yet active (a minute after creation).
|
|
31
33
|
5. The network: `aws ec2 describe-vpcs --filters Name=is-default,Values=true --query 'Vpcs[0].VpcId' --output text`. `None`: `aws ec2 create-default-vpc`. Then the subnets: `aws ec2 describe-subnets --filters Name=vpc-id,Values=<vpc> Name=default-for-az,Values=true --query 'Subnets[].SubnetId' --output text`.
|
|
32
|
-
6. The token: `cf /user/tokens/verify` answers `"status": "active"`; `cf /
|
|
34
|
+
6. The token: `cf /user/tokens/verify` answers `"status": "active"`; `cf "/zones?name=<domain>" > scratch/zone.json` the zone, with its id, its `status` and its account (`ZONE=$(jq -r '.result[0].id' scratch/zone.json)`, `CF_ACCOUNT=$(jq -r '.result[0].account.id' scratch/zone.json)`), or an empty list when the domain is not on Cloudflare yet, in which case `CF_ACCOUNT=$(cf /accounts | jq -r '.result[0].id')` and `ZONE` waits for phase 3. A zone that exists: `cf "/zones/$ZONE/dns_records" | jq '.result | length'`; records of its own mean the zone serves the organization's site already, which the access record says and phase 3 "A zone that serves other hosts" acts on.
|
|
33
35
|
7. The git host: `gh auth status`; the owner the person named is theirs or an organization they are in (`gh api user`, `gh api user/orgs`).
|
|
34
36
|
8. The tools: `node --version` (24 or newer), `git --version`, `jq --version`, `curl --version`.
|
|
35
37
|
|
|
@@ -43,8 +45,8 @@ Nothing is created. The size from the answer and its numbers ([sizing.md](sizing
|
|
|
43
45
|
|---|---|
|
|
44
46
|
| Cluster; services; task families | `console`; `api`, `worker`; `console-api`, `console-worker`, `console-migrate` |
|
|
45
47
|
| Database instance, subnet group, user, database | `console`, `console`, `console`, `console` |
|
|
46
|
-
| Bucket
|
|
47
|
-
| Image repository, log group | `console`, `/console` |
|
|
48
|
+
| Bucket | `console-files-<account id>` (bucket names are global; the account id makes it unique), read and written by the task's role |
|
|
49
|
+
| Image repository, log group | `console`, `/console`; the connector's image in the same repository under `cloudflared-<version>` |
|
|
48
50
|
| Roles | `console-task-execution`, `console-task`, `console-github-deploy` |
|
|
49
51
|
| Security groups | `console-tasks`, `console-db` |
|
|
50
52
|
| Parameters | `/console/<NAME>`, one per secret of the secrets file |
|
|
@@ -57,7 +59,7 @@ The plan message is the estimate message of sizing.md followed by the line in th
|
|
|
57
59
|
|
|
58
60
|
### The zone, first
|
|
59
61
|
|
|
60
|
-
Not on Cloudflare yet: `cf /zones -X POST --data '{"name":"<domain>","account":{"id":"
|
|
62
|
+
Not on Cloudflare yet: `cf /zones -X POST --data '{"name":"<domain>","account":{"id":"'$CF_ACCOUNT'"},"type":"full"}'`, then `ZONE=$(cf "/zones?name=<domain>" | jq -r '.result[0].id')`. Before the nameservers change, the zone must carry what the domain serves already (its site, its mail), or that stops working at the switch: `cf "/zones/$ZONE/dns_records/scan" -X POST` has Cloudflare find the common records and add them (checked 2026-10), `cf "/zones/$ZONE/dns_records" | jq -r '.result[] | "\(.type) \(.name) \(.content)"'` lists what the zone now has, and the list goes to the person with the nameserver steps: "Compare this with the DNS page at your registrar and send me anything that is missing", each missing one added with `cf "/zones/$ZONE/dns_records" -X POST --data '{"type":"<MX | A | CNAME | TXT>","name":"<name>","content":"<value>","priority":<MX only>,"ttl":1}'`. The answer's `name_servers` go to the person with the steps for their registrar (access.md "The domain"), the run goes on, and the hostname answers once `cf "/zones?name=<domain>"` says `"status": "active"`. A domain to buy: `cf "/accounts/$CF_ACCOUNT/registrar/domain-search?q=<words>"` to find one, `cf "/accounts/$CF_ACCOUNT/registrar/domain-check" -X POST --data '{"domains":["<domain>"]}'` for the live price, the checkpoint with the price and the renewal, then `cf "/accounts/$CF_ACCOUNT/registrar/registrations" -X POST --data '{"domain_name":"<domain>"}'`; a `202` is polled at `.../registrations/<domain>/registration-status`. Buying through the API was in beta when checked (2026-10) and is not open to every account: `extension_not_supported_via_api`, a `404` or a route error means the person buys in the dashboard (Domain Registration, Register Domains), with the price in front of them there. A bought domain's zone is active at once, and `ZONE` is read as above.
|
|
61
63
|
|
|
62
64
|
### The network
|
|
63
65
|
|
|
@@ -68,7 +70,7 @@ DB_SG=$(aws ec2 create-security-group --group-name console-db --description "con
|
|
|
68
70
|
aws ec2 authorize-security-group-ingress --group-id $DB_SG --protocol tcp --port 5432 --source-group $TASKS_SG
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
A new group allows all egress and no ingress; the tasks' group stays so. Check: `aws ec2 describe-security-groups --group-ids $TASKS_SG --query 'SecurityGroups[0].IpPermissions'` is `[]`.
|
|
73
|
+
A new group allows all egress and no ingress; the tasks' group stays so. Check: `aws ec2 describe-security-groups --group-ids $TASKS_SG --query 'SecurityGroups[0].IpPermissions'` is `[]`. What the tasks need out, for an organization that restricts egress later: the connector reaches Cloudflare on `7844` (UDP first, then TCP) and `443`; the console reaches the database on `5432`, and the bucket, the sign-in providers and SkillCDN on `443`.
|
|
72
74
|
|
|
73
75
|
### The database
|
|
74
76
|
|
|
@@ -119,18 +121,15 @@ node scratch/store-secrets.mjs derive "$SECRETS" DATABASE_URL "postgres://consol
|
|
|
119
121
|
|
|
120
122
|
The regional bundle holds the three authorities a database in the region may be signed by (about 4.5 KB, read 2026-10), so it covers the instance whatever authority it got; it goes in as `DATABASE_CA`, and the URL carries no `ssl` parameter of its own (the console refuses both together). The password is base64url, so the URL needs no encoding.
|
|
121
123
|
|
|
122
|
-
### The bucket
|
|
124
|
+
### The bucket
|
|
123
125
|
|
|
124
126
|
```sh
|
|
125
127
|
BUCKET=console-files-$ACCOUNT
|
|
126
128
|
aws s3api create-bucket --bucket $BUCKET --region $AWS_REGION --create-bucket-configuration LocationConstraint=$AWS_REGION # us-east-1: without the configuration
|
|
127
129
|
aws s3api put-public-access-block --bucket $BUCKET --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
|
|
128
|
-
aws iam create-user --user-name console-files --tags Key=app,Value=console
|
|
129
|
-
aws iam put-user-policy --user-name console-files --policy-name console-files --policy-document file://scratch/files-policy.json
|
|
130
|
-
node scratch/store-secrets.mjs key "$SECRETS" console-files # makes the key once, stores both halves, prints neither
|
|
131
130
|
```
|
|
132
131
|
|
|
133
|
-
`scratch/files-policy.json`:
|
|
132
|
+
No user and no key: the tasks read and write the bucket as their own role, whose credentials the platform hands them and renews (`S3_CREDENTIALS=platform` in the task definition; `deploy/README.md` "The bucket"). The role gets the policy below when it is made. `scratch/files-policy.json`:
|
|
134
133
|
|
|
135
134
|
```json
|
|
136
135
|
{
|
|
@@ -146,6 +145,8 @@ node scratch/store-secrets.mjs key "$SECRETS" console-files # makes the key
|
|
|
146
145
|
|
|
147
146
|
### The image repository, the logs, the roles, the cluster
|
|
148
147
|
|
|
148
|
+
Two files first. `scratch/ecs-trust.json` lets ECS tasks assume a role: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}`. `scratch/parameters-policy.json` lets the execution role read the console's parameters: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["ssm:GetParameters"],"Resource":"arn:aws:ssm:<region>:<account>:parameter/console/*"}]}` (the parameters use the account's default key, which needs no `kms:Decrypt` of its own; checked 2026-10). Then:
|
|
149
|
+
|
|
149
150
|
```sh
|
|
150
151
|
aws ecr create-repository --repository-name console --image-scanning-configuration scanOnPush=true --image-tag-mutability IMMUTABLE
|
|
151
152
|
aws logs create-log-group --log-group-name /console && aws logs put-retention-policy --log-group-name /console --retention-in-days 30
|
|
@@ -153,23 +154,24 @@ aws iam create-role --role-name console-task-execution --assume-role-policy-docu
|
|
|
153
154
|
aws iam attach-role-policy --role-name console-task-execution --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
|
|
154
155
|
aws iam put-role-policy --role-name console-task-execution --policy-name console-parameters --policy-document file://scratch/parameters-policy.json
|
|
155
156
|
aws iam create-role --role-name console-task --assume-role-policy-document file://scratch/ecs-trust.json
|
|
157
|
+
aws iam put-role-policy --role-name console-task --policy-name console-files --policy-document file://scratch/files-policy.json
|
|
156
158
|
aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com # "has been taken" means it exists: fine
|
|
157
159
|
aws ecs create-cluster --cluster-name console --capacity-providers FARGATE --default-capacity-provider-strategy capacityProvider=FARGATE,weight=1
|
|
158
160
|
```
|
|
159
161
|
|
|
160
|
-
|
|
162
|
+
The task role carries the files policy and nothing else: the platform hands every container of the task the role's credentials at an address it puts in the environment, and the console reads them there.
|
|
161
163
|
|
|
162
164
|
### The tunnel, the hostname and the rules at Cloudflare
|
|
163
165
|
|
|
164
|
-
1. **The tunnel.** `cf "/accounts
|
|
166
|
+
1. **The tunnel.** `cf "/accounts/$CF_ACCOUNT/cfd_tunnel" -X POST --data '{"name":"console","config_src":"cloudflare"}' > scratch/tunnel.json`; `TUNNEL=$(jq -r .result.id scratch/tunnel.json)`; `jq -r .result.token scratch/tunnel.json | set_secret TUNNEL_TOKEN`; `rm scratch/tunnel.json`. Then its routing: `cf "/accounts/$CF_ACCOUNT/cfd_tunnel/$TUNNEL/configurations" -X PUT --data '{"config":{"ingress":[{"hostname":"<hostname>","service":"http://127.0.0.1:11199"},{"service":"http_status:404"}]}}'`. The connector runs beside the `api` container, so the service is the loopback, by its address rather than `localhost`, which may resolve to `::1` and reach the console from there; the last rule, without a hostname, is required.
|
|
165
167
|
2. **The hostname.** `cf "/zones/$ZONE/dns_records" -X POST --data '{"type":"CNAME","name":"<hostname>","content":"'$TUNNEL'.cfargotunnel.com","proxied":true,"ttl":1}'`. Proxied: the traffic goes through Cloudflare, which terminates TLS with its own certificate for the zone.
|
|
166
|
-
3. **
|
|
167
|
-
4. **The rate limiting rule**, on the sign-in and connect paths, which is what the console asks of a proxy; the board's own traffic is not limited. `RULESET=$(cf "/zones/$ZONE/rulesets/phases/http_ratelimit/entrypoint" | jq -r '.result.id')`: the entry point ruleset's id, or `null` when the zone has none yet. With one, `cf "/zones/$ZONE/rulesets/$RULESET/rules" -X POST --data @scratch/rule.json
|
|
168
|
+
3. **Always HTTPS**, so that `http://` turns into `https://`, which `PUBLIC_URL` is. On a zone of the console's own: `cf "/zones/$ZONE/settings/always_use_https" -X PATCH --data '{"value":"on"}'`. On a zone that serves other hosts: the redirect rule of "A zone that serves other hosts" below, for the hostname alone.
|
|
169
|
+
4. **The rate limiting rule**, on the sign-in and connect paths of the console's hostname, which is what the console asks of a proxy; the board's own traffic is not limited. `RULESET=$(cf "/zones/$ZONE/rulesets/phases/http_ratelimit/entrypoint" | jq -r '.result.id')`: the entry point ruleset's id, or `null` when the zone has none yet. With one, `cf "/zones/$ZONE/rulesets/$RULESET/rules" -X POST --data @scratch/rule.json`, which adds the rule and leaves the zone's others alone; with none, `cf "/zones/$ZONE/rulesets" -X POST --data '{"name":"console","kind":"zone","phase":"http_ratelimit","rules":[<the rule>]}'`. Either answer carries the ruleset with its rules: the ruleset's id and the rule's (`.result.id`, `.result.rules[-1].id`) go into the notes and the record. Never `PUT` on the entry point: it replaces every rule of the phase. `scratch/rule.json`:
|
|
168
170
|
|
|
169
171
|
```json
|
|
170
172
|
{
|
|
171
173
|
"description": "console: sign-in and connect, per client",
|
|
172
|
-
"expression": "(starts_with(http.request.uri.path, \"/auth/\") or starts_with(http.request.uri.path, \"/api/v1/connect\"))",
|
|
174
|
+
"expression": "(http.host eq \"<hostname>\" and (starts_with(http.request.uri.path, \"/auth/\") or starts_with(http.request.uri.path, \"/api/v1/connect\")))",
|
|
173
175
|
"action": "block",
|
|
174
176
|
"enabled": true,
|
|
175
177
|
"ratelimit": { "characteristics": ["ip.src", "cf.colo.id"], "period": 10, "requests_per_period": 30, "mitigation_timeout": 10 }
|
|
@@ -178,9 +180,25 @@ aws ecs create-cluster --cluster-name console --capacity-providers FARGATE --def
|
|
|
178
180
|
|
|
179
181
|
The Free plan allows one rate limiting rule, with a ten-second window and a ten-second block (checked 2026-10); a paid plan allows more, with longer windows.
|
|
180
182
|
|
|
183
|
+
### A zone that serves other hosts
|
|
184
|
+
|
|
185
|
+
The organization's site on the same domain is the usual case: the zone has records of its own (phase 1), and the run changes nothing zone-wide. The hostname's record and the rate limiting rule above name the hostname, so they touch nothing of the site. Always HTTPS is a setting of the whole zone, so for the hostname alone it is a redirect rule instead, in the phase `http_request_dynamic_redirect`, added the same way as the rate limiting rule (`RULESET=$(cf "/zones/$ZONE/rulesets/phases/http_request_dynamic_redirect/entrypoint" | jq -r '.result.id')`, then a `POST` of the rule to it, or a new ruleset of that phase with it), with the token's "Dynamic URL Redirects: Edit" (access.md). `scratch/redirect.json`:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"description": "console: https only",
|
|
190
|
+
"expression": "(http.host eq \"<hostname>\" and not ssl)",
|
|
191
|
+
"action": "redirect",
|
|
192
|
+
"action_parameters": { "from_value": { "status_code": 301, "target_url": { "expression": "concat(\"https://\", http.host, http.request.uri.path)" }, "preserve_query_string": true } },
|
|
193
|
+
"enabled": true
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
When the zone's `always_use_https` is `on` already (`cf "/zones/$ZONE/settings/always_use_https"`), the site has it and the console gets it with the rest: no rule is needed, and the record says so. A rule made here goes into the notes and the record with its ruleset's id and its own, as the rate limiting rule does. Other per-host settings of the site (its own rules, its integrity checks, its obfuscation) are the site's: the run neither reads nor changes them.
|
|
198
|
+
|
|
181
199
|
### The parameters
|
|
182
200
|
|
|
183
|
-
|
|
201
|
+
The generated one first, if the Conventions' example was not run as a step: `node -e "process.stdout.write(require('node:crypto').randomBytes(36).toString('base64url'))" | set_secret AUTH_SECRET`. With it, the database's URL and authority and the tunnel's token in the secrets file: `node scratch/store-secrets.mjs store "$SECRETS" /console` writes each as a `SecureString` parameter `/console/<NAME>` (the certificate bundle in the advanced tier, which takes up to 8 KB; the rest standard), skipping `later` and the password that is already inside the URL. Check: `aws ssm describe-parameters --parameter-filters Key=Name,Option=BeginsWith,Values=/console/ --query 'Parameters[].Name'`.
|
|
184
202
|
|
|
185
203
|
### The secrets the person holds
|
|
186
204
|
|
|
@@ -194,7 +212,8 @@ In the run's folder, write:
|
|
|
194
212
|
|
|
195
213
|
```sh
|
|
196
214
|
CONSOLE_REPOSITORY=https://github.com/skillcdn/console.git
|
|
197
|
-
CONSOLE_REF=<the commit of
|
|
215
|
+
CONSOLE_REF=<the commit of the release tag pinned at the plan>
|
|
216
|
+
CLOUDFLARED_VERSION=<the connector's release, such as 2026.10.0, from github.com/cloudflare/cloudflared/releases>
|
|
198
217
|
AWS_ACCOUNT_ID=<account id>
|
|
199
218
|
AWS_REGION=<region>
|
|
200
219
|
ECR_REPOSITORY=console
|
|
@@ -210,7 +229,7 @@ PLATFORM=linux/amd64
|
|
|
210
229
|
|
|
211
230
|
### `deploy/api.task.json`
|
|
212
231
|
|
|
213
|
-
The task definition of the `api` role with the tunnel's connector beside it, in the shape `register-task-definition` takes; the workflow fills `image
|
|
232
|
+
The task definition of the `api` role with the tunnel's connector beside it, in the shape `register-task-definition` takes; the workflow fills both `image` fields. At `small` and `medium`, `WORKER_IN_PROCESS` is `true` and there is no `worker.task.json`; at `large`, it is left out and the worker is a service of its own.
|
|
214
233
|
|
|
215
234
|
```json
|
|
216
235
|
{
|
|
@@ -228,12 +247,13 @@ The task definition of the `api` role with the tunnel's connector beside it, in
|
|
|
228
247
|
"image": "<image>",
|
|
229
248
|
"essential": true,
|
|
230
249
|
"command": ["api"],
|
|
250
|
+
"readonlyRootFilesystem": true,
|
|
231
251
|
"portMappings": [{ "containerPort": 11199, "protocol": "tcp" }],
|
|
232
252
|
"environment": [
|
|
233
253
|
{ "name": "PUBLIC_URL", "value": "https://<hostname>" },
|
|
234
254
|
{ "name": "WORKSPACE_NAME", "value": "<the board's name>" },
|
|
235
255
|
{ "name": "WORKER_IN_PROCESS", "value": "true" },
|
|
236
|
-
{ "name": "TRUSTED_PROXIES", "value": "127.0.0.1" },
|
|
256
|
+
{ "name": "TRUSTED_PROXIES", "value": "127.0.0.1,::1" },
|
|
237
257
|
{ "name": "CLIENT_IP_HEADER", "value": "cf-connecting-ip" },
|
|
238
258
|
{ "name": "HTTP_KEEP_ALIVE_SECONDS", "value": "100" },
|
|
239
259
|
{ "name": "MEMBERS", "value": "<logins and addresses, comma-separated>" },
|
|
@@ -242,15 +262,14 @@ The task definition of the `api` role with the tunnel's connector beside it, in
|
|
|
242
262
|
{ "name": "SKILLS_ADDRESS", "value": "<the organization's skills address, or drop the line>" },
|
|
243
263
|
{ "name": "S3_ENDPOINT", "value": "https://s3.<region>.amazonaws.com" },
|
|
244
264
|
{ "name": "S3_BUCKET", "value": "console-files-<account>" },
|
|
245
|
-
{ "name": "S3_REGION", "value": "<region>" }
|
|
265
|
+
{ "name": "S3_REGION", "value": "<region>" },
|
|
266
|
+
{ "name": "S3_CREDENTIALS", "value": "platform" }
|
|
246
267
|
],
|
|
247
268
|
"secrets": [
|
|
248
269
|
{ "name": "DATABASE_URL", "valueFrom": "arn:aws:ssm:<region>:<account>:parameter/console/DATABASE_URL" },
|
|
249
270
|
{ "name": "DATABASE_CA", "valueFrom": "arn:aws:ssm:<region>:<account>:parameter/console/DATABASE_CA" },
|
|
250
271
|
{ "name": "AUTH_SECRET", "valueFrom": "arn:aws:ssm:<region>:<account>:parameter/console/AUTH_SECRET" },
|
|
251
|
-
{ "name": "GITHUB_CLIENT_SECRET", "valueFrom": "arn:aws:ssm:<region>:<account>:parameter/console/GITHUB_CLIENT_SECRET" }
|
|
252
|
-
{ "name": "S3_ACCESS_KEY_ID", "valueFrom": "arn:aws:ssm:<region>:<account>:parameter/console/S3_ACCESS_KEY_ID" },
|
|
253
|
-
{ "name": "S3_SECRET_ACCESS_KEY", "valueFrom": "arn:aws:ssm:<region>:<account>:parameter/console/S3_SECRET_ACCESS_KEY" }
|
|
272
|
+
{ "name": "GITHUB_CLIENT_SECRET", "valueFrom": "arn:aws:ssm:<region>:<account>:parameter/console/GITHUB_CLIENT_SECRET" }
|
|
254
273
|
],
|
|
255
274
|
"healthCheck": {
|
|
256
275
|
"command": ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:11199/readyz').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))\""],
|
|
@@ -264,7 +283,7 @@ The task definition of the `api` role with the tunnel's connector beside it, in
|
|
|
264
283
|
},
|
|
265
284
|
{
|
|
266
285
|
"name": "tunnel",
|
|
267
|
-
"image": "
|
|
286
|
+
"image": "<the workflow fills it: the connector's image, mirrored into the repository>",
|
|
268
287
|
"essential": true,
|
|
269
288
|
"command": ["tunnel", "--no-autoupdate", "run"],
|
|
270
289
|
"secrets": [{ "name": "TUNNEL_TOKEN", "valueFrom": "arn:aws:ssm:<region>:<account>:parameter/console/TUNNEL_TOKEN" }],
|
|
@@ -276,15 +295,17 @@ The task definition of the `api` role with the tunnel's connector beside it, in
|
|
|
276
295
|
}
|
|
277
296
|
```
|
|
278
297
|
|
|
279
|
-
With Google: `GOOGLE_CLIENT_ID` and `GOOGLE_WORKSPACE_DOMAIN` in `environment`, `GOOGLE_CLIENT_SECRET` in `secrets`; without GitHub, the two GitHub entries are dropped. The connector starts only once the console is ready (`dependsOn` on the health check, which is `/readyz`), and stops first when the task stops, so a rollout never routes to a task that is not serving; the console's `stopTimeout` of 30 seconds is above its grace period of 20. The console sees every request from the loopback with the client's address in `cf-connecting-ip`, which is why the trusted
|
|
298
|
+
With Google: `GOOGLE_CLIENT_ID` and `GOOGLE_WORKSPACE_DOMAIN` in `environment`, `GOOGLE_CLIENT_SECRET` in `secrets`; without GitHub, the two GitHub entries are dropped. The connector starts only once the console is ready (`dependsOn` on the health check, which is `/readyz`), and stops first when the task stops, so a rollout never routes to a task that is not serving; the console's `stopTimeout` of 30 seconds is above its grace period of 20. The console sees every request from the loopback with the client's address in `cf-connecting-ip`, which is why the trusted proxies are the loopback's two addresses; its keep-alive of 100 seconds outlasts the connector's idle timeout of 90 (checked 2026-10). The image writes nothing to its filesystem, so the root filesystem is read-only (`deploy/README.md` "The image"); the connector's is not, since it is not the console's image. The image is the Node.js base, which has no `curl`, so the health check uses Node itself. The bucket's credentials are the task's role's, which the platform hands the container at an address it sets in the environment (`AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`); nothing of that is written here.
|
|
280
299
|
|
|
281
300
|
### `deploy/worker.task.json` (at `large`)
|
|
282
301
|
|
|
283
|
-
The same with `"family": "console-worker"`, `"cpu": "256"`, `"memory": "512"`, one container `worker` with `"command": ["worker"]
|
|
302
|
+
The same with `"family": "console-worker"`, `"cpu": "256"`, `"memory": "512"`, one container `worker` with `"command": ["worker"]` and the read-only root filesystem, the `environment` reduced to `WORKSPACE_NAME`, the `secrets` to `DATABASE_URL` and `DATABASE_CA`, no port, no health check, `"stopTimeout": 30`, the log stream prefix `worker`, and no tunnel.
|
|
284
303
|
|
|
285
304
|
### `deploy/migrate.task.json`
|
|
286
305
|
|
|
287
|
-
The same with `"family": "console-migrate"`, `"cpu": "256"`, `"memory": "512"`, one container `migrate` with `"command": ["migrate"]
|
|
306
|
+
The same with `"family": "console-migrate"`, `"cpu": "256"`, `"memory": "512"`, one container `migrate` with `"command": ["migrate"]` and the read-only root filesystem, `secrets` `DATABASE_URL` and `DATABASE_CA`, no environment, no port, no health check, the log stream prefix `migrate`, and no tunnel: a connector must never start for a one-off task.
|
|
307
|
+
|
|
308
|
+
**Without a pipeline.** A deployment that is rolled by hand, or on a platform that runs one task definition and nothing before it, can run `migrate` inside the tasks instead of as a one-off: a container `migrate` in `api.task.json`, and at `large` in `worker.task.json` too, from the same image with `"command": ["migrate"]`, `"essential": false` and the `secrets` of the migrate task, and the role's own container with `"dependsOn": [{ "containerName": "migrate", "condition": "SUCCESS" }]` (the tunnel keeps its `dependsOn` on `api`). Every task of every role then migrates before it serves; tasks that start together wait on the advisory lock the migration takes, so none races another; a migration that fails leaves the role's container unstarted and the circuit breaker rolls the deployment back. The contract then holds per task rather than per rollout, which hard rule 8 allows in these words. The one-off task stays the default here because it stops the rollout before a single task is replaced.
|
|
288
309
|
|
|
289
310
|
### `.github/workflows/deploy.yml`
|
|
290
311
|
|
|
@@ -312,6 +333,15 @@ jobs:
|
|
|
312
333
|
aws-region: ${{ env.AWS_REGION }}
|
|
313
334
|
- uses: aws-actions/amazon-ecr-login@v2
|
|
314
335
|
id: ecr
|
|
336
|
+
- name: Mirror the connector's image, once per version
|
|
337
|
+
run: |
|
|
338
|
+
TUNNEL_IMAGE="${{ steps.ecr.outputs.registry }}/$ECR_REPOSITORY:cloudflared-$CLOUDFLARED_VERSION"
|
|
339
|
+
if ! aws ecr describe-images --repository-name "$ECR_REPOSITORY" --image-ids imageTag="cloudflared-$CLOUDFLARED_VERSION" > /dev/null 2>&1; then
|
|
340
|
+
docker pull --platform "$PLATFORM" "cloudflare/cloudflared:$CLOUDFLARED_VERSION"
|
|
341
|
+
docker tag "cloudflare/cloudflared:$CLOUDFLARED_VERSION" "$TUNNEL_IMAGE"
|
|
342
|
+
docker push "$TUNNEL_IMAGE"
|
|
343
|
+
fi
|
|
344
|
+
echo "TUNNEL_IMAGE=$TUNNEL_IMAGE" >> "$GITHUB_ENV"
|
|
315
345
|
- name: Get the console at the pinned commit
|
|
316
346
|
run: |
|
|
317
347
|
git clone --no-checkout "$CONSOLE_REPOSITORY" console
|
|
@@ -330,7 +360,7 @@ jobs:
|
|
|
330
360
|
run: |
|
|
331
361
|
for role in migrate api worker; do
|
|
332
362
|
[ -f "deploy/$role.task.json" ] || continue
|
|
333
|
-
jq --arg image "$IMAGE" '(.containerDefinitions[] | select(.name != "tunnel") | .image) = $image' "deploy/$role.task.json" > "$RUNNER_TEMP/$role.json"
|
|
363
|
+
jq --arg image "$IMAGE" --arg tunnel "$TUNNEL_IMAGE" '(.containerDefinitions[] | select(.name != "tunnel") | .image) = $image | (.containerDefinitions[] | select(.name == "tunnel") | .image) = $tunnel' "deploy/$role.task.json" > "$RUNNER_TEMP/$role.json"
|
|
334
364
|
aws ecs register-task-definition --cli-input-json "file://$RUNNER_TEMP/$role.json" --query 'taskDefinition.taskDefinitionArn' --output text
|
|
335
365
|
done
|
|
336
366
|
- name: Migrate, before anything new starts
|
|
@@ -353,10 +383,24 @@ jobs:
|
|
|
353
383
|
aws ecs create-service --cluster "$ECS_CLUSTER" --service-name "$role" --task-definition "console-$role" --desired-count "$COUNT" --launch-type FARGATE --network-configuration "$NETWORK" --deployment-configuration "minimumHealthyPercent=100,maximumPercent=200,deploymentCircuitBreaker={enable=true,rollback=true}" > /dev/null
|
|
354
384
|
fi
|
|
355
385
|
done
|
|
356
|
-
|
|
386
|
+
- name: Wait for the rollout
|
|
387
|
+
run: |
|
|
388
|
+
for role in api worker; do
|
|
389
|
+
[ -f "deploy/$role.task.json" ] || continue
|
|
390
|
+
WANTED=$(aws ecs describe-task-definition --task-definition "console-$role" --query 'taskDefinition.taskDefinitionArn' --output text)
|
|
391
|
+
for i in $(seq 1 60); do
|
|
392
|
+
DEPLOYED=$(aws ecs describe-services --cluster "$ECS_CLUSTER" --services "$role" --query 'services[0].deployments[0]' --output json | jq -r '"\(.taskDefinition) \(.rolloutState)"')
|
|
393
|
+
case "$DEPLOYED" in
|
|
394
|
+
"$WANTED COMPLETED") echo "$role: rolled out"; continue 2 ;;
|
|
395
|
+
"$WANTED IN_PROGRESS") sleep 15 ;;
|
|
396
|
+
*) echo "$role: the rollout failed ($DEPLOYED): a rollback is a deployment of the previous definition"; aws logs tail /console --since 15m --format short --log-stream-name-prefix "$role" || true; exit 1 ;;
|
|
397
|
+
esac
|
|
398
|
+
done
|
|
399
|
+
echo "$role: not rolled out after 15 minutes"; exit 1
|
|
400
|
+
done
|
|
357
401
|
```
|
|
358
402
|
|
|
359
|
-
What it does on every push that touches `deploy/` or `console-ui/`: builds the image from the console's pinned commit (and, when `console-ui/` exists, the organization's own console on top of it, as `deploy/README.md` "A custom console in the image" says), pushes it under a tag of its own (the console's commit, the repository's commit and the run's number, so that a run on the same commit still gets a new tag), registers the task definitions with
|
|
403
|
+
What it does on every push that touches `deploy/` or `console-ui/`: mirrors the connector's image into the repository the first time a version is seen, so that the tasks never pull from elsewhere; builds the image from the console's pinned commit (and, when `console-ui/` exists, the organization's own console on top of it, as `deploy/README.md` "A custom console in the image" says), pushes it under a tag of its own (the console's commit, the repository's commit and the run's number, so that a run on the same commit still gets a new tag), registers the task definitions with the two images, runs `migrate` as a one-off task and stops if it did not exit `0`, then rolls `api` (and `worker`) with a circuit breaker that rolls back a deployment whose tasks never become healthy, and waits for the rollout of the task definition it registered to be `COMPLETED` (`aws ecs wait services-stable` returns as soon as the running count matches, before the tasks are healthy, which on a first deployment is too early; and a rollback is a new deployment of the previous definition, which completes too, so the definition is compared, not the state alone). The first push creates the services. Pin the actions' majors to the current ones (`checkout@v4`, `configure-aws-credentials@v4`, `amazon-ecr-login@v2`, checked 2026-10; the actions' pages say when a newer major exists).
|
|
360
404
|
|
|
361
405
|
### The role the workflow assumes
|
|
362
406
|
|
|
@@ -399,13 +443,15 @@ aws iam put-role-policy --role-name console-github-deploy --policy-name console-
|
|
|
399
443
|
|
|
400
444
|
```sh
|
|
401
445
|
cf "/zones?name=<domain>" | jq -r '.result[0].status' # active; else the wait of the skill
|
|
402
|
-
curl -sS -o /
|
|
446
|
+
curl -sS -o scratch/readyz.txt -w '%{http_code}\n' https://<hostname>/readyz # 200 (a file, not /dev/null: see "When a step fails")
|
|
403
447
|
curl -sS https://<hostname>/api/v1/me | jq .signIn # the providers set, asked as nobody
|
|
404
|
-
cf "/accounts
|
|
405
|
-
aws ecs describe-services --cluster console --services api --query 'services[0].[runningCount,deployments[0].rolloutState]'
|
|
448
|
+
cf "/accounts/$CF_ACCOUNT/cfd_tunnel/$TUNNEL/connections" | jq '.result | length' # 1 or more
|
|
449
|
+
aws ecs describe-services --cluster console --services api --query 'services[0].[runningCount,deployments[0].rolloutState]' # [<the size's api count>, "COMPLETED"]
|
|
406
450
|
aws logs tail /console --since 15m --format short | grep -E 'migrations applied|api listening|files go to the bucket'
|
|
407
451
|
```
|
|
408
452
|
|
|
453
|
+
The three log lines are the console's stable start lines (`deploy/README.md` "Process contract"); `files go to the bucket` carries `"credentials":"platform"`.
|
|
454
|
+
|
|
409
455
|
Then the budget alert, at one and a half times the estimate, to the address of the plan, which emails when the month's bill passes it or is forecast to:
|
|
410
456
|
|
|
411
457
|
```sh
|
|
@@ -423,7 +469,7 @@ console login --url https://<hostname> --agent deploying-a-console # relay t
|
|
|
423
469
|
console use <key>
|
|
424
470
|
printf '# Deployment check\n\nHanded in by the deploying agent on <date> to prove the bucket.\n' > scratch/deployment-check.md
|
|
425
471
|
NUMBER=$(console task new "Deployment check" --body "What this run deployed, in a few lines: the size, the address, what is owed." --json | jq -r .number)
|
|
426
|
-
RUN_ID=$(console take "$NUMBER" --json | jq -r .id)
|
|
472
|
+
RUN_ID=$(console take "$NUMBER" --json | jq -r .run.id) # take answers the run and the task
|
|
427
473
|
console hand-in scratch/deployment-check.md --label "the check"
|
|
428
474
|
HASH=$(console run "$RUN_ID" --json | jq -r '.artifacts[0].file.sha256')
|
|
429
475
|
aws s3api head-object --bucket $BUCKET --key "$HASH" # the object, with its size
|
|
@@ -439,30 +485,31 @@ The hash `console run` shows is the object's key in the bucket (no prefix is set
|
|
|
439
485
|
```markdown
|
|
440
486
|
# The console's deployment
|
|
441
487
|
|
|
442
|
-
Made on <date> with the skill `deploying-a-console` of the SkillCDN Console (skillcdn.ai/gh/skillcdn/console), from
|
|
488
|
+
Made on <date> with the skill `deploying-a-console` of the SkillCDN Console (skillcdn.ai/gh/skillcdn/console), from release `@skillcdn/console@<version>` of `skillcdn/console` (commit `<CONSOLE_REF>`).
|
|
443
489
|
|
|
444
490
|
## What exists
|
|
445
491
|
|
|
446
492
|
| What | Where |
|
|
447
493
|
|---|---|
|
|
448
494
|
| The console | https://<hostname>, through Cloudflare (zone <domain>, tunnel `console`, id `<tunnel id>`) |
|
|
495
|
+
| Cloudflare rules | The rate limiting rule `console: sign-in and connect, per client` (ruleset `<id>`, rule `<id>`); always HTTPS as <the zone's setting | the redirect rule `console: https only` (ruleset `<id>`, rule `<id>`) | the zone's own setting, on already> |
|
|
449
496
|
| AWS account and region | <account id>, <region> |
|
|
450
497
|
| Containers | ECS cluster `console`; services `api` (<n> tasks of <cpu>/<memory>)<, `worker`>; task families `console-api`, `console-migrate`<, `console-worker`> |
|
|
451
498
|
| Database | RDS PostgreSQL <version>, `console`, <class>, <storage>, <one zone | two zones>, backups 7 days, deletion protection on |
|
|
452
|
-
| Files | S3 bucket `console-files-<account>`, read and written by the
|
|
499
|
+
| Files | S3 bucket `console-files-<account>`, read and written by the task's role `console-task`; no key exists |
|
|
453
500
|
| Secrets | SSM Parameter Store, `/console/*`: <the names> |
|
|
454
501
|
| Logs | CloudWatch Logs `/console`, kept 30 days |
|
|
455
|
-
| Images | ECR `console
|
|
456
|
-
| Roles | `console-task-execution`, `console-task
|
|
502
|
+
| Images | ECR `console`: one tag per deployment, and the connector's image under `cloudflared-<version>` |
|
|
503
|
+
| Roles | `console-task-execution`, `console-task` (the bucket), `console-github-deploy` (assumed by this repository's `main`) |
|
|
457
504
|
| Size and cost | `<size>`; about <estimate> a month at <date>'s prices; a budget alert at <amount> emails <address> |
|
|
458
505
|
|
|
459
506
|
## How it is run
|
|
460
507
|
|
|
461
508
|
- **A deployment** is a push to `main` that touches `deploy/` or `console-ui/`: the workflow builds the image, runs the migrations, and rolls the services. `gh run list` shows the runs.
|
|
462
|
-
- **A new version of the console:** set `CONSOLE_REF` in `deploy/console.env` to the commit, push.
|
|
509
|
+
- **A new version of the console:** set `CONSOLE_REF` in `deploy/console.env` to the commit of the newest release tag: the last line of `git ls-remote --tags https://github.com/skillcdn/console 'refs/tags/@skillcdn/console@*' | sort -t@ -k3 -V` is the highest version, with the commit in its first column; move `console-ui/` to the same version of the package when there is one; push. **A new connector:** `CLOUDFLARED_VERSION` there, push.
|
|
463
510
|
- **A person added or removed:** `MEMBERS` and `ADMINS` in `deploy/api.task.json`, push. Administrators also change roles on the People page.
|
|
464
|
-
- **The next size:** the numbers in `deploy/api.task.json` (and `deploy/worker.task.json` at `large`), `API_COUNT` and `WORKER_COUNT` in `deploy/console.env`, push; the database: `aws rds modify-db-instance --db-instance-identifier console --db-instance-class <class> --apply-immediately` (a few minutes of unavailability).
|
|
465
|
-
- **A secret rotated:** `aws ssm put-parameter --
|
|
511
|
+
- **The next size:** the numbers in `deploy/api.task.json` (and `deploy/worker.task.json` at `large`), `API_COUNT` and `WORKER_COUNT` in `deploy/console.env`, push; to `large`, also `deploy/worker.task.json` written as the skill's resources.md gives it and `WORKER_IN_PROCESS` dropped from the api task; the database: `aws rds modify-db-instance --db-instance-identifier console --db-instance-class <class> --apply-immediately` (a few minutes of unavailability).
|
|
512
|
+
- **A secret rotated:** the new value into a secrets file of the same shape with the skill's script (`set`), then `node <the script> store <file> /console`, which overwrites the parameter in its tier (`$(npm root -g)/@skillcdn/console/skills/deploying-a-console/scripts/store-secrets.mjs`; `DATABASE_CA` is in the advanced tier, which an `aws ssm put-parameter --overwrite` by hand must name with `--tier Advanced`); then `aws ecs update-service --cluster console --service api --force-new-deployment`.
|
|
466
513
|
- **The organization's own console:** its source in `console-ui/` (building-a-console makes it); a push builds it into the image.
|
|
467
514
|
- **Paused:** `aws ecs update-service --cluster console --service api --desired-count 0` (and `worker`), `aws rds stop-db-instance --db-instance-identifier console` (seven days at most); the reverse resumes.
|
|
468
515
|
- **Removed:** the skill's "Removing", resource by resource, confirmed.
|
|
@@ -476,11 +523,11 @@ Made on <date> with the skill `deploying-a-console` of the SkillCDN Console (ski
|
|
|
476
523
|
`/readyz` 200 at the address; <who> signed in as administrator; project `<key>` made; the agent connected with a code; the deployment check handed in and found in the bucket under its hash; workflow run <id> green.
|
|
477
524
|
```
|
|
478
525
|
|
|
479
|
-
A change to `DEPLOY.md` or `README.md` alone does not run the workflow (its `paths`), so the record is pushed without a deployment.
|
|
526
|
+
A change to `DEPLOY.md` or `README.md` alone does not run the workflow (its `paths`), so the record is pushed without a deployment: `git add DEPLOY.md README.md && git commit -m "The record" && git push`, never `git add -A` here, after `git status --short` shows nothing else of the folder as untracked (`scratch/`, the files and `.skillcdn-console.json` are ignored).
|
|
480
527
|
|
|
481
528
|
## Removing
|
|
482
529
|
|
|
483
|
-
Only on request, each step confirmed, in this order, after a last snapshot of the database: the services (`aws ecs update-service --desired-count 0`, then `delete-service`), the cluster; the hostname (`DELETE /zones/<zone>/dns_records/<id>`) and the tunnel (`DELETE /accounts/<account>/cfd_tunnel/<id>`, after `.../connections` is empty); the database (`modify-db-instance --no-deletion-protection`, then `delete-db-instance --final-db-snapshot-identifier console-final-<date>`); the bucket, emptied first (`aws s3 rm s3://<bucket> --recursive`, then `delete-bucket`); the parameters; the
|
|
530
|
+
Only on request, each step confirmed, in this order, after a last snapshot of the database: the services (`aws ecs update-service --desired-count 0`, then `delete-service`), the cluster; the hostname (`DELETE /zones/<zone>/dns_records/<id>`), the rules (`DELETE /zones/<zone>/rulesets/<ruleset>/rules/<rule>`, the two ids of the record; a zone-wide always-HTTPS the run turned on is left as it is, since the zone may have come to rely on it) and the tunnel (`DELETE /accounts/<account>/cfd_tunnel/<id>`, after `.../connections` is empty); the database (`modify-db-instance --no-deletion-protection`, then `delete-db-instance --final-db-snapshot-identifier console-final-<date>`); the bucket, emptied first (`aws s3 rm s3://<bucket> --recursive`, then `delete-bucket`); the parameters; the roles' policies and the roles; the log group; the image repository (`--force`); the security groups; the budget. The deployment repository stays, with the record of what was removed and the snapshot's name.
|
|
484
531
|
|
|
485
532
|
## When a step fails
|
|
486
533
|
|
|
@@ -488,6 +535,8 @@ Only on request, each step confirmed, in this order, after a last snapshot of th
|
|
|
488
535
|
|---|---|---|
|
|
489
536
|
| `AccessDenied` or `UnauthorizedOperation` naming an action | The deployer's policy lacks it | Add the action to the policy (access.md) and run the step again |
|
|
490
537
|
| A log group or a parameter gets a name like `C:/Program Files/Git/console` | Git Bash rewrote the `/console` argument | `export MSYS_NO_PATHCONV=1`, delete the misnamed resource, run the step again |
|
|
538
|
+
| With that set, `curl` says it cannot open `/dev/null`, or a program cannot find `/tmp/...` | Git Bash now passes those as written, and a native program on Windows knows no such paths | Give the program a file under `scratch/` instead (`-o scratch/out.txt`); the shell's own `> /dev/null` still works |
|
|
539
|
+
| `POST .../cfd_tunnel` answers `403` with code `10000` while listing tunnels works | The token has Read where Edit is needed: Read lists what Edit makes | Edit the token's permission (access.md's table, "Cloudflare One Connector: cloudflared"), run the step again; the same for a ruleset's `POST` under Zone WAF or Dynamic URL Redirects |
|
|
491
540
|
| `aws rds create-db-instance` refuses the engine version | The region has no PostgreSQL 18 yet | `describe-db-engine-versions --engine postgres` and take the newest major it lists |
|
|
492
541
|
| `create-bucket` says the name is taken | Bucket names are global | Add a short suffix and update the task definition and the files policy |
|
|
493
542
|
| `POST /zones` is refused with a permission error | The token lacks Zone Edit | Add the row (access.md) and run the step again |
|
|
@@ -497,20 +546,28 @@ Only on request, each step confirmed, in this order, after a last snapshot of th
|
|
|
497
546
|
| A task stops before it is healthy; the log says `api listening` and then nothing | The health check fails: wrong port or `/readyz` not `200` (database unreachable, schema behind) | `aws logs tail /console --since 10m`; `/readyz` says which of the three |
|
|
498
547
|
| The service never stabilizes and rolls back | The circuit breaker tripped on the above | Fix the cause; `--force-new-deployment` |
|
|
499
548
|
| The tunnel shows no connector while `api` is healthy | The token is wrong, or the task has no public IP for its egress | Check `TUNNEL_TOKEN` and `assignPublicIp=ENABLED` in the network configuration |
|
|
549
|
+
| The connector's log warns at start about ICMP or the ping group, and that it `failed to sufficiently increase receive buffer size` | Harmless on Fargate: no raw sockets for pinging through the tunnel, and a small UDP buffer; HTTP through the tunnel needs neither | Nothing; the connector is up when its log says `Registered tunnel connection` and the tunnel shows a connector |
|
|
550
|
+
| `files go to the bucket` is missing from the log, and the first file answers `503` | The task's role cannot reach the bucket: no `taskRoleArn`, no files policy on `console-task`, or `S3_CREDENTIALS` not `platform` | Check the three; the log's warning at start says what the bucket or the credentials answered |
|
|
500
551
|
| `https://<hostname>` answers 530 or 502 from Cloudflare | No connector yet, or the hostname's ingress rule points elsewhere | Wait for the service; check the tunnel's configuration and the DNS record |
|
|
501
552
|
| `https://<hostname>` does not resolve | The zone is not active yet | The nameservers at the registrar; `cf "/zones?name=<domain>"` says when |
|
|
502
553
|
| The sign-in button is missing | The provider's id or secret is not in the task definition | Both halves or neither; check `environment` and `secrets` |
|
|
503
554
|
| The sign-in is refused after the provider | The login is not in `MEMBERS` (or the domain not in `GOOGLE_WORKSPACE_DOMAIN`) | Add it, push |
|
|
504
|
-
| The
|
|
555
|
+
| The mirror step cannot pull `cloudflare/cloudflared` | Docker Hub's anonymous pull limit on the runner | Run the workflow again (`gh run rerun <id>`); the pull happens once per connector version, and the tasks pull from the repository afterwards |
|
|
505
556
|
| `gh run watch` shows the build failing on `npm ci` in `console-ui` | The UI's lockfile is out of step | Fix in `console-ui/` (building-a-console), push |
|
|
506
557
|
| A 524 at Cloudflare on a long read | A request held longer than the proxy's 100 seconds | The console holds a decision's read for 50 at most; look for a slow database |
|
|
507
558
|
|
|
559
|
+
## As infrastructure code
|
|
560
|
+
|
|
561
|
+
An organization that writes this shape as infrastructure code (Terraform, for one) instead of running the commands keeps the same resources, names and contract; the first such rendering found these worth knowing (2026-10). A tunnel resource with its configuration at Cloudflare (`config_src = "cloudflare"`) and no secret of its own keeps the tunnel's secret out of the state, and the connector token is then read by a script straight into the secret store rather than through a data source, which would land it in the state. The database's password and the parameters go in as write-only arguments where the provider has them, for the same reason. Where the secret store is Secrets Manager rather than the parameter store, two halves of a key pair (the bucket on R2) fit one JSON secret, which a task definition reads by key (`arn:...:secret:<name>:S3_ACCESS_KEY_ID::`). The tasks' role, the connector beside `api`, the read-only root filesystem and the rollout's state apply as written.
|
|
562
|
+
|
|
508
563
|
## Facts that move
|
|
509
564
|
|
|
510
565
|
- AWS App Runner stopped taking new customers on 2026-04-30 and is in maintenance; ECS Express Mode (2025-11) is AWS's successor and provisions a load balancer (checked 2026-10).
|
|
566
|
+
- ECS: `aws ecs wait services-stable` returns once the running count matches the desired one, before the tasks are healthy; a deployment's `rolloutState` is `COMPLETED` once they are, or `FAILED` when the circuit breaker rolled it back (checked 2026-10).
|
|
567
|
+
- The connector reaches Cloudflare on `7844` (QUIC over UDP, falling back to TCP) and `443`, and reads its token from `TUNNEL_TOKEN`; its image is `cloudflare/cloudflared` on Docker Hub, tagged by release (checked 2026-10).
|
|
511
568
|
- Fargate: `stopTimeout` from 2 to 120 seconds, 30 when unset; containers stop in the reverse order of their start dependencies (checked 2026-10).
|
|
512
569
|
- SSM Parameter Store: 4 KB per standard parameter, 8 KB per advanced one at about $0.05 a month; the RDS regional certificate bundle is about 4.5 KB with three authorities (checked 2026-10).
|
|
513
570
|
- RDS for PostgreSQL has had major version 18 since 2025-11 (checked 2026-10).
|
|
514
|
-
- Cloudflare: a proxied request's idle limit is 100 seconds on every plan but Enterprise; the Free plan allows one rate limiting rule with a ten-second window; a tunnel costs nothing
|
|
571
|
+
- Cloudflare: a proxied request's idle limit is 100 seconds on every plan but Enterprise; the Free plan allows one rate limiting rule with a ten-second window; a tunnel costs nothing; a remotely managed tunnel's routing is the `configurations` document, whose last rule has no hostname; creating a zone needs Zone Edit on the token; the tunnel's permission is listed as "Cloudflare One Connector: cloudflared" in the dashboard and reported as "Cloudflare Tunnel Write" by the API; a redirect rule lives in the phase `http_request_dynamic_redirect` under "Dynamic URL Redirects" (checked 2026-10).
|
|
515
572
|
- A new AWS account made after 2025-07-15 starts with $100 of credits and a choice between a free plan that ends after six months and a paid one (checked 2026-10).
|
|
516
|
-
- Git Bash on Windows rewrites an argument that starts with `/` into a Windows path for a native program unless `MSYS_NO_PATHCONV=1` is set (checked 2026-10).
|
|
573
|
+
- Git Bash on Windows rewrites an argument that starts with `/` into a Windows path for a native program unless `MSYS_NO_PATHCONV=1` is set; with it set, `/dev/null` and `/tmp/...` as arguments reach the program as written, which knows no such paths, while the shell's own redirections keep working (checked 2026-10).
|