create-interview-cockpit 0.29.0 → 0.30.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/package.json
CHANGED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import type { InfraLabWorkspace } from "./types";
|
|
2
|
+
|
|
3
|
+
// Mirrors the structure of a real `governance-iam` Terraform module
|
|
4
|
+
// (roles, policies, attachments, users, OIDC providers, multi-account
|
|
5
|
+
// `for_each` expansion) but trimmed down so it can run end-to-end
|
|
6
|
+
// against LocalStack inside the Infra Lab runner.
|
|
7
|
+
//
|
|
8
|
+
// What works on LocalStack (free):
|
|
9
|
+
// - aws_iam_policy / aws_iam_role / *_policy_attachment / aws_iam_user
|
|
10
|
+
// - aws_iam_openid_connect_provider (object exists, trust is not exercised)
|
|
11
|
+
//
|
|
12
|
+
// What does NOT work realistically on LocalStack (left as plan-only / read):
|
|
13
|
+
// - real cross-account assume-role via Organizations
|
|
14
|
+
// - real OIDC federation (Azure DevOps, EKS IRSA, etc.)
|
|
15
|
+
export const AWS_GOVERNANCE_IAM_FILES: Record<string, string> = {
|
|
16
|
+
"README.md": `# AWS Governance IAM Lab
|
|
17
|
+
|
|
18
|
+
Practice the *shape* of an enterprise \`governance-iam\` Terraform module
|
|
19
|
+
against LocalStack. You get the same patterns the real module uses:
|
|
20
|
+
|
|
21
|
+
1. **OIDC providers** (\`aws_iam_openid_connect_provider\`)
|
|
22
|
+
2. **Custom IAM policies** loaded from JSON files in \`policy/\`
|
|
23
|
+
3. **IAM roles** with trust documents from \`assume_role/\`
|
|
24
|
+
4. **Policy attachments** (custom + AWS-managed)
|
|
25
|
+
5. **IAM users** with attachments
|
|
26
|
+
6. **Multi-account expansion** via \`for_each\` keyed as \`account/name\`
|
|
27
|
+
|
|
28
|
+
## How to run it
|
|
29
|
+
|
|
30
|
+
In the Infra Lab console, one command at a time:
|
|
31
|
+
|
|
32
|
+
1. \`terraform init\`
|
|
33
|
+
2. \`terraform validate\`
|
|
34
|
+
3. \`terraform plan -out=tfplan\`
|
|
35
|
+
4. \`terraform apply -auto-approve\`
|
|
36
|
+
5. Inspect with \`terraform state list\` and \`terraform show\`
|
|
37
|
+
6. \`terraform destroy -auto-approve\` when done
|
|
38
|
+
|
|
39
|
+
## What's real vs simulated
|
|
40
|
+
|
|
41
|
+
LocalStack accepts the IAM CRUD calls, so all roles/policies/users/attachments
|
|
42
|
+
are actually created. The OIDC providers are stored as objects, but no real
|
|
43
|
+
external identity (Azure DevOps, EKS, GKE) can federate into them here.
|
|
44
|
+
|
|
45
|
+
For the real federation handshake, you'd point this at a free-tier AWS
|
|
46
|
+
account by replacing the \`endpoints\` block in \`provider.tf\` and giving
|
|
47
|
+
real credentials.
|
|
48
|
+
|
|
49
|
+
## Multi-account note
|
|
50
|
+
|
|
51
|
+
LocalStack free tier is single-account, so the two "accounts"
|
|
52
|
+
(\`access\`, \`workload\`) are simulated by provider aliases pointing at the
|
|
53
|
+
same LocalStack endpoint. The \`for_each\` and \`account/name\` key shape is
|
|
54
|
+
the same as in a real Organizations setup — flip the endpoints and you can
|
|
55
|
+
reuse it as-is.
|
|
56
|
+
|
|
57
|
+
## Suggested experiments
|
|
58
|
+
|
|
59
|
+
- Add a new JSON file in \`policy/\` and reference it in \`terraform.tfvars\`.
|
|
60
|
+
- Add a new role in \`terraform.tfvars\` and watch the keyspace expand.
|
|
61
|
+
- Change a trust policy in \`assume_role/\` and run \`terraform plan\` to see
|
|
62
|
+
the in-place update.
|
|
63
|
+
- Try \`terraform plan\` after removing an attachment to learn how detach
|
|
64
|
+
works without destroying the role.
|
|
65
|
+
`,
|
|
66
|
+
"provider.tf": `terraform {
|
|
67
|
+
required_version = ">= 1.5.0"
|
|
68
|
+
|
|
69
|
+
required_providers {
|
|
70
|
+
aws = {
|
|
71
|
+
source = "hashicorp/aws"
|
|
72
|
+
version = "~> 5.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Two provider aliases simulate the multi-account fan-out used by the real
|
|
78
|
+
# governance-iam module. Both point at the same LocalStack endpoint here;
|
|
79
|
+
# in production they would target different AWS accounts via assume-role.
|
|
80
|
+
provider "aws" {
|
|
81
|
+
alias = "access"
|
|
82
|
+
region = "us-east-1"
|
|
83
|
+
access_key = "test"
|
|
84
|
+
secret_key = "test"
|
|
85
|
+
skip_credentials_validation = true
|
|
86
|
+
skip_metadata_api_check = true
|
|
87
|
+
skip_requesting_account_id = true
|
|
88
|
+
|
|
89
|
+
endpoints {
|
|
90
|
+
iam = "http://localhost:4566"
|
|
91
|
+
sts = "http://localhost:4566"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
provider "aws" {
|
|
96
|
+
alias = "workload"
|
|
97
|
+
region = "us-east-1"
|
|
98
|
+
access_key = "test"
|
|
99
|
+
secret_key = "test"
|
|
100
|
+
skip_credentials_validation = true
|
|
101
|
+
skip_metadata_api_check = true
|
|
102
|
+
skip_requesting_account_id = true
|
|
103
|
+
|
|
104
|
+
endpoints {
|
|
105
|
+
iam = "http://localhost:4566"
|
|
106
|
+
sts = "http://localhost:4566"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`,
|
|
110
|
+
"variables.tf": `# Logical inputs that mirror the real module. Swap values in
|
|
111
|
+
# terraform.tfvars to re-shape the deployment without touching main.tf.
|
|
112
|
+
|
|
113
|
+
variable "accounts" {
|
|
114
|
+
description = "Logical AWS account name -> provider alias key."
|
|
115
|
+
type = map(string)
|
|
116
|
+
default = {
|
|
117
|
+
access = "access"
|
|
118
|
+
workload = "workload"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
variable "policies" {
|
|
123
|
+
description = "Custom policies to create. Keyed as <account>/<name>."
|
|
124
|
+
type = map(object({
|
|
125
|
+
path = string
|
|
126
|
+
description = string
|
|
127
|
+
}))
|
|
128
|
+
default = {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
variable "roles" {
|
|
132
|
+
description = "Roles to create. Keyed as <account>/<name>."
|
|
133
|
+
type = map(object({
|
|
134
|
+
name = string
|
|
135
|
+
assume_role_policy_file = string
|
|
136
|
+
assume_role_policy_variable = map(string)
|
|
137
|
+
custom_policies = list(string) # tf_policy_keys to attach
|
|
138
|
+
managed_policy_arns = list(string) # AWS-managed ARNs to attach
|
|
139
|
+
}))
|
|
140
|
+
default = {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
variable "users" {
|
|
144
|
+
description = "IAM users to create. Keyed as <account>/<name>."
|
|
145
|
+
type = map(object({
|
|
146
|
+
name = string
|
|
147
|
+
custom_policies = list(string)
|
|
148
|
+
managed_policy_arns = list(string)
|
|
149
|
+
}))
|
|
150
|
+
default = {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
variable "oidc_identity" {
|
|
154
|
+
description = "OIDC providers to register. Keyed as <account>/<label>."
|
|
155
|
+
type = map(object({
|
|
156
|
+
account = string
|
|
157
|
+
oidc_url = string
|
|
158
|
+
audiences = list(string)
|
|
159
|
+
thumbprint = list(string)
|
|
160
|
+
}))
|
|
161
|
+
default = {}
|
|
162
|
+
}
|
|
163
|
+
`,
|
|
164
|
+
"local.tf": `# Real governance-iam splits a single declaration into per-account
|
|
165
|
+
# resources. We do the same here; the keys are "<account>/<name>" so a
|
|
166
|
+
# role declared in two accounts becomes two distinct resources.
|
|
167
|
+
locals {
|
|
168
|
+
account_policies = var.policies
|
|
169
|
+
account_roles = var.roles
|
|
170
|
+
account_users = var.users
|
|
171
|
+
|
|
172
|
+
# Flatten role -> custom policy attachments.
|
|
173
|
+
custom_role_attachments = merge([
|
|
174
|
+
for role_key, role in var.roles : {
|
|
175
|
+
for pol_key in role.custom_policies :
|
|
176
|
+
"\${role_key}::\${pol_key}" => {
|
|
177
|
+
tf_role_key = role_key
|
|
178
|
+
tf_policy_key = pol_key
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]...)
|
|
182
|
+
|
|
183
|
+
# Flatten role -> AWS-managed policy attachments.
|
|
184
|
+
managed_role_attachments = merge([
|
|
185
|
+
for role_key, role in var.roles : {
|
|
186
|
+
for arn in role.managed_policy_arns :
|
|
187
|
+
"\${role_key}::\${arn}" => {
|
|
188
|
+
tf_role_key = role_key
|
|
189
|
+
policy_arn = arn
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
]...)
|
|
193
|
+
|
|
194
|
+
custom_user_attachments = merge([
|
|
195
|
+
for user_key, user in var.users : {
|
|
196
|
+
for pol_key in user.custom_policies :
|
|
197
|
+
"\${user_key}::\${pol_key}" => {
|
|
198
|
+
tf_user_key = user_key
|
|
199
|
+
tf_policy_key = pol_key
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
]...)
|
|
203
|
+
|
|
204
|
+
managed_user_attachments = merge([
|
|
205
|
+
for user_key, user in var.users : {
|
|
206
|
+
for arn in user.managed_policy_arns :
|
|
207
|
+
"\${user_key}::\${arn}" => {
|
|
208
|
+
tf_user_key = user_key
|
|
209
|
+
policy_arn = arn
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
]...)
|
|
213
|
+
}
|
|
214
|
+
`,
|
|
215
|
+
"main.tf": `# @ref: aws-oidc-provider
|
|
216
|
+
# Trust links so external systems (ADO, EKS, GKE) can federate without
|
|
217
|
+
# storing AWS keys. On LocalStack the object is created but no real JWT
|
|
218
|
+
# is verified against it.
|
|
219
|
+
resource "aws_iam_openid_connect_provider" "oidc" {
|
|
220
|
+
for_each = var.oidc_identity
|
|
221
|
+
provider = aws.access
|
|
222
|
+
|
|
223
|
+
url = each.value.oidc_url
|
|
224
|
+
client_id_list = each.value.audiences
|
|
225
|
+
thumbprint_list = each.value.thumbprint
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# @ref: aws-custom-policy
|
|
229
|
+
# Custom permission documents loaded from JSON files in ./policy.
|
|
230
|
+
# The split() trick is what the real module uses to derive a name from
|
|
231
|
+
# the "<account>/<group>/<file>.json" key shape.
|
|
232
|
+
resource "aws_iam_policy" "policies" {
|
|
233
|
+
for_each = local.account_policies
|
|
234
|
+
provider = aws.access
|
|
235
|
+
|
|
236
|
+
name = split("/", each.key)[1]
|
|
237
|
+
description = each.value.description
|
|
238
|
+
policy = file("\${path.module}/policy/\${each.value.path}")
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# @ref: aws-iam-role
|
|
242
|
+
# Trust policy != permission policy. The trust policy answers
|
|
243
|
+
# "who is allowed to become this role?". Permissions come from attachments.
|
|
244
|
+
resource "aws_iam_role" "roles" {
|
|
245
|
+
for_each = local.account_roles
|
|
246
|
+
provider = aws.access
|
|
247
|
+
|
|
248
|
+
name = each.value.name
|
|
249
|
+
assume_role_policy = templatefile(
|
|
250
|
+
"\${path.module}/assume_role/\${each.value.assume_role_policy_file}",
|
|
251
|
+
each.value.assume_role_policy_variable,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# @ref: aws-role-policy-attachment (custom)
|
|
256
|
+
resource "aws_iam_role_policy_attachment" "custom_role_policy" {
|
|
257
|
+
for_each = local.custom_role_attachments
|
|
258
|
+
provider = aws.access
|
|
259
|
+
|
|
260
|
+
role = aws_iam_role.roles[each.value.tf_role_key].name
|
|
261
|
+
policy_arn = aws_iam_policy.policies[each.value.tf_policy_key].arn
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# @ref: aws-role-policy-attachment (managed)
|
|
265
|
+
resource "aws_iam_role_policy_attachment" "managed_role_policy" {
|
|
266
|
+
for_each = local.managed_role_attachments
|
|
267
|
+
provider = aws.access
|
|
268
|
+
|
|
269
|
+
role = aws_iam_role.roles[each.value.tf_role_key].name
|
|
270
|
+
policy_arn = each.value.policy_arn
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# @ref: aws-iam-user
|
|
274
|
+
# Long-lived identity. Kept as an exercise; modern setups prefer roles
|
|
275
|
+
# + federation, but the real module still supports both.
|
|
276
|
+
resource "aws_iam_user" "users" {
|
|
277
|
+
for_each = local.account_users
|
|
278
|
+
provider = aws.access
|
|
279
|
+
|
|
280
|
+
name = each.value.name
|
|
281
|
+
path = "/"
|
|
282
|
+
force_destroy = true
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
resource "aws_iam_user_policy_attachment" "custom_user_policy" {
|
|
286
|
+
for_each = local.custom_user_attachments
|
|
287
|
+
provider = aws.access
|
|
288
|
+
|
|
289
|
+
user = aws_iam_user.users[each.value.tf_user_key].name
|
|
290
|
+
policy_arn = aws_iam_policy.policies[each.value.tf_policy_key].arn
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
resource "aws_iam_user_policy_attachment" "managed_user_policy" {
|
|
294
|
+
for_each = local.managed_user_attachments
|
|
295
|
+
provider = aws.access
|
|
296
|
+
|
|
297
|
+
user = aws_iam_user.users[each.value.tf_user_key].name
|
|
298
|
+
policy_arn = each.value.policy_arn
|
|
299
|
+
}
|
|
300
|
+
`,
|
|
301
|
+
"ado.tf": `# @ref: aws-ado-role
|
|
302
|
+
# Mirrors the real ado.tf: a per-account role assumed by an Azure DevOps
|
|
303
|
+
# pipeline via OIDC, with AdministratorAccess attached. On LocalStack the
|
|
304
|
+
# role exists but the actual federation handshake is not exercised.
|
|
305
|
+
locals {
|
|
306
|
+
ado_accounts = ["access", "workload"]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
resource "aws_iam_role" "ado_role" {
|
|
310
|
+
for_each = toset(local.ado_accounts)
|
|
311
|
+
provider = aws.access
|
|
312
|
+
|
|
313
|
+
name = "ado-role-\${each.key}"
|
|
314
|
+
assume_role_policy = templatefile(
|
|
315
|
+
"\${path.module}/assume_role/ado-assume-role.json",
|
|
316
|
+
{
|
|
317
|
+
account_id = "000000000000"
|
|
318
|
+
environment = "lab"
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
resource "aws_iam_role_policy_attachment" "ado_admin" {
|
|
324
|
+
for_each = toset(local.ado_accounts)
|
|
325
|
+
provider = aws.access
|
|
326
|
+
|
|
327
|
+
role = aws_iam_role.ado_role[each.key].name
|
|
328
|
+
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
resource "aws_iam_role" "ado_readonly" {
|
|
332
|
+
for_each = toset(local.ado_accounts)
|
|
333
|
+
provider = aws.access
|
|
334
|
+
|
|
335
|
+
name = "ado-role-readonly-\${each.key}"
|
|
336
|
+
assume_role_policy = templatefile(
|
|
337
|
+
"\${path.module}/assume_role/ado-assume-role.json",
|
|
338
|
+
{
|
|
339
|
+
account_id = "000000000000"
|
|
340
|
+
environment = "lab"
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
resource "aws_iam_role_policy_attachment" "ado_readonly" {
|
|
346
|
+
for_each = toset(local.ado_accounts)
|
|
347
|
+
provider = aws.access
|
|
348
|
+
|
|
349
|
+
role = aws_iam_role.ado_readonly[each.key].name
|
|
350
|
+
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
|
|
351
|
+
}
|
|
352
|
+
`,
|
|
353
|
+
"outputs.tf": `output "policy_arns" {
|
|
354
|
+
description = "ARNs of all custom policies that were created."
|
|
355
|
+
value = { for k, p in aws_iam_policy.policies : k => p.arn }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
output "role_arns" {
|
|
359
|
+
description = "ARNs of all roles that were created."
|
|
360
|
+
value = { for k, r in aws_iam_role.roles : k => r.arn }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
output "ado_role_arns" {
|
|
364
|
+
description = "ARNs of the Azure DevOps deployment roles."
|
|
365
|
+
value = { for k, r in aws_iam_role.ado_role : k => r.arn }
|
|
366
|
+
}
|
|
367
|
+
`,
|
|
368
|
+
"terraform.tfvars": `# Sample data that drives the module. Everything below is what would
|
|
369
|
+
# normally live in variables.<env>.tfvars in the real repo.
|
|
370
|
+
|
|
371
|
+
policies = {
|
|
372
|
+
"access/external-dns-policy-v1" = {
|
|
373
|
+
path = "external-dns-policy-v1.json"
|
|
374
|
+
description = "Allows external-dns to manage Route53 records."
|
|
375
|
+
}
|
|
376
|
+
"access/cloudwatch-exporter-policy-v1" = {
|
|
377
|
+
path = "cloudwatch-exporter-policy-v1.json"
|
|
378
|
+
description = "Allows the cloudwatch exporter to read metrics."
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
roles = {
|
|
383
|
+
"access/external-dns" = {
|
|
384
|
+
name = "external-dns-role"
|
|
385
|
+
assume_role_policy_file = "eks-irsa-assume-role.json"
|
|
386
|
+
assume_role_policy_variable = {
|
|
387
|
+
account_id = "000000000000"
|
|
388
|
+
oidc_provider = "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633"
|
|
389
|
+
namespace = "kube-system"
|
|
390
|
+
service_account = "external-dns"
|
|
391
|
+
}
|
|
392
|
+
custom_policies = [
|
|
393
|
+
"access/external-dns-policy-v1",
|
|
394
|
+
]
|
|
395
|
+
managed_policy_arns = []
|
|
396
|
+
}
|
|
397
|
+
"access/cw-exporter" = {
|
|
398
|
+
name = "cloudwatch-exporter-role"
|
|
399
|
+
assume_role_policy_file = "eks-irsa-assume-role.json"
|
|
400
|
+
assume_role_policy_variable = {
|
|
401
|
+
account_id = "000000000000"
|
|
402
|
+
oidc_provider = "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633"
|
|
403
|
+
namespace = "monitoring"
|
|
404
|
+
service_account = "cloudwatch-exporter"
|
|
405
|
+
}
|
|
406
|
+
custom_policies = [
|
|
407
|
+
"access/cloudwatch-exporter-policy-v1",
|
|
408
|
+
]
|
|
409
|
+
managed_policy_arns = [
|
|
410
|
+
"arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess",
|
|
411
|
+
]
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
users = {
|
|
416
|
+
"access/break-glass" = {
|
|
417
|
+
name = "break-glass-admin"
|
|
418
|
+
custom_policies = []
|
|
419
|
+
managed_policy_arns = [
|
|
420
|
+
"arn:aws:iam::aws:policy/ReadOnlyAccess",
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
oidc_identity = {
|
|
426
|
+
"access/eks-main" = {
|
|
427
|
+
account = "access"
|
|
428
|
+
oidc_url = "https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633"
|
|
429
|
+
audiences = ["sts.amazonaws.com"]
|
|
430
|
+
thumbprint = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"]
|
|
431
|
+
}
|
|
432
|
+
"access/azure-devops" = {
|
|
433
|
+
account = "access"
|
|
434
|
+
oidc_url = "https://vstoken.dev.azure.com/00000000-0000-0000-0000-000000000000"
|
|
435
|
+
audiences = ["api://AzureADTokenExchange"]
|
|
436
|
+
thumbprint = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"]
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
`,
|
|
440
|
+
"policy/external-dns-policy-v1.json": `{
|
|
441
|
+
"Version": "2012-10-17",
|
|
442
|
+
"Statement": [
|
|
443
|
+
{
|
|
444
|
+
"Effect": "Allow",
|
|
445
|
+
"Action": [
|
|
446
|
+
"route53:ChangeResourceRecordSets"
|
|
447
|
+
],
|
|
448
|
+
"Resource": "arn:aws:route53:::hostedzone/*"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"Effect": "Allow",
|
|
452
|
+
"Action": [
|
|
453
|
+
"route53:ListHostedZones",
|
|
454
|
+
"route53:ListResourceRecordSets"
|
|
455
|
+
],
|
|
456
|
+
"Resource": "*"
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
}
|
|
460
|
+
`,
|
|
461
|
+
"policy/cloudwatch-exporter-policy-v1.json": `{
|
|
462
|
+
"Version": "2012-10-17",
|
|
463
|
+
"Statement": [
|
|
464
|
+
{
|
|
465
|
+
"Effect": "Allow",
|
|
466
|
+
"Action": [
|
|
467
|
+
"cloudwatch:GetMetricData",
|
|
468
|
+
"cloudwatch:GetMetricStatistics",
|
|
469
|
+
"cloudwatch:ListMetrics",
|
|
470
|
+
"tag:GetResources"
|
|
471
|
+
],
|
|
472
|
+
"Resource": "*"
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
}
|
|
476
|
+
`,
|
|
477
|
+
"assume_role/eks-irsa-assume-role.json": `{
|
|
478
|
+
"Version": "2012-10-17",
|
|
479
|
+
"Statement": [
|
|
480
|
+
{
|
|
481
|
+
"Effect": "Allow",
|
|
482
|
+
"Principal": {
|
|
483
|
+
"Federated": "arn:aws:iam::\${account_id}:oidc-provider/\${oidc_provider}"
|
|
484
|
+
},
|
|
485
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
486
|
+
"Condition": {
|
|
487
|
+
"StringEquals": {
|
|
488
|
+
"\${oidc_provider}:sub": "system:serviceaccount:\${namespace}:\${service_account}",
|
|
489
|
+
"\${oidc_provider}:aud": "sts.amazonaws.com"
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
}
|
|
495
|
+
`,
|
|
496
|
+
"assume_role/ado-assume-role.json": `{
|
|
497
|
+
"Version": "2012-10-17",
|
|
498
|
+
"Statement": [
|
|
499
|
+
{
|
|
500
|
+
"Effect": "Allow",
|
|
501
|
+
"Principal": {
|
|
502
|
+
"Federated": "arn:aws:iam::\${account_id}:oidc-provider/vstoken.dev.azure.com/00000000-0000-0000-0000-000000000000"
|
|
503
|
+
},
|
|
504
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
505
|
+
"Condition": {
|
|
506
|
+
"StringEquals": {
|
|
507
|
+
"vstoken.dev.azure.com/00000000-0000-0000-0000-000000000000:aud": "api://AzureADTokenExchange"
|
|
508
|
+
},
|
|
509
|
+
"StringLike": {
|
|
510
|
+
"vstoken.dev.azure.com/00000000-0000-0000-0000-000000000000:sub": "sc://my-org/my-project/\${environment}-*"
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
]
|
|
515
|
+
}
|
|
516
|
+
`,
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export const AWS_GOVERNANCE_IAM_LAB: InfraLabWorkspace = {
|
|
520
|
+
version: 1,
|
|
521
|
+
label: "AWS Governance IAM Lab",
|
|
522
|
+
provider: "aws",
|
|
523
|
+
executionMode: "localstack",
|
|
524
|
+
activeFile: "README.md",
|
|
525
|
+
files: AWS_GOVERNANCE_IAM_FILES,
|
|
526
|
+
};
|
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
parseInfraLabWorkspace,
|
|
9
9
|
} from "../infraLab";
|
|
10
10
|
import {
|
|
11
|
+
AWS_GOVERNANCE_GHA_LAB,
|
|
11
12
|
DEFAULT_GHA_LAB,
|
|
12
13
|
GOVERNANCE_GHA_LAB,
|
|
13
14
|
parseGhaLabWorkspace,
|
|
14
15
|
REACT_VITE_TYPESCRIPT_GHA_LAB,
|
|
15
16
|
} from "../githubActionsLab";
|
|
16
17
|
import { ENTERPRISE_LOCAL_AUTH_LAB } from "../enterpriseLocalLab";
|
|
18
|
+
import { AWS_GOVERNANCE_IAM_LAB } from "../awsGovernanceIamLab";
|
|
17
19
|
import {
|
|
18
20
|
parseFrontendLabWorkspace,
|
|
19
21
|
ISOLATED_MODULE_FEDERATION_LAB,
|
|
@@ -672,6 +674,12 @@ export default function LabsPanel() {
|
|
|
672
674
|
"Terraform deploys NestJS BFF, OIDC mock, Redis & claims API",
|
|
673
675
|
onClick: () => openInfraLab(ENTERPRISE_LOCAL_AUTH_LAB),
|
|
674
676
|
},
|
|
677
|
+
{
|
|
678
|
+
label: "AWS Governance IAM",
|
|
679
|
+
description:
|
|
680
|
+
"Roles, policies, attachments, users & OIDC mirroring a real governance-iam module on LocalStack",
|
|
681
|
+
onClick: () => openInfraLab(AWS_GOVERNANCE_IAM_LAB),
|
|
682
|
+
},
|
|
675
683
|
]}
|
|
676
684
|
onOpen={openInfraFile}
|
|
677
685
|
openTitle="Open in Infrastructure Lab"
|
|
@@ -703,6 +711,12 @@ export default function LabsPanel() {
|
|
|
703
711
|
"PLF-style mono-repo: CODEOWNERS, PR template, Azure PIM/Policy + AWS IAM deploy workflows, offboarding",
|
|
704
712
|
onClick: () => openGhaLab(GOVERNANCE_GHA_LAB),
|
|
705
713
|
},
|
|
714
|
+
{
|
|
715
|
+
label: "AWS Governance IAM via GitHub Actions",
|
|
716
|
+
description:
|
|
717
|
+
"PR plan + main-branch apply workflows that drive a real Terraform IAM module against LocalStack via a reusable composite action",
|
|
718
|
+
onClick: () => openGhaLab(AWS_GOVERNANCE_GHA_LAB),
|
|
719
|
+
},
|
|
706
720
|
]}
|
|
707
721
|
onOpen={openGhaFile}
|
|
708
722
|
openTitle="Open in GitHub Lab"
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
GithubLabRulesetRules,
|
|
13
13
|
} from "./types";
|
|
14
14
|
import { rulesetFromLegacyProtection } from "./codeowners";
|
|
15
|
+
import { AWS_GOVERNANCE_IAM_FILES } from "./awsGovernanceIamLab";
|
|
15
16
|
|
|
16
17
|
// ─── Default GitHub Lab "org" roster ────────────────────────────────────
|
|
17
18
|
//
|
|
@@ -2169,6 +2170,238 @@ Write-Host "\`nDone. DryRun=$DryRun"
|
|
|
2169
2170
|
`,
|
|
2170
2171
|
};
|
|
2171
2172
|
|
|
2173
|
+
// ─── AWS Governance IAM via GitHub Actions ───────────────────────────────
|
|
2174
|
+
//
|
|
2175
|
+
// Combines the Infra Lab's `governance-iam` Terraform module with a
|
|
2176
|
+
// GitHub-Actions-driven deploy flow that mirrors the real PLF setup, but
|
|
2177
|
+
// runs entirely locally via `act` + LocalStack:
|
|
2178
|
+
//
|
|
2179
|
+
// - PR opens / updates → `terraform-pr.yml` runs `tofu plan`
|
|
2180
|
+
// - merge to main → `terraform-ci.yml` runs `tofu apply`
|
|
2181
|
+
// - shared steps live in a composite action `./.github/actions/run-tofu-action`
|
|
2182
|
+
//
|
|
2183
|
+
// LocalStack is reachable from the act runner container at
|
|
2184
|
+
// `host.docker.internal:4566` on macOS / Windows, and via the
|
|
2185
|
+
// `--network host` runner override (or the literal IP) on Linux.
|
|
2186
|
+
const AWS_IAM_GHA_FILES: Record<string, string> = {
|
|
2187
|
+
"README.md": `# AWS Governance IAM — GitHub Actions Lab
|
|
2188
|
+
|
|
2189
|
+
End-to-end mimic of the real PLF \`governance-iam\` deploy flow, but driven
|
|
2190
|
+
by **GitHub Actions** (not Azure DevOps) and pointed at **LocalStack**
|
|
2191
|
+
instead of real AWS.
|
|
2192
|
+
|
|
2193
|
+
## What this lab demonstrates
|
|
2194
|
+
|
|
2195
|
+
1. \`PR opens\` → workflow runs \`tofu plan\` (read-only preview)
|
|
2196
|
+
2. \`merge to main\` → workflow runs \`tofu apply\` (deploy)
|
|
2197
|
+
3. **Composite action** \`./.github/actions/run-tofu-action\` factors out the
|
|
2198
|
+
init/plan/apply boilerplate, exactly like the real repo's reusable action.
|
|
2199
|
+
4. **Auth** is faked with static \`test\` credentials and a LocalStack
|
|
2200
|
+
endpoint — the *shape* matches \`provider.tf\`'s assume-role pattern.
|
|
2201
|
+
|
|
2202
|
+
## How to run it
|
|
2203
|
+
|
|
2204
|
+
In the right pane, pick an event + workflow and click **Run**. Useful combos:
|
|
2205
|
+
|
|
2206
|
+
- \`pull_request\` + \`terraform-pr.yml\` → simulates a plan run on a PR
|
|
2207
|
+
- \`push\` + \`terraform-ci.yml\` → simulates an apply on main
|
|
2208
|
+
- \`workflow_dispatch\` + either workflow → manual trigger
|
|
2209
|
+
|
|
2210
|
+
You also need LocalStack running on \`localhost:4566\` on your host. The
|
|
2211
|
+
runner container reaches it via \`host.docker.internal:4566\`.
|
|
2212
|
+
|
|
2213
|
+
## How this maps to the real PLF setup
|
|
2214
|
+
|
|
2215
|
+
| Real PLF | This lab |
|
|
2216
|
+
| ------------------------------------- | ----------------------------------------------- |
|
|
2217
|
+
| Azure DevOps pipeline (\`pr.yml\`) | GitHub Actions workflow \`terraform-pr.yml\` |
|
|
2218
|
+
| Azure DevOps pipeline (\`ci.yml\`) | GitHub Actions workflow \`terraform-ci.yml\` |
|
|
2219
|
+
| Shared template \`deploy-aws.yml\` | Composite action \`run-tofu-action\` |
|
|
2220
|
+
| AWS service connection | Static \`test\` creds + LocalStack endpoint |
|
|
2221
|
+
| Azure OIDC for state backend | Local state file (no remote backend) |
|
|
2222
|
+
| \`assume_role\` to target accounts | Single LocalStack account, dual provider alias |
|
|
2223
|
+
|
|
2224
|
+
## File map
|
|
2225
|
+
|
|
2226
|
+
- \`.github/workflows/terraform-pr.yml\` — plan on PR
|
|
2227
|
+
- \`.github/workflows/terraform-ci.yml\` — apply on push to main
|
|
2228
|
+
- \`.github/actions/run-tofu-action/\` — reusable plan/apply steps
|
|
2229
|
+
- \`.github/CODEOWNERS\` — PR review routing
|
|
2230
|
+
- \`terraform/\` — full governance-iam module
|
|
2231
|
+
`,
|
|
2232
|
+
|
|
2233
|
+
".github/CODEOWNERS": `# Reviewers auto-requested when terraform/ changes.
|
|
2234
|
+
/terraform/ @acme/platform
|
|
2235
|
+
/.github/ @acme/platform
|
|
2236
|
+
*.md @acme/docs
|
|
2237
|
+
`,
|
|
2238
|
+
|
|
2239
|
+
".github/pull_request_template.md": `## Summary
|
|
2240
|
+
|
|
2241
|
+
<!-- What does this change to the IAM module? -->
|
|
2242
|
+
|
|
2243
|
+
## Plan output
|
|
2244
|
+
|
|
2245
|
+
\`\`\`
|
|
2246
|
+
<!-- Paste the relevant section of \`tofu plan\` here -->
|
|
2247
|
+
\`\`\`
|
|
2248
|
+
|
|
2249
|
+
## Checklist
|
|
2250
|
+
|
|
2251
|
+
- [ ] PR title follows conventional-commit style
|
|
2252
|
+
- [ ] \`terraform-pr.yml\` shows a clean plan
|
|
2253
|
+
- [ ] No unintended role/policy deletions
|
|
2254
|
+
`,
|
|
2255
|
+
|
|
2256
|
+
".github/workflows/terraform-pr.yml": `name: terraform-pr
|
|
2257
|
+
|
|
2258
|
+
# PR-only workflow. Mirrors the real PLF \`pr.yml\`: run a plan, never apply.
|
|
2259
|
+
on:
|
|
2260
|
+
pull_request:
|
|
2261
|
+
paths:
|
|
2262
|
+
- "terraform/**"
|
|
2263
|
+
- ".github/workflows/terraform-*.yml"
|
|
2264
|
+
- ".github/actions/run-tofu-action/**"
|
|
2265
|
+
workflow_dispatch:
|
|
2266
|
+
|
|
2267
|
+
permissions:
|
|
2268
|
+
contents: read
|
|
2269
|
+
pull-requests: write
|
|
2270
|
+
|
|
2271
|
+
jobs:
|
|
2272
|
+
plan:
|
|
2273
|
+
runs-on: ubuntu-latest
|
|
2274
|
+
steps:
|
|
2275
|
+
- uses: actions/checkout@v4
|
|
2276
|
+
|
|
2277
|
+
- name: Plan IAM module
|
|
2278
|
+
uses: ./.github/actions/run-tofu-action
|
|
2279
|
+
with:
|
|
2280
|
+
working-directory: terraform
|
|
2281
|
+
mode: plan
|
|
2282
|
+
`,
|
|
2283
|
+
|
|
2284
|
+
".github/workflows/terraform-ci.yml": `name: terraform-ci
|
|
2285
|
+
|
|
2286
|
+
# Main-branch workflow. Mirrors the real PLF \`ci.yml\`: apply after merge.
|
|
2287
|
+
on:
|
|
2288
|
+
push:
|
|
2289
|
+
branches: [main]
|
|
2290
|
+
paths:
|
|
2291
|
+
- "terraform/**"
|
|
2292
|
+
- ".github/workflows/terraform-*.yml"
|
|
2293
|
+
- ".github/actions/run-tofu-action/**"
|
|
2294
|
+
workflow_dispatch:
|
|
2295
|
+
|
|
2296
|
+
permissions:
|
|
2297
|
+
contents: read
|
|
2298
|
+
|
|
2299
|
+
jobs:
|
|
2300
|
+
apply:
|
|
2301
|
+
runs-on: ubuntu-latest
|
|
2302
|
+
steps:
|
|
2303
|
+
- uses: actions/checkout@v4
|
|
2304
|
+
|
|
2305
|
+
- name: Apply IAM module
|
|
2306
|
+
uses: ./.github/actions/run-tofu-action
|
|
2307
|
+
with:
|
|
2308
|
+
working-directory: terraform
|
|
2309
|
+
mode: apply
|
|
2310
|
+
`,
|
|
2311
|
+
|
|
2312
|
+
".github/actions/run-tofu-action/action.yml": `# Reusable composite action — same idea as the real repo's run-tofu-action.
|
|
2313
|
+
# Hides the OpenTofu install + init + plan/apply behind a single \`uses:\`.
|
|
2314
|
+
|
|
2315
|
+
name: "Run OpenTofu"
|
|
2316
|
+
description: "Install OpenTofu, init, then plan or apply against LocalStack."
|
|
2317
|
+
|
|
2318
|
+
inputs:
|
|
2319
|
+
working-directory:
|
|
2320
|
+
description: "Folder containing the Terraform/OpenTofu config."
|
|
2321
|
+
required: true
|
|
2322
|
+
mode:
|
|
2323
|
+
description: "Either 'plan' or 'apply'."
|
|
2324
|
+
required: true
|
|
2325
|
+
default: "plan"
|
|
2326
|
+
|
|
2327
|
+
runs:
|
|
2328
|
+
using: "composite"
|
|
2329
|
+
steps:
|
|
2330
|
+
- name: Install OpenTofu
|
|
2331
|
+
shell: bash
|
|
2332
|
+
run: |
|
|
2333
|
+
curl -fsSL https://get.opentofu.org/install-opentofu.sh -o /tmp/install-opentofu.sh
|
|
2334
|
+
chmod +x /tmp/install-opentofu.sh
|
|
2335
|
+
/tmp/install-opentofu.sh --install-method standalone --skip-verify
|
|
2336
|
+
tofu --version
|
|
2337
|
+
|
|
2338
|
+
- name: Tofu init
|
|
2339
|
+
shell: bash
|
|
2340
|
+
working-directory: \${{ inputs.working-directory }}
|
|
2341
|
+
env:
|
|
2342
|
+
AWS_ACCESS_KEY_ID: test
|
|
2343
|
+
AWS_SECRET_ACCESS_KEY: test
|
|
2344
|
+
AWS_DEFAULT_REGION: us-east-1
|
|
2345
|
+
run: tofu init -input=false
|
|
2346
|
+
|
|
2347
|
+
- name: Tofu plan
|
|
2348
|
+
if: \${{ inputs.mode == 'plan' }}
|
|
2349
|
+
shell: bash
|
|
2350
|
+
working-directory: \${{ inputs.working-directory }}
|
|
2351
|
+
env:
|
|
2352
|
+
AWS_ACCESS_KEY_ID: test
|
|
2353
|
+
AWS_SECRET_ACCESS_KEY: test
|
|
2354
|
+
AWS_DEFAULT_REGION: us-east-1
|
|
2355
|
+
run: tofu plan -input=false -no-color
|
|
2356
|
+
|
|
2357
|
+
- name: Tofu apply
|
|
2358
|
+
if: \${{ inputs.mode == 'apply' }}
|
|
2359
|
+
shell: bash
|
|
2360
|
+
working-directory: \${{ inputs.working-directory }}
|
|
2361
|
+
env:
|
|
2362
|
+
AWS_ACCESS_KEY_ID: test
|
|
2363
|
+
AWS_SECRET_ACCESS_KEY: test
|
|
2364
|
+
AWS_DEFAULT_REGION: us-east-1
|
|
2365
|
+
run: tofu apply -input=false -auto-approve -no-color
|
|
2366
|
+
`,
|
|
2367
|
+
|
|
2368
|
+
".actrc": `# Pin the runner image so installs are reproducible across machines.
|
|
2369
|
+
-P ubuntu-latest=catthehacker/ubuntu:act-latest
|
|
2370
|
+
--container-architecture linux/amd64
|
|
2371
|
+
`,
|
|
2372
|
+
|
|
2373
|
+
// Inline the full governance-iam Terraform module under \`terraform/\` so
|
|
2374
|
+
// the workflows have something real to plan/apply against.
|
|
2375
|
+
...Object.fromEntries(
|
|
2376
|
+
Object.entries(AWS_GOVERNANCE_IAM_FILES).map(([path, body]) => {
|
|
2377
|
+
// The IAM module assumes provider endpoints point at \`localhost:4566\`,
|
|
2378
|
+
// but the act runner is a separate container and can't reach
|
|
2379
|
+
// \`localhost\` on the host. Rewrite to host.docker.internal so the
|
|
2380
|
+
// workflow run actually succeeds.
|
|
2381
|
+
const rewritten =
|
|
2382
|
+
path === "provider.tf"
|
|
2383
|
+
? body.replace(
|
|
2384
|
+
/http:\/\/localhost:4566/g,
|
|
2385
|
+
"http://host.docker.internal:4566",
|
|
2386
|
+
)
|
|
2387
|
+
: body;
|
|
2388
|
+
return [`terraform/${path}`, rewritten];
|
|
2389
|
+
}),
|
|
2390
|
+
),
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
export const AWS_GOVERNANCE_GHA_LAB: GithubActionsLabWorkspace = {
|
|
2394
|
+
version: 1,
|
|
2395
|
+
label: "AWS Governance IAM via GitHub Actions",
|
|
2396
|
+
activeFile: ".github/workflows/terraform-pr.yml",
|
|
2397
|
+
defaultEvent: "pull_request",
|
|
2398
|
+
defaultWorkflow: ".github/workflows/terraform-pr.yml",
|
|
2399
|
+
files: AWS_IAM_GHA_FILES,
|
|
2400
|
+
ghOrg: DEFAULT_GH_LAB_ORG,
|
|
2401
|
+
rulesets: DEFAULT_GH_LAB_RULESETS,
|
|
2402
|
+
pullRequest: DEFAULT_GH_LAB_PULL_REQUEST,
|
|
2403
|
+
};
|
|
2404
|
+
|
|
2172
2405
|
export const GOVERNANCE_GHA_LAB: GithubActionsLabWorkspace = {
|
|
2173
2406
|
version: 1,
|
|
2174
2407
|
label: "Platform Governance Template",
|
package/template/cockpit.json
CHANGED