@intentius/behold 0.17.0 → 0.19.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.
Files changed (60) hide show
  1. package/AGENTS.md +757 -26
  2. package/LICENSE +190 -0
  3. package/README.md +251 -187
  4. package/THIRD_PARTY.md +75 -0
  5. package/demos.json +80 -9
  6. package/dist/cli.js +5479 -1673
  7. package/example-argo-estate/package-lock.json +6 -6
  8. package/example-carve/app/package-lock.json +3 -3
  9. package/example-choudoufu-estate/.behold.json +8 -0
  10. package/example-choudoufu-estate/README.md +48 -0
  11. package/example-choudoufu-estate/monolith/behaviour.live.json +495 -0
  12. package/example-choudoufu-estate/monolith/carve.json +12 -0
  13. package/example-choudoufu-estate/monolith/main.tf +127 -0
  14. package/example-choudoufu-estate/scripts/choudoufu-down.sh +8 -0
  15. package/example-choudoufu-estate/scripts/choudoufu-up.sh +62 -0
  16. package/example-choudoufu-estate/team-a/behaviour.live.json +179 -0
  17. package/example-choudoufu-estate/team-a/main.tf +58 -0
  18. package/example-choudoufu-estate/team-b/behaviour.live.json +179 -0
  19. package/example-choudoufu-estate/team-b/main.tf +73 -0
  20. package/example-choudoufu-estate/team-c/behaviour.live.json +179 -0
  21. package/example-choudoufu-estate/team-c/main.tf +51 -0
  22. package/example-flux-estate/package-lock.json +6 -6
  23. package/example-k8s/package-lock.json +6 -6
  24. package/example-terraform-estate/README.md +109 -0
  25. package/example-terraform-estate/backends/backend.s3.tf +10 -0
  26. package/example-terraform-estate/envs/dev/README.md +5 -0
  27. package/example-terraform-estate/envs/prod/data.tf +18 -0
  28. package/example-terraform-estate/envs/prod/network.tf +80 -0
  29. package/example-terraform-estate/envs/prod/outputs.tf +13 -0
  30. package/example-terraform-estate/envs/prod/provider.tf +10 -0
  31. package/example-terraform-estate/envs/prod/services.tf +21 -0
  32. package/example-terraform-estate/envs/prod/storage.tf +31 -0
  33. package/example-terraform-estate/envs/prod/variables.tf +17 -0
  34. package/example-terraform-estate/envs/prod/versions.tf +10 -0
  35. package/example-terraform-estate/modules/service/main.tf +58 -0
  36. package/example-terraform-estate/modules/service/outputs.tf +7 -0
  37. package/example-terraform-estate/modules/service/variables.tf +16 -0
  38. package/example-terraform-estate/modules/service/versions.tf +10 -0
  39. package/example-terraform-estate/platform/iam.tf +40 -0
  40. package/example-terraform-estate/platform/kms.tf +16 -0
  41. package/example-terraform-estate/platform/outputs.tf +7 -0
  42. package/example-terraform-estate/platform/variables.tf +10 -0
  43. package/example-terraform-estate/platform/versions.tf +10 -0
  44. package/example-writes/package-lock.json +3 -3
  45. package/licenses/cncf-artwork-LICENSE.md +13 -0
  46. package/licenses/iTerm2-Color-Schemes-LICENSE +31 -0
  47. package/licenses/kubernetes-icons-LICENSE +201 -0
  48. package/package.json +25 -7
  49. package/web/app.js +1309 -41
  50. package/web/behaviour-scale.js +380 -0
  51. package/web/behaviour-scale.test.js +307 -0
  52. package/web/cross-env.js +58 -0
  53. package/web/cross-env.test.js +61 -0
  54. package/web/index.html +62 -2
  55. package/web/pending.js +67 -0
  56. package/web/pending.test.js +62 -0
  57. package/web/read-cost.js +49 -0
  58. package/web/read-cost.test.js +76 -0
  59. package/web/refresh-queue.js +26 -0
  60. package/web/refresh-queue.test.js +54 -0
@@ -0,0 +1,179 @@
1
+ {
2
+ "meta": {
3
+ "engine": "behold-fixture",
4
+ "version": "0",
5
+ "at": {
6
+ "traffic": "100 rps, p50"
7
+ }
8
+ },
9
+ "entities": {
10
+ "aws_iam_role.team_c": {
11
+ "at": {
12
+ "traffic": "100 rps, p50"
13
+ },
14
+ "cost": {
15
+ "perHour": 0.0004,
16
+ "currency": "USD"
17
+ },
18
+ "headroom": {
19
+ "cpu": 0.9,
20
+ "latency": 0.85
21
+ },
22
+ "errorRate": 0.001,
23
+ "resilience": {
24
+ "failure": "one zone lost",
25
+ "verdict": "survives"
26
+ },
27
+ "provenance": {
28
+ "engine": "behold-fixture",
29
+ "version": "0",
30
+ "tolerance": "fixture",
31
+ "basis": "modeled"
32
+ }
33
+ },
34
+ "aws_iam_role_policy.team_c_inline": {
35
+ "at": {
36
+ "traffic": "100 rps, p50"
37
+ },
38
+ "cost": {
39
+ "perHour": 0.0002,
40
+ "currency": "USD"
41
+ },
42
+ "headroom": {
43
+ "cpu": 0.84,
44
+ "latency": 0.76
45
+ },
46
+ "errorRate": 0.001,
47
+ "resilience": {
48
+ "failure": "one zone lost",
49
+ "verdict": "survives"
50
+ },
51
+ "provenance": {
52
+ "engine": "behold-fixture",
53
+ "version": "0",
54
+ "tolerance": "fixture",
55
+ "basis": "modeled"
56
+ }
57
+ },
58
+ "aws_iam_policy.team_c": {
59
+ "at": {
60
+ "traffic": "100 rps, p50"
61
+ },
62
+ "cost": {
63
+ "perHour": 0.0002,
64
+ "currency": "USD"
65
+ },
66
+ "headroom": {
67
+ "cpu": 0.78,
68
+ "latency": 0.67
69
+ },
70
+ "errorRate": 0.001,
71
+ "resilience": {
72
+ "failure": "one zone lost",
73
+ "verdict": "survives"
74
+ },
75
+ "provenance": {
76
+ "engine": "behold-fixture",
77
+ "version": "0",
78
+ "tolerance": "fixture",
79
+ "basis": "modeled"
80
+ }
81
+ },
82
+ "aws_iam_role_policy_attachment.team_c": {
83
+ "at": {
84
+ "traffic": "100 rps, p50"
85
+ },
86
+ "cost": {
87
+ "perHour": 0.0001,
88
+ "currency": "USD"
89
+ },
90
+ "headroom": {
91
+ "cpu": 0.72,
92
+ "latency": 0.58
93
+ },
94
+ "errorRate": 0.001,
95
+ "resilience": {
96
+ "failure": "one zone lost",
97
+ "verdict": "survives"
98
+ },
99
+ "provenance": {
100
+ "engine": "behold-fixture",
101
+ "version": "0",
102
+ "tolerance": "fixture",
103
+ "basis": "modeled"
104
+ }
105
+ },
106
+ "aws_cloudwatch_log_group.team_c_0": {
107
+ "at": {
108
+ "traffic": "100 rps, p50"
109
+ },
110
+ "cost": {
111
+ "perHour": 0.0416,
112
+ "currency": "USD"
113
+ },
114
+ "headroom": {
115
+ "cpu": 0.66,
116
+ "latency": 0.49
117
+ },
118
+ "errorRate": 0.0004,
119
+ "resilience": {
120
+ "failure": "one zone lost",
121
+ "verdict": "survives"
122
+ },
123
+ "provenance": {
124
+ "engine": "behold-fixture",
125
+ "version": "0",
126
+ "tolerance": "fixture",
127
+ "basis": "modeled"
128
+ }
129
+ },
130
+ "aws_cloudwatch_log_group.team_c_1": {
131
+ "at": {
132
+ "traffic": "100 rps, p50"
133
+ },
134
+ "cost": {
135
+ "perHour": 0.0312,
136
+ "currency": "USD"
137
+ },
138
+ "headroom": {
139
+ "cpu": 0.6,
140
+ "latency": 0.85
141
+ },
142
+ "errorRate": 0.0004,
143
+ "resilience": {
144
+ "failure": "one zone lost",
145
+ "verdict": "survives"
146
+ },
147
+ "provenance": {
148
+ "engine": "behold-fixture",
149
+ "version": "0",
150
+ "tolerance": "fixture",
151
+ "basis": "modeled"
152
+ }
153
+ },
154
+ "aws_cloudwatch_log_group.team_c_2": {
155
+ "at": {
156
+ "traffic": "100 rps, p50"
157
+ },
158
+ "cost": {
159
+ "perHour": 0.0208,
160
+ "currency": "USD"
161
+ },
162
+ "headroom": {
163
+ "cpu": 0.54,
164
+ "latency": 0.76
165
+ },
166
+ "errorRate": 0.0004,
167
+ "resilience": {
168
+ "failure": "one zone lost",
169
+ "verdict": "survives"
170
+ },
171
+ "provenance": {
172
+ "engine": "behold-fixture",
173
+ "version": "0",
174
+ "tolerance": "fixture",
175
+ "basis": "modeled"
176
+ }
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,51 @@
1
+ terraform {
2
+ required_providers {
3
+ aws = {
4
+ source = "hashicorp/aws"
5
+ version = "= 6.58.0"
6
+ }
7
+ }
8
+ live {
9
+ estate = "tlmig-sample-team-c"
10
+ }
11
+ }
12
+
13
+ provider "aws" {
14
+ region = "us-east-1"
15
+ }
16
+
17
+ resource "aws_iam_role" "team_c" {
18
+ name = "tlmig-sample-team-c-role"
19
+ assume_role_policy = jsonencode({ Version = "2012-10-17", Statement = [{ Effect = "Allow", Principal = { Service = "ec2.amazonaws.com" }, Action = "sts:AssumeRole" }] })
20
+ }
21
+
22
+ resource "aws_iam_role_policy" "team_c_inline" {
23
+ name = "tlmig-sample-team-c-inline"
24
+ role = aws_iam_role.team_c.id
25
+ policy = jsonencode({ Version = "2012-10-17", Statement = [{ Effect = "Allow", Action = ["logs:CreateLogStream"], Resource = "*" }] })
26
+ }
27
+
28
+ resource "aws_iam_policy" "team_c" {
29
+ name = "tlmig-sample-team-c-policy"
30
+ policy = jsonencode({ Version = "2012-10-17", Statement = [{ Effect = "Allow", Action = ["logs:PutLogEvents"], Resource = "*" }] })
31
+ }
32
+
33
+ resource "aws_iam_role_policy_attachment" "team_c" {
34
+ role = aws_iam_role.team_c.name
35
+ policy_arn = aws_iam_policy.team_c.arn
36
+ }
37
+
38
+ resource "aws_cloudwatch_log_group" "team_c_0" {
39
+ name = "/tlmig-sample/team-c/svc-0"
40
+ retention_in_days = 1
41
+ }
42
+
43
+ resource "aws_cloudwatch_log_group" "team_c_1" {
44
+ name = "/tlmig-sample/team-c/svc-1"
45
+ retention_in_days = 1
46
+ }
47
+
48
+ resource "aws_cloudwatch_log_group" "team_c_2" {
49
+ name = "/tlmig-sample/team-c/svc-2"
50
+ retention_in_days = 1
51
+ }
@@ -561,9 +561,9 @@
561
561
  }
562
562
  },
563
563
  "node_modules/@kubernetes/client-node/node_modules/js-yaml": {
564
- "version": "5.4.1",
565
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz",
566
- "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==",
564
+ "version": "5.4.2",
565
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz",
566
+ "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==",
567
567
  "funding": [
568
568
  {
569
569
  "type": "github",
@@ -1113,9 +1113,9 @@
1113
1113
  }
1114
1114
  },
1115
1115
  "node_modules/js-yaml": {
1116
- "version": "4.3.1",
1117
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
1118
- "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
1116
+ "version": "4.3.2",
1117
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
1118
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
1119
1119
  "funding": [
1120
1120
  {
1121
1121
  "type": "github",
@@ -555,9 +555,9 @@
555
555
  }
556
556
  },
557
557
  "node_modules/@kubernetes/client-node/node_modules/js-yaml": {
558
- "version": "5.4.1",
559
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz",
560
- "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==",
558
+ "version": "5.4.2",
559
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz",
560
+ "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==",
561
561
  "funding": [
562
562
  {
563
563
  "type": "github",
@@ -1187,9 +1187,9 @@
1187
1187
  }
1188
1188
  },
1189
1189
  "node_modules/js-yaml": {
1190
- "version": "4.3.1",
1191
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
1192
- "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
1190
+ "version": "4.3.2",
1191
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
1192
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
1193
1193
  "funding": [
1194
1194
  {
1195
1195
  "type": "github",
@@ -0,0 +1,109 @@
1
+ # example-terraform-estate
2
+
3
+ A multi-root Terraform estate, declared only. No cloud account, no Docker, no
4
+ `terraform` binary — behold reads the HCL through chant's terraform lexicon and
5
+ draws what it finds.
6
+
7
+ ```sh
8
+ npx @intentius/behold demo terraform-estate
9
+ ```
10
+
11
+ ## What it declares
12
+
13
+ Two roots that are applied separately, plus the shared module both are built
14
+ on. 25 resources, drawing 37 cards and 31 edges at the `resources` zoom.
15
+
16
+ | directory | what it is |
17
+ |---|---|
18
+ | `envs/prod` | The environment root: a VPC with three subnets, a route table and an edge security group, an artifacts bucket with its versioning/public-access/encryption siblings, and two calls to the shared service module. |
19
+ | `platform` | The estate-wide root, applied separately: the permissions boundary every service role attaches, a deploy role, the KMS key and alias, an audit log group. It declares **no provider block** — it inherits one where it is used. |
20
+ | `modules/service` | The shared module `envs/prod` calls twice. An IAM role and inline policy, a log group, a work queue with its dead-letter queue, a task security group. |
21
+ | `backends` | A backend fragment, copied into a root when one is initialised. |
22
+ | `envs/dev` | Not built yet. A README and no `.tf`. |
23
+
24
+ ## What each directory is there to show
25
+
26
+ Discovery has to tell five things apart, and this estate is built to be each of
27
+ them. A root is a directory with a `.tf` declaring a line-start `terraform {` or
28
+ `provider "` block beside a `resource`, `data` or `module` block — a regex probe
29
+ at depth, never an HCL parse.
30
+
31
+ **`envs/prod` is the obvious root.** Provider block, resources, module calls.
32
+
33
+ **`platform` is the root that has no provider block.** The naive reading of "a
34
+ root configures a provider" would miss it. It is a root because it declares
35
+ resources beside a `terraform` block, and it is applied on its own.
36
+
37
+ **`modules/service` is the case that defeats the probe.** Its `versions.tf` is
38
+ byte-for-byte `envs/prod/versions.tf` — the same `required_version`, the same
39
+ `required_providers`. Nothing in the file's content says which of the two is a
40
+ root. What separates them is that this one sits under a `modules/` segment,
41
+ Terraform's own standard module structure, and the roots that call it draw its
42
+ blocks already. Drawing it again would double every card in it.
43
+
44
+ **`backends` has a `terraform` block and nothing to draw.** No resource, no
45
+ data source, no module call. It is reported as skipped rather than drawn as an
46
+ empty box — a box with nothing in it is a question, and the note answers it.
47
+
48
+ **`envs/dev` is neither drawn nor reported.** It holds no `.tf` at all, so
49
+ there is nothing here that looks like Terraform and nothing to say about it.
50
+ Silence is the right answer; reporting it would be noise about every directory
51
+ in every repository.
52
+
53
+ The graph's status line says all of this:
54
+
55
+ ```
56
+ 2 roots — prod, platform; skipped backends (no resource, data or module block —
57
+ nothing to draw), modules/service (called as a module, never applied on its own)
58
+ ```
59
+
60
+ ## The edge that is not drawn
61
+
62
+ `platform` declares the KMS key and its alias. `envs/prod` reads it back:
63
+
64
+ ```hcl
65
+ data "aws_kms_key" "estate" {
66
+ key_id = "alias/${var.name}-estate"
67
+ }
68
+ ```
69
+
70
+ That is a real cross-root reference, and it is how a multi-root estate is
71
+ actually wired — two roots applied separately, neither a module of the other,
72
+ joined by a name one publishes and the other looks up.
73
+
74
+ **behold draws no edge for it**, and that is deliberate. Both ends carry the
75
+ same unresolved `${var.name}` interpolation, so matching them would be a
76
+ coincidence of variable naming rather than a relationship either root states.
77
+ It was measured on a real estate and refused ([#381]). The data source card says
78
+ what it reads instead, which is a claim the estate does make.
79
+
80
+ Module calls *do* draw: each `module` block expands into its own contents under
81
+ `module.checkout/…`, with the reference edges chant's lexicon resolves from the
82
+ HCL.
83
+
84
+ ## It needs the lexicon
85
+
86
+ behold parses no HCL and ships no HCL parser. The reading goes through
87
+ `@intentius/chant-lexicon-terraform`, which loads `@cdktf/hcl2json` underneath —
88
+ a ~1.8 MB wasm blob. Both are declared as optional peers and behold does not
89
+ install them, so that every user who serves an ordinary chant project does not
90
+ carry an HCL parser they will never run:
91
+
92
+ ```sh
93
+ npm install --no-save @intentius/chant-lexicon-terraform @cdktf/hcl2json
94
+ ```
95
+
96
+ Without them the demo refuses with that exact line and says where it looked.
97
+
98
+ ## Not the carve demo
99
+
100
+ `behold demo carve` also involves Terraform and answers a different question.
101
+ This estate asks **what is in my Terraform** — draw it, box it by root, tell the
102
+ roots from the modules. The carve demo asks **what would it cost to leave**:
103
+ `chant carve advise` ranks each resource by peelability and the walkthrough
104
+ carves one across the line.
105
+
106
+ Nothing here is being migrated. There is no chant project beside it, no
107
+ peelability score on any card, and no stepper.
108
+
109
+ [#381]: https://github.com/INTENTIUS/behold/issues/381
@@ -0,0 +1,10 @@
1
+ # A backend fragment, copied into a root when one is initialised. It declares
2
+ # a `terraform` block and nothing to draw, which is why discovery reports it as
3
+ # skipped rather than drawing an empty box for it.
4
+ terraform {
5
+ backend "s3" {
6
+ bucket = "waterline-tfstate"
7
+ key = "prod/terraform.tfstate"
8
+ region = "us-east-1"
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ # envs/dev
2
+
3
+ Not built yet. This directory holds no `.tf` at all, which is why discovery
4
+ neither draws it nor reports it as skipped: there is nothing here that looks
5
+ like Terraform, so there is nothing to say about it.
@@ -0,0 +1,18 @@
1
+ data "aws_caller_identity" "current" {}
2
+
3
+ data "aws_region" "current" {}
4
+
5
+ # The cross-root read, and what behold does with it.
6
+ #
7
+ # `platform` declares this key and its alias; this root reads it back by alias
8
+ # rather than by a reference Terraform could resolve, because the two roots are
9
+ # applied separately and neither is a module of the other. That is how a real
10
+ # multi-root estate is wired, and it is also why NO EDGE is drawn between them:
11
+ # both ends carry the same unresolved `${var.name}` interpolation, so a match
12
+ # would be a coincidence of variable naming rather than a stated relationship.
13
+ # behold measured that and refused it (INTENTIUS/behold#381). What you get
14
+ # instead is this card, saying exactly what it reads — which is a claim the
15
+ # estate actually makes.
16
+ data "aws_kms_key" "estate" {
17
+ key_id = "alias/${var.name}-estate"
18
+ }
@@ -0,0 +1,80 @@
1
+ resource "aws_vpc" "main" {
2
+ cidr_block = var.vpc_cidr
3
+ enable_dns_hostnames = true
4
+ enable_dns_support = true
5
+
6
+ tags = { Name = "${var.name}-vpc" }
7
+ }
8
+
9
+ resource "aws_internet_gateway" "main" {
10
+ vpc_id = aws_vpc.main.id
11
+
12
+ tags = { Name = "${var.name}-igw" }
13
+ }
14
+
15
+ resource "aws_subnet" "public_a" {
16
+ vpc_id = aws_vpc.main.id
17
+ cidr_block = cidrsubnet(var.vpc_cidr, 8, 0)
18
+ availability_zone = "${var.region}a"
19
+ map_public_ip_on_launch = true
20
+
21
+ tags = { Name = "${var.name}-public-a" }
22
+ }
23
+
24
+ resource "aws_subnet" "public_b" {
25
+ vpc_id = aws_vpc.main.id
26
+ cidr_block = cidrsubnet(var.vpc_cidr, 8, 1)
27
+ availability_zone = "${var.region}b"
28
+ map_public_ip_on_launch = true
29
+
30
+ tags = { Name = "${var.name}-public-b" }
31
+ }
32
+
33
+ resource "aws_subnet" "private_a" {
34
+ vpc_id = aws_vpc.main.id
35
+ cidr_block = cidrsubnet(var.vpc_cidr, 8, 10)
36
+ availability_zone = "${var.region}a"
37
+
38
+ tags = { Name = "${var.name}-private-a" }
39
+ }
40
+
41
+ resource "aws_route_table" "public" {
42
+ vpc_id = aws_vpc.main.id
43
+
44
+ route {
45
+ cidr_block = "0.0.0.0/0"
46
+ gateway_id = aws_internet_gateway.main.id
47
+ }
48
+
49
+ tags = { Name = "${var.name}-public" }
50
+ }
51
+
52
+ resource "aws_route_table_association" "public_a" {
53
+ subnet_id = aws_subnet.public_a.id
54
+ route_table_id = aws_route_table.public.id
55
+ }
56
+
57
+ resource "aws_route_table_association" "public_b" {
58
+ subnet_id = aws_subnet.public_b.id
59
+ route_table_id = aws_route_table.public.id
60
+ }
61
+
62
+ resource "aws_security_group" "edge" {
63
+ name = "${var.name}-edge"
64
+ description = "Ingress for the public tier."
65
+ vpc_id = aws_vpc.main.id
66
+
67
+ ingress {
68
+ from_port = 443
69
+ to_port = 443
70
+ protocol = "tcp"
71
+ cidr_blocks = ["0.0.0.0/0"]
72
+ }
73
+
74
+ egress {
75
+ from_port = 0
76
+ to_port = 0
77
+ protocol = "-1"
78
+ cidr_blocks = ["0.0.0.0/0"]
79
+ }
80
+ }
@@ -0,0 +1,13 @@
1
+ output "vpc_id" {
2
+ description = "The network everything in this environment sits in."
3
+ value = aws_vpc.main.id
4
+ }
5
+
6
+ output "artifacts_bucket" {
7
+ value = aws_s3_bucket.artifacts.bucket
8
+ }
9
+
10
+ output "service_role_arns" {
11
+ description = "One per service module, for the platform root to reference back."
12
+ value = [module.checkout.role_arn, module.ledger.role_arn]
13
+ }
@@ -0,0 +1,10 @@
1
+ provider "aws" {
2
+ region = var.region
3
+
4
+ default_tags {
5
+ tags = {
6
+ Estate = "waterline"
7
+ Environment = "prod"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,21 @@
1
+ module "checkout" {
2
+ source = "../../modules/service"
3
+
4
+ name = "${var.name}-checkout"
5
+ subnet_ids = [aws_subnet.private_a.id]
6
+ boundary_arn = data.aws_iam_policy.boundary.arn
7
+ log_kms_key_arn = data.aws_kms_key.estate.arn
8
+ }
9
+
10
+ module "ledger" {
11
+ source = "../../modules/service"
12
+
13
+ name = "${var.name}-ledger"
14
+ subnet_ids = [aws_subnet.private_a.id]
15
+ boundary_arn = data.aws_iam_policy.boundary.arn
16
+ log_kms_key_arn = data.aws_kms_key.estate.arn
17
+ }
18
+
19
+ data "aws_iam_policy" "boundary" {
20
+ name = "${var.name}-boundary"
21
+ }
@@ -0,0 +1,31 @@
1
+ resource "aws_s3_bucket" "artifacts" {
2
+ bucket = "${var.name}-artifacts"
3
+ }
4
+
5
+ resource "aws_s3_bucket_versioning" "artifacts" {
6
+ bucket = aws_s3_bucket.artifacts.id
7
+
8
+ versioning_configuration {
9
+ status = "Enabled"
10
+ }
11
+ }
12
+
13
+ resource "aws_s3_bucket_public_access_block" "artifacts" {
14
+ bucket = aws_s3_bucket.artifacts.id
15
+
16
+ block_public_acls = true
17
+ block_public_policy = true
18
+ ignore_public_acls = true
19
+ restrict_public_buckets = true
20
+ }
21
+
22
+ resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
23
+ bucket = aws_s3_bucket.artifacts.id
24
+
25
+ rule {
26
+ apply_server_side_encryption_by_default {
27
+ sse_algorithm = "aws:kms"
28
+ kms_master_key_id = data.aws_kms_key.estate.arn
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,17 @@
1
+ variable "region" {
2
+ description = "The region every resource in this root is created in."
3
+ type = string
4
+ default = "us-east-1"
5
+ }
6
+
7
+ variable "name" {
8
+ description = "Prefix for every resource this root names."
9
+ type = string
10
+ default = "waterline-prod"
11
+ }
12
+
13
+ variable "vpc_cidr" {
14
+ description = "The network this environment sits in."
15
+ type = string
16
+ default = "10.40.0.0/16"
17
+ }
@@ -0,0 +1,10 @@
1
+ terraform {
2
+ required_version = ">= 1.9.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = "~> 6.0"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,58 @@
1
+ resource "aws_iam_role" "this" {
2
+ name = "${var.name}-role"
3
+ permissions_boundary = var.boundary_arn
4
+
5
+ assume_role_policy = jsonencode({
6
+ Version = "2012-10-17"
7
+ Statement = [{
8
+ Effect = "Allow"
9
+ Principal = { Service = "ecs-tasks.amazonaws.com" }
10
+ Action = "sts:AssumeRole"
11
+ }]
12
+ })
13
+ }
14
+
15
+ resource "aws_iam_role_policy" "this" {
16
+ name = "${var.name}-inline"
17
+ role = aws_iam_role.this.id
18
+
19
+ policy = jsonencode({
20
+ Version = "2012-10-17"
21
+ Statement = [{
22
+ Effect = "Allow"
23
+ Action = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]
24
+ Resource = aws_sqs_queue.work.arn
25
+ }]
26
+ })
27
+ }
28
+
29
+ resource "aws_cloudwatch_log_group" "this" {
30
+ name = "/service/${var.name}"
31
+ retention_in_days = 14
32
+ kms_key_id = var.log_kms_key_arn
33
+ }
34
+
35
+ resource "aws_sqs_queue" "work" {
36
+ name = "${var.name}-work"
37
+ visibility_timeout_seconds = 60
38
+ redrive_policy = jsonencode({
39
+ deadLetterTargetArn = aws_sqs_queue.dead.arn
40
+ maxReceiveCount = 5
41
+ })
42
+ }
43
+
44
+ resource "aws_sqs_queue" "dead" {
45
+ name = "${var.name}-dead"
46
+ }
47
+
48
+ resource "aws_security_group" "task" {
49
+ name = "${var.name}-task"
50
+ description = "Egress only; the task reaches the queue and the log group."
51
+
52
+ egress {
53
+ from_port = 0
54
+ to_port = 0
55
+ protocol = "-1"
56
+ cidr_blocks = ["0.0.0.0/0"]
57
+ }
58
+ }