@intentius/behold 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +377 -76
- package/README.md +238 -190
- package/demos.json +17 -8
- package/dist/cli.js +13118 -11622
- package/example-argo-estate/README.md +8 -8
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/chant.workspace.json +27 -0
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +139 -62
- package/example-carve/README.md +14 -14
- package/example-carve/app/package-lock.json +75 -12
- package/example-carve/app/package.json +2 -2
- package/example-carve/legacy-tf/versions.tf +1 -1
- package/example-choudoufu-estate/README.md +4 -4
- package/example-choudoufu-estate/chant.workspace.json +32 -0
- package/example-choudoufu-estate/scripts/choudoufu-up.sh +21 -0
- package/example-flux-estate/README.md +5 -5
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/chant.workspace.json +27 -0
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/package-lock.json +139 -62
- package/example-k8s/README.md +20 -20
- package/example-k8s/chant.config.ts +1 -1
- package/example-k8s/ops/k3d-apply.op.ts +1 -1
- package/example-k8s/package-lock.json +135 -217
- package/example-k8s/package.json +2 -3
- package/example-terraform-estate/README.md +109 -0
- package/example-terraform-estate/backends/backend.s3.tf +10 -0
- package/example-terraform-estate/chant.workspace.json +22 -0
- package/example-terraform-estate/envs/dev/README.md +5 -0
- package/example-terraform-estate/envs/prod/data.tf +18 -0
- package/example-terraform-estate/envs/prod/network.tf +80 -0
- package/example-terraform-estate/envs/prod/outputs.tf +13 -0
- package/example-terraform-estate/envs/prod/provider.tf +10 -0
- package/example-terraform-estate/envs/prod/services.tf +21 -0
- package/example-terraform-estate/envs/prod/storage.tf +31 -0
- package/example-terraform-estate/envs/prod/variables.tf +17 -0
- package/example-terraform-estate/envs/prod/versions.tf +10 -0
- package/example-terraform-estate/modules/service/main.tf +58 -0
- package/example-terraform-estate/modules/service/outputs.tf +7 -0
- package/example-terraform-estate/modules/service/variables.tf +16 -0
- package/example-terraform-estate/modules/service/versions.tf +10 -0
- package/example-terraform-estate/platform/iam.tf +40 -0
- package/example-terraform-estate/platform/kms.tf +16 -0
- package/example-terraform-estate/platform/outputs.tf +7 -0
- package/example-terraform-estate/platform/variables.tf +10 -0
- package/example-terraform-estate/platform/versions.tf +10 -0
- package/example-writes/README.md +5 -5
- package/example-writes/chant.config.ts +1 -1
- package/example-writes/ops/apply.op.ts +1 -1
- package/example-writes/ops/floci.op.ts +2 -2
- package/example-writes/ops/reconcile.op.ts +1 -1
- package/example-writes/package-lock.json +65 -178
- package/example-writes/package.json +2 -3
- package/package.json +7 -6
- package/web/app.js +439 -34
- package/web/behaviour-scale.js +32 -0
- package/web/behaviour-scale.test.js +39 -0
- package/web/index.html +23 -1
- package/web/pending.js +67 -0
- package/web/pending.test.js +62 -0
- package/web/read-cost.js +49 -0
- package/web/read-cost.test.js +76 -0
- package/web/refresh-queue.js +26 -0
- package/web/refresh-queue.test.js +54 -0
- package/example-choudoufu-estate/.behold.json +0 -8
|
@@ -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 `@cdktn/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 @cdktn/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,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-terraform-estate",
|
|
3
|
+
"schema": 1,
|
|
4
|
+
"pins": [
|
|
5
|
+
{
|
|
6
|
+
"package": "@intentius/chant-lexicon-terraform",
|
|
7
|
+
"version": "0.95.0"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"members": [
|
|
11
|
+
{
|
|
12
|
+
"name": "prod",
|
|
13
|
+
"dir": "envs/prod",
|
|
14
|
+
"kind": "terraform"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "platform",
|
|
18
|
+
"dir": "platform",
|
|
19
|
+
"kind": "terraform"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -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,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,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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
variable "name" {
|
|
2
|
+
type = string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
variable "subnet_ids" {
|
|
6
|
+
type = list(string)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
variable "boundary_arn" {
|
|
10
|
+
description = "The platform root's permissions boundary."
|
|
11
|
+
type = string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
variable "log_kms_key_arn" {
|
|
15
|
+
type = string
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# The permissions boundary every service role in the estate attaches. Declared
|
|
2
|
+
# once here; each environment root reads it back by name as a data source.
|
|
3
|
+
resource "aws_iam_policy" "boundary" {
|
|
4
|
+
name = "${var.name}-boundary"
|
|
5
|
+
description = "Ceiling for every role the service module creates."
|
|
6
|
+
|
|
7
|
+
policy = jsonencode({
|
|
8
|
+
Version = "2012-10-17"
|
|
9
|
+
Statement = [
|
|
10
|
+
{
|
|
11
|
+
Effect = "Allow"
|
|
12
|
+
Action = ["s3:GetObject", "s3:PutObject", "logs:PutLogEvents", "sqs:*"]
|
|
13
|
+
Resource = "*"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
Effect = "Deny"
|
|
17
|
+
Action = ["iam:*", "organizations:*"]
|
|
18
|
+
Resource = "*"
|
|
19
|
+
},
|
|
20
|
+
]
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
resource "aws_iam_role" "deploy" {
|
|
25
|
+
name = "${var.name}-deploy"
|
|
26
|
+
|
|
27
|
+
assume_role_policy = jsonencode({
|
|
28
|
+
Version = "2012-10-17"
|
|
29
|
+
Statement = [{
|
|
30
|
+
Effect = "Allow"
|
|
31
|
+
Principal = { Service = "codebuild.amazonaws.com" }
|
|
32
|
+
Action = "sts:AssumeRole"
|
|
33
|
+
}]
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resource "aws_iam_role_policy_attachment" "deploy_boundary" {
|
|
38
|
+
role = aws_iam_role.deploy.name
|
|
39
|
+
policy_arn = aws_iam_policy.boundary.arn
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
resource "aws_kms_key" "estate" {
|
|
2
|
+
description = "Estate-wide key: artifact encryption and log group encryption."
|
|
3
|
+
deletion_window_in_days = 14
|
|
4
|
+
enable_key_rotation = true
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
resource "aws_kms_alias" "estate" {
|
|
8
|
+
name = "alias/${var.name}-estate"
|
|
9
|
+
target_key_id = aws_kms_key.estate.key_id
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
resource "aws_cloudwatch_log_group" "audit" {
|
|
13
|
+
name = "/${var.name}/audit"
|
|
14
|
+
retention_in_days = var.log_retention_days
|
|
15
|
+
kms_key_id = aws_kms_key.estate.arn
|
|
16
|
+
}
|
package/example-writes/README.md
CHANGED
|
@@ -10,7 +10,7 @@ src/bucket.ts one S3 Bucket + a TLS-only policy (the whole "infra")
|
|
|
10
10
|
ops/apply.op.ts ApplyOp "prod-apply" — code → real AWS (aws cloudformation deploy)
|
|
11
11
|
ops/floci.op.ts Op "floci-apply" — code → local Floci (CloudFormation API), no account
|
|
12
12
|
ops/reconcile.op.ts ReconcileOp "prod-reconcile" — cloud → code PR (the Adopt button)
|
|
13
|
-
chant.config.ts lexicons [aws
|
|
13
|
+
chant.config.ts lexicons [aws], environments [prod]
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The **Sync** button appears because a project declares an `ApplyOp`. A project with
|
|
@@ -19,7 +19,7 @@ no `*.op.ts` (like `../example`) shows no Sync — that's expected, not a bug.
|
|
|
19
19
|
## Creds-free: deploy to a local emulator (`--local`)
|
|
20
20
|
|
|
21
21
|
No AWS account? Serve with `--local` and behold boots a local Floci emulator
|
|
22
|
-
(needs Docker), then the **floci-apply** Op deploys the bucket to it
|
|
22
|
+
(needs Docker), then the **floci-apply** Op deploys the bucket to it, the same
|
|
23
23
|
delegated-write path, zero cloud creds:
|
|
24
24
|
|
|
25
25
|
```sh
|
|
@@ -36,7 +36,7 @@ you Ctrl-C. Everything below uses real AWS instead.
|
|
|
36
36
|
## Prerequisites
|
|
37
37
|
|
|
38
38
|
- Node 20+, and **AWS credentials** in your environment (`aws sts get-caller-identity`
|
|
39
|
-
should work). The apply creates a real S3 bucket via CloudFormation
|
|
39
|
+
should work). The apply creates a real S3 bucket via CloudFormation, a few cents,
|
|
40
40
|
torn down at the end. behold holds no creds; `chant` uses yours on this machine.
|
|
41
41
|
- One edit: S3 bucket names are **globally unique**, so change `BucketName` in
|
|
42
42
|
[`src/bucket.ts`](src/bucket.ts) to something of your own (e.g. include your AWS
|
|
@@ -59,10 +59,10 @@ npm run dev -- serve example-writes --env prod
|
|
|
59
59
|
colour flip.)
|
|
60
60
|
2. In the header, click **Sync**. The now-line streams the Op's phases:
|
|
61
61
|
`▶ chant run prod-apply` → Build → Plan → **Apply** (`aws cloudformation deploy`).
|
|
62
|
-
`prod-apply` is **ungated**, so it applies straight away
|
|
62
|
+
`prod-apply` is **ungated**, so it applies straight away, no approval step.
|
|
63
63
|
3. When it finishes, the `store` node flips **blue → green (managed)**. Click it: the
|
|
64
64
|
inspect panel's **live** section shows the bucket's observed **physical id** and
|
|
65
|
-
status. That's the hydration
|
|
65
|
+
status. That's the hydration, the cloud's view of your declared resource.
|
|
66
66
|
4. Click **Refresh** any time to re-check drift and drop a lanes frame.
|
|
67
67
|
|
|
68
68
|
## What "gated" looks like
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApplyOp } from "@intentius/chant
|
|
1
|
+
import { ApplyOp } from "@intentius/chant/op";
|
|
2
2
|
|
|
3
3
|
// code → cloud. Additive apply (no gate) runs one-shot on the local executor —
|
|
4
4
|
// behold's Sync button shells `chant run prod-apply`. Target cloudformation;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Op, phase, build,
|
|
1
|
+
import { Op, phase, build, httpCheck } from "@intentius/chant/op";
|
|
2
|
+
import { awsApply } from "@intentius/chant-lexicon-aws";
|
|
2
3
|
|
|
3
4
|
// Deploy the S3 bucket to the local Floci emulator via the CloudFormation API
|
|
4
5
|
// (awsApply — direct create-or-update + poll, no aws CLI, honours the endpoint).
|
|
@@ -8,7 +9,6 @@ import { Op, phase, build, awsApply, httpCheck } from "@intentius/chant-lexicon-
|
|
|
8
9
|
export default Op({
|
|
9
10
|
name: "floci-apply",
|
|
10
11
|
overview: "S3 bucket → local Floci (CloudFormation API), no cloud account",
|
|
11
|
-
taskQueue: "behold-local",
|
|
12
12
|
phases: [
|
|
13
13
|
phase("Build", [build(".", { script: "build" })]),
|
|
14
14
|
// Stack name = the env ("prod"), so `serve --local --env prod`'s overlay —
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReconcileOp } from "@intentius/chant
|
|
1
|
+
import { ReconcileOp } from "@intentius/chant/op";
|
|
2
2
|
|
|
3
3
|
// cloud → code. Snapshot live prod, diff vs source, open a PR for drift/orphans.
|
|
4
4
|
// Scoped to chant-owned resources. behold's Adopt button triggers this.
|