hecks 1.4.0 → 1.5.1

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 (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -0,0 +1,624 @@
1
+ module Hecks
2
+ module Projections
3
+ module Deploy
4
+ # Plumbing `Lambda` and `Fargate` both need — VPC/subnet/security-group
5
+ # resources, RDS/Aurora, `bastion.yaml`, and a cross-domain policy's
6
+ # own least-privilege invoke grant are the same problem (get a
7
+ # domain's own Postgres instance stood up and reachable for the one
8
+ # boot that mints era 1, and let a domain call another domain's
9
+ # Lambda) whichever compute target ships the domain's own code.
10
+ #
11
+ # Plain module functions, not a registered `Projector::Target` — this
12
+ # has no `deployed_to(...)` block of its own to generate from, only
13
+ # helpers the two real targets call with the facts they have already
14
+ # resolved (`db_id`, `aurora`, `google_oauth_present`, and so on).
15
+ #
16
+ # Every `*_table` method here answers the same `key`/`var`/`name`
17
+ # strings regardless of which target built them — `stack_outputs`'
18
+ # own `"FunctionSecurityGroupId"` key, in particular, is what a
19
+ # Shared-mode domain looks up from its owner's stack (`bin/project_deploy`'s
20
+ # own Shared-mode branch), and that lookup has to succeed the same
21
+ # way whether the owner is a `Lambda` or a `Fargate` deploy.
22
+ module Shared
23
+ module_function
24
+
25
+ # The stack↔bastion contract — one shared table each of
26
+ # `template.yaml`'s Outputs, `bastion.yaml`'s Parameters, and a
27
+ # Makefile's own eval/`--parameter-overrides` lines reads from,
28
+ # instead of three to four independent string literals per fact
29
+ # with nothing checking they agree. `var` is the Make variable
30
+ # each output becomes.
31
+ #
32
+ # Empty when `shared` — a Shared-mode domain provisions no
33
+ # VPC/RDS of its own to expose at all (`bastion_parameters`,
34
+ # `bastion_yaml`, and era-minting are all skipped for the same
35
+ # reason: there's nothing here for a bastion to reach). A
36
+ # Shared-mode domain's own stack is never an owner, so nothing
37
+ # downstream ever needs to look these up from it either.
38
+ #
39
+ # @param shared [Boolean] whether this domain borrows another domain's
40
+ # RDS instance (`database "Shared"`) rather than provisioning its own
41
+ # @param db_id [String] the RDS/VPC resources' own shared logical-id prefix
42
+ # @param db_ref_id [String] the logical id `.Endpoint`/`.MasterUserSecret` resolve
43
+ # against — `db_id` itself for plain RDS, `"#{db_id}Cluster"` for Aurora
44
+ # @param secret_intrinsic [String] the rendered `!Ref`/`!GetAtt` expression for
45
+ # this domain's own database secret
46
+ # @param compute_security_group_ref [String] the `!Ref` expression for the
47
+ # compute resource's own security group (a Lambda function's, an `ECS`
48
+ # service's) — reused by a Shared-mode domain elsewhere to attach its own
49
+ # compute to this domain's VPC without minting a security group of its own
50
+ # @param google_oauth_present [Boolean] whether this domain's own NAT
51
+ # Gateway/public-subnet resources exist, and so whether `PublicSubnetId`/
52
+ # `BastionSubnetId` are real outputs to expose
53
+ # @return [Array<Hash{Symbol => String}>] frozen; each entry a `{key:, var:,
54
+ # ref:}` triple
55
+ def stack_outputs(shared:, db_id:, db_ref_id:, secret_intrinsic:, compute_security_group_ref:, google_oauth_present:)
56
+ return [].freeze if shared
57
+
58
+ [
59
+ { key: "VpcId", var: "VPC_ID", ref: "!Ref #{db_id}Vpc" },
60
+ { key: "DbSecurityGroupId", var: "DB_SG_ID", ref: "!Ref #{db_id}SecurityGroup" },
61
+ { key: "DatabaseEndpoint", var: "DB_HOST", ref: "!GetAtt #{db_ref_id}.Endpoint.Address" },
62
+ { key: "DatabaseSecretArn", var: "DB_SECRET_ARN", ref: secret_intrinsic },
63
+ # Not consumed by anything in this domain's own generated output —
64
+ # a "Shared"-mode domain elsewhere looks these two up live to
65
+ # attach its own compute to this domain's VPC/security group
66
+ # without minting either itself.
67
+ { key: "FunctionSecurityGroupId", var: "FN_SG_ID", ref: compute_security_group_ref },
68
+ { key: "PrivateSubnetAId", var: "PRIVATE_SUBNET_A_ID", ref: "!Ref #{db_id}SubnetA" },
69
+ { key: "PrivateSubnetBId", var: "PRIVATE_SUBNET_B_ID", ref: "!Ref #{db_id}SubnetB" },
70
+ # Only when google_oauth_present — #{db_id}PublicSubnet (and the
71
+ # Internet Gateway it's attached through) only exist as resources
72
+ # at all when the NAT Gateway block does. `bastion.yaml` needs
73
+ # this: a VPC accepts only one attached Internet Gateway, and one
74
+ # already exists once this is true — its own temporary IGW+subnet
75
+ # would collide (a real, live "Resource.AlreadyAssociated").
76
+ *(google_oauth_present ? [{ key: "PublicSubnetId", var: "PUBLIC_SUBNET_ID", ref: "!Ref #{db_id}PublicSubnet" }] : []),
77
+ # A separate subnet from PublicSubnetId, same shared route
78
+ # table/Internet Gateway -- #{db_id}PublicSubnet always lands in
79
+ # AZ index 0 (`!Select [0, !GetAZs '']`), and a real, live
80
+ # CREATE_FAILED caught this account unable to launch any EC2
81
+ # instance there at all ("Your requested instance type ... is
82
+ # not supported in your requested Availability Zone
83
+ # (us-east-1a)") -- NatGateway and RDS aren't EC2 instances and
84
+ # never hit this, only BastionInstance does. AZ index 1 instead,
85
+ # matching #{db_id}SubnetB's own choice.
86
+ *(google_oauth_present ? [{ key: "BastionSubnetId", var: "BASTION_SUBNET_ID", ref: "!Ref #{db_id}BastionPublicSubnet" }] : []),
87
+ ].freeze
88
+ end
89
+
90
+ # `bastion.yaml`'s own Parameters — deliberately allowed to rename
91
+ # (`RdsSecurityGroupId` reads better inside `bastion.yaml` than the
92
+ # stack output's own `DbSecurityGroupId`), which is exactly the kind
93
+ # of rename that can silently drift between the two files without
94
+ # this shared table.
95
+ #
96
+ # @param shared [Boolean] whether this domain borrows another domain's
97
+ # RDS instance — see `stack_outputs`' own comment
98
+ # @param google_oauth_present [Boolean] see `stack_outputs`
99
+ # @return [Array<Hash{Symbol => String}>] frozen; each entry a `{name:,
100
+ # from_output:, type:}` triple
101
+ def bastion_parameters(shared:, google_oauth_present:)
102
+ return [].freeze if shared
103
+
104
+ [
105
+ { name: "VpcId", from_output: "VpcId", type: "AWS::EC2::VPC::Id" },
106
+ { name: "RdsSecurityGroupId", from_output: "DbSecurityGroupId", type: "AWS::EC2::SecurityGroup::Id" },
107
+ *(google_oauth_present ? [{ name: "PublicSubnetId", from_output: "PublicSubnetId", type: "AWS::EC2::Subnet::Id" }] : []),
108
+ *(google_oauth_present ? [{ name: "BastionSubnetId", from_output: "BastionSubnetId", type: "AWS::EC2::Subnet::Id" }] : []),
109
+ ].freeze
110
+ end
111
+
112
+ # A generation-time assertion, not a bluebook refusal — this is the
113
+ # generator catching its own bug immediately, before writing a
114
+ # single file, instead of producing a `bastion.yaml` whose Parameter
115
+ # nothing could ever fill.
116
+ #
117
+ # @param bastion_parameters [Array<Hash{Symbol => String}>] as `bastion_parameters` builds
118
+ # @param stack_outputs [Array<Hash{Symbol => String}>] as `stack_outputs` builds
119
+ # @return [void]
120
+ # @raise [RuntimeError] if any parameter names a `from_output` absent from
121
+ # `stack_outputs`
122
+ def check_bastion_parameters!(bastion_parameters, stack_outputs)
123
+ bastion_parameters.each do |param|
124
+ stack_outputs.any? { |o| o[:key] == param[:from_output] } or
125
+ raise "bastion parameter #{param[:name]} references undeclared stack output #{param[:from_output]}"
126
+ end
127
+ end
128
+
129
+ # Renders the least-privilege IAM grant a cross-domain policy's own
130
+ # invoke needs — one `lambda:InvokeFunction` statement per declared
131
+ # `across:` target, appended to the compute role's existing
132
+ # `Policies:` list. `rust/host`'s own `lambda_client.rs`
133
+ # (`AwsLambdaInvoker`) is the one piece of a cross-domain dispatch
134
+ # path that needs this at all, regardless of which compute target
135
+ # ships the calling domain's own code — a Fargate task and a Lambda
136
+ # function both reach another domain's dispatch Lambda the same way.
137
+ #
138
+ # Declared, not deployed: an ARN naming a function that does not
139
+ # exist yet is still valid IAM policy — the call simply fails with
140
+ # `ResourceNotFoundException` instead of `AccessDeniedException`
141
+ # until the target domain is deployed for real.
142
+ #
143
+ # @param targets [Array<String>] the domain names this stack's `across:` targets
144
+ # declare
145
+ # @param base [String] the marker line's own rendered indentation whitespace;
146
+ # everything below is built relative to it
147
+ # @return [String] the rendered comment plus one `Statement`-shaped IAM policy
148
+ # entry granting invoke on each target's function ARN, ending in exactly one
149
+ # trailing newline; an empty string if `targets` is empty
150
+ def cross_domain_invoke_policy_yaml(targets, base)
151
+ return "" if targets.empty?
152
+
153
+ comment = [
154
+ "least-privilege, one ARN per declared `across:` target -- the same",
155
+ "shape this compute's own DB-secret grant already takes, extended to",
156
+ "a target this stack does not own (so no `!Ref` to reach for -- the",
157
+ "target's own function name is `lambda_client.rs`'s own computed",
158
+ '"hecks-#{domain}" convention, read directly, matching every other',
159
+ "function-name computation in this whole project). Targets: #{targets.join(', ')}.",
160
+ ].map { |line| "#{base}# #{line}" }.join("\n")
161
+
162
+ resources = targets.map { |target|
163
+ "#{base} - !Sub \"arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:hecks-#{target.downcase}\""
164
+ }.join("\n")
165
+
166
+ [
167
+ comment,
168
+ "#{base}- Statement:",
169
+ "#{base} - Effect: Allow",
170
+ "#{base} Action: lambda:InvokeFunction",
171
+ "#{base} Resource:",
172
+ resources,
173
+ ].join("\n") + "\n"
174
+ end
175
+
176
+ # Renders one domain's own private VPC, subnets, RDS/Aurora Postgres
177
+ # instance, and the two security groups pairing it with its compute
178
+ # — everything `Lambda` and `Fargate` both need to give a
179
+ # non-`"Shared"`-mode domain a real, reachable Postgres endpoint.
180
+ # Dedented flush-left, the same shape a caller's own enclosing
181
+ # `<<~` heredoc expects to reindent as it splices this in.
182
+ #
183
+ # `compute_logical_id`'s own security group carries no inbound rule
184
+ # at all — neither a Lambda function nor a Fargate task/`ALB` target
185
+ # group receives traffic over this VPC-attached `ENI` directly, only
186
+ # egress to the database (and, when `google_oauth_present`, to the
187
+ # internet for a real OAuth token exchange).
188
+ #
189
+ # @param db_id [String] the RDS/VPC resources' own shared logical-id prefix
190
+ # @param db_name [String] the database identifier RDS/Aurora provisions
191
+ # @param infra_name [String] the domain's own AWS-facing name, quoted in the
192
+ # `DBSubnetGroupDescription`
193
+ # @param aurora [Boolean] Aurora Serverless v2 when true, plain RDS otherwise
194
+ # @param google_oauth_present [Boolean] whether a NAT Gateway, public subnet, and
195
+ # the compute security group's own internet egress rule are needed
196
+ # @param compute_logical_id [String] the compute resource's own logical id,
197
+ # naming its security group and egress rules
198
+ # @param compute_description [String] the compute security group's own
199
+ # `GroupDescription` text — what actually terminates the `ENI` this ingress
200
+ # pairs with
201
+ # @return [String] the rendered CloudFormation Resources, flush-left
202
+ def vpc_and_database_yaml(db_id:, db_name:, infra_name:, aurora:, google_oauth_present:, compute_logical_id:, compute_description:)
203
+ <<~OWNDB.rstrip
204
+ #{db_id}Vpc:
205
+ Type: AWS::EC2::VPC
206
+ Properties:
207
+ CidrBlock: 10.0.0.0/16
208
+ EnableDnsSupport: true
209
+ EnableDnsHostnames: true
210
+
211
+ #{db_id}SubnetA:
212
+ Type: AWS::EC2::Subnet
213
+ Properties:
214
+ VpcId: !Ref #{db_id}Vpc
215
+ CidrBlock: 10.0.1.0/24
216
+ AvailabilityZone: !Select [0, !GetAZs '']
217
+
218
+ #{db_id}SubnetB:
219
+ Type: AWS::EC2::Subnet
220
+ Properties:
221
+ VpcId: !Ref #{db_id}Vpc
222
+ CidrBlock: 10.0.2.0/24
223
+ AvailabilityZone: !Select [1, !GetAZs '']
224
+
225
+ #{google_oauth_present ? <<~NATGW.rstrip : ""}
226
+ # Real Google OAuth token exchange needs real internet access --
227
+ # see the enclosing template's own "NO NAT GATEWAY" comment for
228
+ # why this is opt-in. One NAT Gateway, one public subnet, wired
229
+ # only when google_oauth_present -- nothing else in this stack
230
+ # pays for it.
231
+ #{db_id}PublicSubnet:
232
+ Type: AWS::EC2::Subnet
233
+ Properties:
234
+ VpcId: !Ref #{db_id}Vpc
235
+ CidrBlock: 10.0.3.0/24
236
+ AvailabilityZone: !Select [0, !GetAZs '']
237
+ MapPublicIpOnLaunch: true
238
+
239
+ #{db_id}InternetGateway:
240
+ Type: AWS::EC2::InternetGateway
241
+
242
+ #{db_id}InternetGatewayAttachment:
243
+ Type: AWS::EC2::VPCGatewayAttachment
244
+ Properties:
245
+ VpcId: !Ref #{db_id}Vpc
246
+ InternetGatewayId: !Ref #{db_id}InternetGateway
247
+
248
+ #{db_id}PublicRouteTable:
249
+ Type: AWS::EC2::RouteTable
250
+ Properties:
251
+ VpcId: !Ref #{db_id}Vpc
252
+
253
+ #{db_id}PublicRoute:
254
+ Type: AWS::EC2::Route
255
+ DependsOn: #{db_id}InternetGatewayAttachment
256
+ Properties:
257
+ RouteTableId: !Ref #{db_id}PublicRouteTable
258
+ DestinationCidrBlock: 0.0.0.0/0
259
+ GatewayId: !Ref #{db_id}InternetGateway
260
+
261
+ #{db_id}PublicSubnetRouteTableAssociation:
262
+ Type: AWS::EC2::SubnetRouteTableAssociation
263
+ Properties:
264
+ SubnetId: !Ref #{db_id}PublicSubnet
265
+ RouteTableId: !Ref #{db_id}PublicRouteTable
266
+
267
+ # A second public subnet, AZ index 1 -- not where NatGateway
268
+ # lives (that's still #{db_id}PublicSubnet, AZ index 0). Only
269
+ # for bastion.yaml's own BastionInstance, a real EC2 instance
270
+ # that a real, live CREATE_FAILED proved this account can't
271
+ # launch in AZ index 0 at all. Same shared
272
+ # #{db_id}PublicRouteTable/#{db_id}InternetGateway as
273
+ # #{db_id}PublicSubnet, not a second IGW -- a VPC accepts only one.
274
+ #{db_id}BastionPublicSubnet:
275
+ Type: AWS::EC2::Subnet
276
+ Properties:
277
+ VpcId: !Ref #{db_id}Vpc
278
+ CidrBlock: 10.0.4.0/24
279
+ AvailabilityZone: !Select [1, !GetAZs '']
280
+ MapPublicIpOnLaunch: true
281
+
282
+ #{db_id}BastionPublicSubnetRouteTableAssociation:
283
+ Type: AWS::EC2::SubnetRouteTableAssociation
284
+ Properties:
285
+ SubnetId: !Ref #{db_id}BastionPublicSubnet
286
+ RouteTableId: !Ref #{db_id}PublicRouteTable
287
+
288
+ #{db_id}NatEip:
289
+ Type: AWS::EC2::EIP
290
+ Properties:
291
+ Domain: vpc
292
+
293
+ #{db_id}NatGateway:
294
+ Type: AWS::EC2::NatGateway
295
+ Properties:
296
+ SubnetId: !Ref #{db_id}PublicSubnet
297
+ AllocationId: !GetAtt #{db_id}NatEip.AllocationId
298
+
299
+ # #{db_id}SubnetA/B (private -- the compute and RDS live here)
300
+ # have no route to the internet at all without this: routes
301
+ # outbound through #{db_id}NatGateway instead of an Internet
302
+ # Gateway directly -- the whole reason a NAT (not just an IGW)
303
+ # exists: outbound-only, no inbound path back into these subnets.
304
+ #{db_id}PrivateRouteTable:
305
+ Type: AWS::EC2::RouteTable
306
+ Properties:
307
+ VpcId: !Ref #{db_id}Vpc
308
+
309
+ #{db_id}PrivateRoute:
310
+ Type: AWS::EC2::Route
311
+ Properties:
312
+ RouteTableId: !Ref #{db_id}PrivateRouteTable
313
+ DestinationCidrBlock: 0.0.0.0/0
314
+ NatGatewayId: !Ref #{db_id}NatGateway
315
+
316
+ #{db_id}SubnetARouteTableAssociation:
317
+ Type: AWS::EC2::SubnetRouteTableAssociation
318
+ Properties:
319
+ SubnetId: !Ref #{db_id}SubnetA
320
+ RouteTableId: !Ref #{db_id}PrivateRouteTable
321
+
322
+ #{db_id}SubnetBRouteTableAssociation:
323
+ Type: AWS::EC2::SubnetRouteTableAssociation
324
+ Properties:
325
+ SubnetId: !Ref #{db_id}SubnetB
326
+ RouteTableId: !Ref #{db_id}PrivateRouteTable
327
+
328
+ NATGW
329
+ #{db_id}SubnetGroup:
330
+ Type: AWS::RDS::DBSubnetGroup
331
+ Properties:
332
+ DBSubnetGroupDescription: !Sub "${AWS::StackName} - private subnets for #{infra_name}'s own RDS instance"
333
+ SubnetIds: [!Ref #{db_id}SubnetA, !Ref #{db_id}SubnetB]
334
+
335
+ # No inline SecurityGroupEgress/Ingress on either group -- each
336
+ # would need to reference the other's id from inside its own
337
+ # Properties, a genuine circular create-order dependency
338
+ # CloudFormation refuses outright (cfn-lint E3004). Standard
339
+ # fix: create both groups empty, then wire the cross-reference
340
+ # through two standalone rule resources below, each of which
341
+ # can legitimately depend on both groups already existing.
342
+ #{compute_logical_id}SecurityGroup:
343
+ Type: AWS::EC2::SecurityGroup
344
+ Properties:
345
+ VpcId: !Ref #{db_id}Vpc
346
+ GroupDescription: #{compute_description}
347
+
348
+ #{db_id}SecurityGroup:
349
+ Type: AWS::EC2::SecurityGroup
350
+ Properties:
351
+ VpcId: !Ref #{db_id}Vpc
352
+ GroupDescription: #{db_id} - ingress rule attached separately below, from #{compute_logical_id} only
353
+
354
+ #{compute_logical_id}EgressToDb:
355
+ Type: AWS::EC2::SecurityGroupEgress
356
+ Properties:
357
+ GroupId: !Ref #{compute_logical_id}SecurityGroup
358
+ IpProtocol: tcp
359
+ FromPort: 5432
360
+ ToPort: 5432
361
+ DestinationSecurityGroupId: !Ref #{db_id}SecurityGroup
362
+
363
+ #{google_oauth_present ? <<~OAUTHEGRESS.rstrip : ""}
364
+ # A standalone AWS::EC2::SecurityGroupEgress resource for a
365
+ # group replaces that group's own default allow-all-outbound
366
+ # rule with only what's explicitly declared here --
367
+ # #{compute_logical_id}EgressToDb's own existence already means
368
+ # #{compute_logical_id}SecurityGroup has no other egress at
369
+ # all. 443 to 0.0.0.0/0, not a narrower destination -- Google
370
+ # publishes no fixed IP range for oauth2.googleapis.com an AWS
371
+ # security group could reference instead.
372
+ #{compute_logical_id}EgressToInternet:
373
+ Type: AWS::EC2::SecurityGroupEgress
374
+ Properties:
375
+ GroupId: !Ref #{compute_logical_id}SecurityGroup
376
+ IpProtocol: tcp
377
+ FromPort: 443
378
+ ToPort: 443
379
+ CidrIp: 0.0.0.0/0
380
+
381
+ OAUTHEGRESS
382
+ #{db_id}IngressFromFunction:
383
+ Type: AWS::EC2::SecurityGroupIngress
384
+ Properties:
385
+ GroupId: !Ref #{db_id}SecurityGroup
386
+ IpProtocol: tcp
387
+ FromPort: 5432
388
+ ToPort: 5432
389
+ SourceSecurityGroupId: !Ref #{compute_logical_id}SecurityGroup
390
+
391
+ #{if aurora
392
+ <<~AURORADB.rstrip
393
+ # Aurora Serverless v2, Postgres-compatible -- the split
394
+ # shape Aurora needs (a DBCluster carrying the managed
395
+ # password/endpoint/subnet-group/security-group, plus at
396
+ # least one DBInstance inside it) is the only thing
397
+ # different from the plain-RDS path below;
398
+ # Adapters::PostgresEra/tokio_postgres speak the identical
399
+ # wire protocol against either endpoint.
400
+ # Self-managed, not ManageMasterUserPassword: true --
401
+ # CloudFormation's `{{resolve:secretsmanager:...}}` only
402
+ # resolves an auto-rotating managed secret fresh at the
403
+ # compute resource's own creation, and RDS's managed-
404
+ # password feature rotates the password itself shortly
405
+ # after standing up the cluster -- every subsequent
406
+ # invocation would keep authenticating with the now-stale
407
+ # original value. A self-managed secret (generated once,
408
+ # here, never auto-rotated) has nothing to go stale.
409
+ #{db_id}Secret:
410
+ Type: AWS::SecretsManager::Secret
411
+ Properties:
412
+ GenerateSecretString:
413
+ SecretStringTemplate: '{"username":"postgres"}'
414
+ GenerateStringKey: password
415
+ PasswordLength: 32
416
+ ExcludeCharacters: '"@/'
417
+
418
+ #{db_id}Cluster:
419
+ Type: AWS::RDS::DBCluster
420
+ DeletionPolicy: Snapshot
421
+ UpdateReplacePolicy: Snapshot
422
+ DependsOn: #{db_id}Secret
423
+ Properties:
424
+ Engine: aurora-postgresql
425
+ EngineVersion: "16.14"
426
+ DatabaseName: #{db_name}
427
+ MasterUsername: postgres
428
+ MasterUserPassword: !Sub "{{resolve:secretsmanager:${#{db_id}Secret}:SecretString:password}}"
429
+ StorageEncrypted: true
430
+ DBSubnetGroupName: !Ref #{db_id}SubnetGroup
431
+ VpcSecurityGroupIds: [!Ref #{db_id}SecurityGroup]
432
+ ServerlessV2ScalingConfiguration:
433
+ MinCapacity: 0.5
434
+ MaxCapacity: 1
435
+
436
+ #{db_id}Instance:
437
+ Type: AWS::RDS::DBInstance
438
+ DeletionPolicy: Snapshot
439
+ UpdateReplacePolicy: Snapshot
440
+ Properties:
441
+ Engine: aurora-postgresql
442
+ DBInstanceClass: db.serverless
443
+ DBClusterIdentifier: !Ref #{db_id}Cluster
444
+ PubliclyAccessible: false
445
+ AURORADB
446
+ else
447
+ <<~PLAINDB.rstrip
448
+ #{db_id}:
449
+ Type: AWS::RDS::DBInstance
450
+ DeletionPolicy: Snapshot
451
+ UpdateReplacePolicy: Snapshot
452
+ Properties:
453
+ Engine: postgres
454
+ EngineVersion: "16.14"
455
+ DBInstanceClass: db.t4g.micro
456
+ AllocatedStorage: "20"
457
+ StorageEncrypted: true
458
+ DBName: #{db_name}
459
+ MasterUsername: postgres
460
+ ManageMasterUserPassword: true
461
+ PubliclyAccessible: false
462
+ DBSubnetGroupName: !Ref #{db_id}SubnetGroup
463
+ VPCSecurityGroups: [!Ref #{db_id}SecurityGroup]
464
+ PLAINDB
465
+ end}
466
+ OWNDB
467
+ end
468
+
469
+ # Renders the temporary era-minting bastion — a standalone
470
+ # CloudFormation template, deployed and destroyed by `make
471
+ # mint-era`, never merged into the main stack. SSM Session Manager
472
+ # only, never SSH: no key pair, no inbound security group rule at
473
+ # all — the only way in is `aws ssm start-session`, itself gated by
474
+ # the caller's own IAM permissions.
475
+ #
476
+ # Callers skip this entirely when `shared` — a Shared-mode domain
477
+ # provisions no RDS/VPC of its own for a bastion to reach, and era-
478
+ # minting for it reuses its owner's own already-standing bastion
479
+ # path instead.
480
+ #
481
+ # @param domain [String] the domain directory's path, named in the header comment
482
+ # @param infra_name [String] the domain's own AWS-facing name
483
+ # @param stack_name [String] the main stack's own name, tagged onto the bastion instance
484
+ # @param db_id [String] the RDS/VPC resources' own shared logical-id prefix, as
485
+ # `vpc_and_database_yaml` names them
486
+ # @param google_oauth_present [Boolean] whether the main stack already has its own
487
+ # public subnet/Internet Gateway to reuse, rather than minting a second one
488
+ # @param bastion_parameters [Array<Hash{Symbol => String}>] as `bastion_parameters` builds
489
+ # @return [String] the rendered CloudFormation template
490
+ def bastion_yaml(domain:, infra_name:, stack_name:, db_id:, google_oauth_present:, bastion_parameters:)
491
+ <<~YAML
492
+ # GENERATED by bin/project_deploy #{domain} — re-run it to refresh
493
+ # this file rather than hand-editing. Deployed and destroyed by
494
+ # `make mint-era` (this directory's own Makefile) — never left
495
+ # running: it exists only long enough for one Ruby boot to reach
496
+ # #{infra_name}'s private RDS instance and mint era 1.
497
+ AWSTemplateFormatVersion: '2010-09-09'
498
+ Description: >
499
+ TEMPORARY — #{infra_name}'s era-minting bastion. Deployed and torn
500
+ down by `make mint-era`; not meant to run continuously.
501
+
502
+ # bastion_parameters (Shared.bastion_parameters) — the other end of
503
+ # the contract the main template's own Outputs comment describes;
504
+ # the Makefile's --parameter-overrides line fills these from the
505
+ # same table.
506
+ Parameters:
507
+ #{bastion_parameters.map { |p| "#{p[:name]}:\n Type: #{p[:type]}" }.join("\n ")}
508
+
509
+ Resources:
510
+ #{google_oauth_present ? <<~REUSEPUBLIC.each_line.with_index.map { |l, i| (i.zero? ? "" : " ") + l }.join.rstrip : <<~OWNPUBLIC.each_line.with_index.map { |l, i| (i.zero? ? "" : " ") + l }.join.rstrip}
511
+ # BastionSubnetId, not a subnet minted here — the main
512
+ # template's own #{db_id}BastionPublicSubnet (and the
513
+ # Internet Gateway it shares with #{db_id}PublicSubnet)
514
+ # already exist once google_oauth_present is true, and a VPC
515
+ # accepts only one attached Internet Gateway: this stack's
516
+ # own BastionInternetGateway (below, the no-oauth branch)
517
+ # would collide with it. Its own subnet, not PublicSubnetId
518
+ # directly, despite sharing that subnet's route table --
519
+ # PublicSubnetId's AZ can't launch EC2 instances at all on
520
+ # this account.
521
+ REUSEPUBLIC
522
+ # A new public subnet, not the RDS's own private ones --
523
+ # those have no internet route at all, and the SSM agent
524
+ # needs to reach AWS's own SSM endpoints over HTTPS.
525
+ # 10.0.99.0/24 is picked to sit outside the main template's
526
+ # own 10.0.1.0/24 and 10.0.2.0/24 ranges inside the same /16 VPC.
527
+ BastionSubnet:
528
+ Type: AWS::EC2::Subnet
529
+ Properties:
530
+ VpcId: !Ref VpcId
531
+ CidrBlock: 10.0.99.0/24
532
+ AvailabilityZone: !Select [1, !GetAZs '']
533
+ MapPublicIpOnLaunch: true
534
+
535
+ BastionInternetGateway:
536
+ Type: AWS::EC2::InternetGateway
537
+
538
+ BastionVpcGatewayAttachment:
539
+ Type: AWS::EC2::VPCGatewayAttachment
540
+ Properties:
541
+ VpcId: !Ref VpcId
542
+ InternetGatewayId: !Ref BastionInternetGateway
543
+
544
+ BastionRouteTable:
545
+ Type: AWS::EC2::RouteTable
546
+ Properties:
547
+ VpcId: !Ref VpcId
548
+
549
+ BastionInternetRoute:
550
+ Type: AWS::EC2::Route
551
+ DependsOn: BastionVpcGatewayAttachment
552
+ Properties:
553
+ RouteTableId: !Ref BastionRouteTable
554
+ DestinationCidrBlock: 0.0.0.0/0
555
+ GatewayId: !Ref BastionInternetGateway
556
+
557
+ BastionSubnetRouteTableAssociation:
558
+ Type: AWS::EC2::SubnetRouteTableAssociation
559
+ Properties:
560
+ SubnetId: !Ref BastionSubnet
561
+ RouteTableId: !Ref BastionRouteTable
562
+
563
+ OWNPUBLIC
564
+ # Zero Ingress rules — SSM Session Manager is outbound-only from
565
+ # the instance's side (it dials out to AWS's SSM service), so
566
+ # nothing needs to reach this instance over the network at all,
567
+ # despite it sitting in a public subnet with a public IP.
568
+ BastionSecurityGroup:
569
+ Type: AWS::EC2::SecurityGroup
570
+ Properties:
571
+ VpcId: !Ref VpcId
572
+ # A plain hyphen, not an em-dash - EC2's GroupDescription
573
+ # field rejects any non-ASCII character outright.
574
+ GroupDescription: "TEMPORARY era-minting bastion - SSM only, no inbound rules"
575
+
576
+ BastionToRds:
577
+ Type: AWS::EC2::SecurityGroupIngress
578
+ Properties:
579
+ GroupId: !Ref RdsSecurityGroupId
580
+ IpProtocol: tcp
581
+ FromPort: 5432
582
+ ToPort: 5432
583
+ SourceSecurityGroupId: !Ref BastionSecurityGroup
584
+
585
+ BastionRole:
586
+ Type: AWS::IAM::Role
587
+ Properties:
588
+ AssumeRolePolicyDocument:
589
+ Version: '2012-10-17'
590
+ Statement:
591
+ - Effect: Allow
592
+ Principal: { Service: ec2.amazonaws.com }
593
+ Action: sts:AssumeRole
594
+ ManagedPolicyArns:
595
+ - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
596
+
597
+ BastionInstanceProfile:
598
+ Type: AWS::IAM::InstanceProfile
599
+ Properties:
600
+ Roles: [!Ref BastionRole]
601
+
602
+ BastionInstance:
603
+ Type: AWS::EC2::Instance
604
+ Properties:
605
+ # Amazon Linux 2023's own SSM parameter — always the current
606
+ # AL2023 AMI for whatever region this deploys into, never a
607
+ # hand-copied, region-specific, staleness-prone AMI id.
608
+ ImageId: "{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}"
609
+ InstanceType: t3.micro
610
+ SubnetId: !Ref #{google_oauth_present ? "BastionSubnetId" : "BastionSubnet"}
611
+ SecurityGroupIds: [!Ref BastionSecurityGroup]
612
+ IamInstanceProfile: !Ref BastionInstanceProfile
613
+ Tags:
614
+ - { Key: Name, Value: #{stack_name}-bastion }
615
+
616
+ Outputs:
617
+ InstanceId:
618
+ Value: !Ref BastionInstance
619
+ YAML
620
+ end
621
+ end
622
+ end
623
+ end
624
+ end