hecks 3.0.5 → 3.1.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.
- checksums.yaml +4 -4
- data/exe/hecks +24 -12
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
- data/lib/hecks/bluebook/hexagon.rb +45 -25
- data/lib/hecks/bluebook/model_check.rb +7 -7
- data/lib/hecks/chapters.rb +32 -3
- data/lib/hecks/cli/check_era.rb +1 -1
- data/lib/hecks/cli/deploy_template_diff.rb +1 -1
- data/lib/hecks/cli/doc_coverage.rb +1 -1
- data/lib/hecks/cli/domain_stub.rb +128 -0
- data/lib/hecks/cli/domain_writer.rb +39 -0
- data/lib/hecks/cli/interview_agent.rb +59 -0
- data/lib/hecks/cli/interview_draft.rb +246 -0
- data/lib/hecks/cli/interview_run.rb +87 -0
- data/lib/hecks/cli/interview_session.rb +299 -0
- data/lib/hecks/cli/model_check.rb +1 -1
- data/lib/hecks/cli/project_cli.rb +51 -29
- data/lib/hecks/cli/reference.rb +2 -1
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
- data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
- data/lib/hecks/cli/spec_example.rb +1 -1
- data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
- data/lib/hecks/corpus.rb +12 -8
- data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +4 -3
- data/lib/hecks/doors/cli_door.rb +13 -13
- data/lib/hecks/doors/cli_runner.rb +53 -33
- data/lib/hecks/doors/launcher_options.rb +5 -5
- data/lib/hecks/doors/mcp_door.rb +40 -18
- data/lib/hecks/doors/mcp_door_scope.rb +244 -24
- data/lib/hecks/doors/usage_cache.rb +123 -0
- data/lib/hecks/doors.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -2
- data/lib/hecks/forms/banking_presentation.rb +1 -1
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
- data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
- data/lib/hecks/framework.rb +1 -1
- data/lib/hecks/fuzzing/combination_miner.rb +1 -1
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
- data/lib/hecks/fuzzing/differential.rb +4 -2
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/form_census.rb +2 -2
- data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
- data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
- data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
- data/lib/hecks/gate/stages.yml +5 -5
- data/lib/hecks/grammar/grammar.hecksagon +2 -2
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
- data/lib/hecks/hecks/adapters/finding_github.adapter +7 -0
- data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
- data/lib/hecks/hecks/adapters/host_http.rb +4 -3
- data/lib/hecks/hecks/adapters/local_files.rb +59 -0
- data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
- data/lib/hecks/hecks/adapters/terminal.rb +15 -0
- data/lib/hecks/hecks/codebase.bluebook +4 -3
- data/lib/hecks/hecks/context_map.hecksagon +2 -2
- data/lib/hecks/hecks/custodian.bluebook +81 -3
- data/lib/hecks/hecks/hecks.bluebook +18 -0
- data/lib/hecks/hecks/hecks.hecksagon +16 -4
- data/lib/hecks/hecks/hecks.world +7 -6
- data/lib/hecks/hecks/site.behaviors +17 -0
- data/lib/hecks/hecks/site.world +10 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
- data/lib/hecks/projections/deploy/box/settings.rb +324 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +268 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +68 -0
- data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
- data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +181 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
- data/lib/hecks/projections/deploy/box.rb +553 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +11 -10
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
- data/lib/hecks/projections/diagrams.rb +2 -2
- data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
- data/lib/hecks/projections/site/edge/checks.rb +137 -0
- data/lib/hecks/projections/site/edge/pattern.rb +50 -0
- data/lib/hecks/projections/site/edge/rules.rb +128 -0
- data/lib/hecks/projections/site/edge.rb +136 -0
- data/lib/hecks/projections/site/regions.rb +44 -0
- data/lib/hecks/projections/site/routes_ts.rb +284 -0
- data/lib/hecks/projections/site/site_cdn.rb +78 -0
- data/lib/hecks/projections/site/table/checks.rb +124 -0
- data/lib/hecks/projections/site/table/row_builder.rb +155 -0
- data/lib/hecks/projections/site/table.rb +98 -0
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/cli_projector.rb +153 -111
- data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
- data/lib/hecks/quality_control/adapters/agent.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
- data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
- data/lib/hecks/quality_control/quality_control.bluebook +18 -18
- data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
- data/lib/hecks/runtime/loader.rb +31 -8
- data/lib/hecks/runtime/registry/verification.rb +15 -29
- data/lib/hecks/runtime/registry.rb +14 -20
- data/lib/hecks/rust_build/append_optionals.rb +79 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
- data/lib/hecks/rust_build/domain_name.rb +26 -0
- data/lib/hecks/rust_build/kernel_input.rb +58 -0
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
- data/lib/hecks/rust_build/project_rust.rb +57 -24
- data/lib/hecks/rust_build/rust_literal.rb +33 -0
- data/lib/hecks/rust_build/write_if_changed.rb +86 -0
- data/lib/hecks/site/bluebook/site.bluebook +205 -0
- data/lib/hecks/site/bluebook/site.hecksagon +5 -0
- data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
- data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
- data/lib/hecks/storehouse.rb +37 -7
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
- data/lib/hecks/three_zero/forms.yml +88 -88
- data/lib/hecks/tickets/bluebook/tickets.bluebook +331 -0
- data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
- data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
- data/lib/hecks/tools/ci_gates.rb +1 -1
- data/lib/hecks/tools/deploy_recipe.rb +14 -6
- data/lib/hecks/tools/oidc_manifests.rb +2 -1
- data/lib/hecks/tools/sequence_script.rb +2 -2
- data/lib/hecks/tools/site_routes.rb +152 -0
- data/lib/hecks/tools/tenant_provisioning.rb +1 -1
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +1 -0
- data/rust/build/src/cargo_sync.rs +1 -1
- data/rust/build/src/pipeline.rs +23 -16
- data/rust/build/src/resolve.rs +39 -6
- data/rust/codegen/src/json_codec.rs +3 -0
- data/rust/codegen/src/naming.rs +4 -0
- data/rust/codegen/src/types.rs +45 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +87 -7
- data/rust/host/src/needs.rs +111 -0
- data/rust/parser/src/keywords.rs +7 -6
- data/rust/parser/src/main.rs +4 -3
- data/rust/parser/src/parse/chapter.rs +1 -1
- data/rust/parser/src/parse/hecksagon.rs +25 -10
- data/rust/project/mutations.rb +4 -30
- data/rust/project/naming.rb +8 -28
- data/rust/project/write_if_changed.rb +4 -80
- data/rust/project_rust_pipeline.rb +16 -12
- data/rust/src/kernel/cli.rs +2 -0
- data/rust/src/kernel/needs.rs +74 -1
- metadata +51 -2
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: "2010-09-09"
|
|
2
|
+
Description: >
|
|
3
|
+
One EC2 box that runs @@STACK@@'s containers with Docker Compose behind Caddy, reading the RDS
|
|
4
|
+
instance from rds.yaml. No inbound rules but an optional CDN prefix list on port 80; use SSM Session
|
|
5
|
+
Manager to reach it. Secrets are read from Secrets Manager on the box at deploy time.
|
|
6
|
+
|
|
7
|
+
Parameters:
|
|
8
|
+
VpcId:
|
|
9
|
+
Type: AWS::EC2::VPC::Id
|
|
10
|
+
SubnetId:
|
|
11
|
+
Type: AWS::EC2::Subnet::Id
|
|
12
|
+
Description: A public subnet. The box needs a public IP to reach ECR and Secrets Manager.
|
|
13
|
+
DbSecurityGroupId:
|
|
14
|
+
Type: AWS::EC2::SecurityGroup::Id
|
|
15
|
+
Description: DbSecurityGroupId output of the RDS stack.
|
|
16
|
+
DbSecretArn:
|
|
17
|
+
Type: String
|
|
18
|
+
Description: DbSecretArn output of the RDS stack.
|
|
19
|
+
InstanceType:
|
|
20
|
+
Type: String
|
|
21
|
+
Default: @@INSTANCE_TYPE@@
|
|
22
|
+
AmiId:
|
|
23
|
+
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
|
|
24
|
+
Default: @@AMI_PARAMETER@@
|
|
25
|
+
Description: Never re-resolve this on a live box, because a newer image replaces the instance.
|
|
26
|
+
RootVolumeGb:
|
|
27
|
+
Type: Number
|
|
28
|
+
Default: @@VOLUME_GB@@
|
|
29
|
+
AlertTopicArn:
|
|
30
|
+
Type: String
|
|
31
|
+
Default: ""
|
|
32
|
+
Description: AlertTopicArn output of the RDS stack. Blank creates the alarms with no notification target.
|
|
33
|
+
CdnPrefixListId:
|
|
34
|
+
Type: String
|
|
35
|
+
Default: ""
|
|
36
|
+
Description: >
|
|
37
|
+
A managed prefix list of the CDN's origin-facing addresses. When set, port 80 is open to it (the
|
|
38
|
+
proxy still refuses requests without the origin secret). Blank leaves the box with no inbound at all.
|
|
39
|
+
Rehearsal:
|
|
40
|
+
Type: String
|
|
41
|
+
Default: "false"
|
|
42
|
+
AllowedValues: ["true", "false"]
|
|
43
|
+
Description: true is a throwaway box with no Elastic IP.
|
|
44
|
+
|
|
45
|
+
Conditions:
|
|
46
|
+
HasPrefixList: !Not [!Equals [!Ref CdnPrefixListId, ""]]
|
|
47
|
+
HasAlerts: !Not [!Equals [!Ref AlertTopicArn, ""]]
|
|
48
|
+
IsProduction: !Equals [!Ref Rehearsal, "false"]
|
|
49
|
+
|
|
50
|
+
Resources:
|
|
51
|
+
BoxSecurityGroup:
|
|
52
|
+
Type: AWS::EC2::SecurityGroup
|
|
53
|
+
Properties:
|
|
54
|
+
GroupDescription: @@STACK@@ app box, port 80 from the CDN only, egress to RDS and 443
|
|
55
|
+
VpcId: !Ref VpcId
|
|
56
|
+
SecurityGroupIngress:
|
|
57
|
+
- !If
|
|
58
|
+
- HasPrefixList
|
|
59
|
+
- IpProtocol: tcp
|
|
60
|
+
FromPort: 80
|
|
61
|
+
ToPort: 80
|
|
62
|
+
SourcePrefixListId: !Ref CdnPrefixListId
|
|
63
|
+
Description: CDN origin-facing addresses
|
|
64
|
+
- !Ref AWS::NoValue
|
|
65
|
+
SecurityGroupEgress:
|
|
66
|
+
- IpProtocol: tcp
|
|
67
|
+
FromPort: 5432
|
|
68
|
+
ToPort: 5432
|
|
69
|
+
DestinationSecurityGroupId: !Ref DbSecurityGroupId
|
|
70
|
+
- IpProtocol: tcp
|
|
71
|
+
FromPort: 443
|
|
72
|
+
ToPort: 443
|
|
73
|
+
CidrIp: 0.0.0.0/0
|
|
74
|
+
@@TUNNEL_EGRESS@@
|
|
75
|
+
BoxToDbIngress:
|
|
76
|
+
Type: AWS::EC2::SecurityGroupIngress
|
|
77
|
+
Properties:
|
|
78
|
+
GroupId: !Ref DbSecurityGroupId
|
|
79
|
+
IpProtocol: tcp
|
|
80
|
+
FromPort: 5432
|
|
81
|
+
ToPort: 5432
|
|
82
|
+
SourceSecurityGroupId: !Ref BoxSecurityGroup
|
|
83
|
+
Description: App box
|
|
84
|
+
|
|
85
|
+
BoxRole:
|
|
86
|
+
Type: AWS::IAM::Role
|
|
87
|
+
Properties:
|
|
88
|
+
AssumeRolePolicyDocument:
|
|
89
|
+
Version: "2012-10-17"
|
|
90
|
+
Statement:
|
|
91
|
+
- Effect: Allow
|
|
92
|
+
Principal: { Service: ec2.amazonaws.com }
|
|
93
|
+
Action: sts:AssumeRole
|
|
94
|
+
ManagedPolicyArns:
|
|
95
|
+
- !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore
|
|
96
|
+
- !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
|
|
97
|
+
Policies:
|
|
98
|
+
- PolicyName: read-secrets
|
|
99
|
+
PolicyDocument:
|
|
100
|
+
Version: "2012-10-17"
|
|
101
|
+
Statement:
|
|
102
|
+
- Effect: Allow
|
|
103
|
+
Action: secretsmanager:GetSecretValue
|
|
104
|
+
Resource:
|
|
105
|
+
- !Ref DbSecretArn
|
|
106
|
+
@@SECRET_RESOURCES@@
|
|
107
|
+
@@S3_POLICY@@
|
|
108
|
+
|
|
109
|
+
BoxProfile:
|
|
110
|
+
Type: AWS::IAM::InstanceProfile
|
|
111
|
+
Properties:
|
|
112
|
+
Roles: [!Ref BoxRole]
|
|
113
|
+
|
|
114
|
+
Box:
|
|
115
|
+
Type: AWS::EC2::Instance
|
|
116
|
+
Properties:
|
|
117
|
+
ImageId: !Ref AmiId
|
|
118
|
+
InstanceType: !Ref InstanceType
|
|
119
|
+
SubnetId: !Ref SubnetId
|
|
120
|
+
SecurityGroupIds: [!Ref BoxSecurityGroup]
|
|
121
|
+
IamInstanceProfile: !Ref BoxProfile
|
|
122
|
+
BlockDeviceMappings:
|
|
123
|
+
- DeviceName: /dev/xvda
|
|
124
|
+
Ebs:
|
|
125
|
+
VolumeType: gp3
|
|
126
|
+
VolumeSize: !Ref RootVolumeGb
|
|
127
|
+
Encrypted: true
|
|
128
|
+
DeleteOnTermination: true
|
|
129
|
+
MetadataOptions:
|
|
130
|
+
HttpTokens: required
|
|
131
|
+
HttpPutResponseHopLimit: 2
|
|
132
|
+
Tags:
|
|
133
|
+
- Key: Name
|
|
134
|
+
Value: @@STACK@@-box
|
|
135
|
+
- Key: Backup
|
|
136
|
+
Value: @@STACK@@-box
|
|
137
|
+
UserData:
|
|
138
|
+
Fn::Base64: |
|
|
139
|
+
#!/bin/bash
|
|
140
|
+
set -eux
|
|
141
|
+
dnf install -y docker jq
|
|
142
|
+
systemctl enable --now docker
|
|
143
|
+
mkdir -p /usr/local/lib/docker/cli-plugins
|
|
144
|
+
curl -fsSL https://github.com/docker/compose/releases/download/v2.29.7/docker-compose-linux-$(uname -m) \
|
|
145
|
+
-o /usr/local/lib/docker/cli-plugins/docker-compose
|
|
146
|
+
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
|
147
|
+
@@SWAP_COMMANDS@@ mkdir -p /opt/@@STACK@@
|
|
148
|
+
|
|
149
|
+
# Production only. A CDN needs an origin DNS name that survives a stop and start; an Elastic IP
|
|
150
|
+
# keeps the instance's public DNS name stable.
|
|
151
|
+
BoxAddress:
|
|
152
|
+
Type: AWS::EC2::EIP
|
|
153
|
+
Condition: IsProduction
|
|
154
|
+
Properties:
|
|
155
|
+
Domain: vpc
|
|
156
|
+
Tags:
|
|
157
|
+
- Key: Name
|
|
158
|
+
Value: @@STACK@@-box
|
|
159
|
+
|
|
160
|
+
BoxAddressAssociation:
|
|
161
|
+
Type: AWS::EC2::EIPAssociation
|
|
162
|
+
Condition: IsProduction
|
|
163
|
+
Properties:
|
|
164
|
+
AllocationId: !GetAtt BoxAddress.AllocationId
|
|
165
|
+
InstanceId: !Ref Box
|
|
166
|
+
|
|
167
|
+
# Daily snapshot of every volume on the instance. The database has its own RDS backups; this makes
|
|
168
|
+
# rebuilding the box fast.
|
|
169
|
+
SnapshotRole:
|
|
170
|
+
Type: AWS::IAM::Role
|
|
171
|
+
Properties:
|
|
172
|
+
AssumeRolePolicyDocument:
|
|
173
|
+
Version: "2012-10-17"
|
|
174
|
+
Statement:
|
|
175
|
+
- Effect: Allow
|
|
176
|
+
Principal: { Service: dlm.amazonaws.com }
|
|
177
|
+
Action: sts:AssumeRole
|
|
178
|
+
ManagedPolicyArns:
|
|
179
|
+
- !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSDataLifecycleManagerServiceRole
|
|
180
|
+
|
|
181
|
+
SnapshotPolicy:
|
|
182
|
+
Type: AWS::DLM::LifecyclePolicy
|
|
183
|
+
Properties:
|
|
184
|
+
# DLM allows only letters, digits, spaces, underscores and hyphens here.
|
|
185
|
+
Description: Daily snapshots of the @@STACK@@ app box - keep @@SNAPSHOTS_KEEP@@
|
|
186
|
+
State: ENABLED
|
|
187
|
+
ExecutionRoleArn: !GetAtt SnapshotRole.Arn
|
|
188
|
+
PolicyDetails:
|
|
189
|
+
ResourceTypes: [INSTANCE]
|
|
190
|
+
TargetTags:
|
|
191
|
+
- Key: Backup
|
|
192
|
+
Value: @@STACK@@-box
|
|
193
|
+
Schedules:
|
|
194
|
+
- Name: daily
|
|
195
|
+
CopyTags: true
|
|
196
|
+
CreateRule:
|
|
197
|
+
Interval: 24
|
|
198
|
+
IntervalUnit: HOURS
|
|
199
|
+
Times: ["07:00"]
|
|
200
|
+
RetainRule:
|
|
201
|
+
Count: @@SNAPSHOTS_KEEP@@
|
|
202
|
+
|
|
203
|
+
# A hardware fault on the host: EC2 moves the instance to healthy hardware (same disk, same
|
|
204
|
+
# address) and the topic is told.
|
|
205
|
+
SystemStatusAlarm:
|
|
206
|
+
Type: AWS::CloudWatch::Alarm
|
|
207
|
+
Properties:
|
|
208
|
+
AlarmDescription: EC2 system status check failing, recovering the instance
|
|
209
|
+
Namespace: AWS/EC2
|
|
210
|
+
MetricName: StatusCheckFailed_System
|
|
211
|
+
Dimensions: [{ Name: InstanceId, Value: !Ref Box }]
|
|
212
|
+
Statistic: Maximum
|
|
213
|
+
Period: 60
|
|
214
|
+
EvaluationPeriods: 2
|
|
215
|
+
Threshold: 0
|
|
216
|
+
ComparisonOperator: GreaterThanThreshold
|
|
217
|
+
TreatMissingData: notBreaching
|
|
218
|
+
AlarmActions: !If
|
|
219
|
+
- HasAlerts
|
|
220
|
+
- [!Ref AlertTopicArn, !Sub "arn:${AWS::Partition}:automate:${AWS::Region}:ec2:recover"]
|
|
221
|
+
- [!Sub "arn:${AWS::Partition}:automate:${AWS::Region}:ec2:recover"]
|
|
222
|
+
|
|
223
|
+
# The OS or networking inside the instance is unhealthy. Not auto-fixed: a reboot can lose state
|
|
224
|
+
# worth looking at.
|
|
225
|
+
InstanceStatusAlarm:
|
|
226
|
+
Type: AWS::CloudWatch::Alarm
|
|
227
|
+
Condition: HasAlerts
|
|
228
|
+
Properties:
|
|
229
|
+
AlarmDescription: EC2 instance status check failing for 5 minutes
|
|
230
|
+
Namespace: AWS/EC2
|
|
231
|
+
MetricName: StatusCheckFailed_Instance
|
|
232
|
+
Dimensions: [{ Name: InstanceId, Value: !Ref Box }]
|
|
233
|
+
Statistic: Maximum
|
|
234
|
+
Period: 60
|
|
235
|
+
EvaluationPeriods: 5
|
|
236
|
+
Threshold: 0
|
|
237
|
+
ComparisonOperator: GreaterThanThreshold
|
|
238
|
+
TreatMissingData: notBreaching
|
|
239
|
+
AlarmActions: [!Ref AlertTopicArn]
|
|
240
|
+
OKActions: [!Ref AlertTopicArn]
|
|
241
|
+
|
|
242
|
+
CpuHighAlarm:
|
|
243
|
+
Type: AWS::CloudWatch::Alarm
|
|
244
|
+
Condition: HasAlerts
|
|
245
|
+
Properties:
|
|
246
|
+
AlarmDescription: "Box CPU above 80% for 30 minutes"
|
|
247
|
+
Namespace: AWS/EC2
|
|
248
|
+
MetricName: CPUUtilization
|
|
249
|
+
Dimensions: [{ Name: InstanceId, Value: !Ref Box }]
|
|
250
|
+
Statistic: Average
|
|
251
|
+
Period: 300
|
|
252
|
+
EvaluationPeriods: 6
|
|
253
|
+
Threshold: 80
|
|
254
|
+
ComparisonOperator: GreaterThanThreshold
|
|
255
|
+
TreatMissingData: notBreaching
|
|
256
|
+
AlarmActions: [!Ref AlertTopicArn]
|
|
257
|
+
OKActions: [!Ref AlertTopicArn]
|
|
258
|
+
@@ECR_REPOSITORIES@@
|
|
259
|
+
Outputs:
|
|
260
|
+
InstanceId:
|
|
261
|
+
Value: !Ref Box
|
|
262
|
+
AppOriginDomain:
|
|
263
|
+
Condition: IsProduction
|
|
264
|
+
Description: The box's stable public DNS name, for a CDN's origin.
|
|
265
|
+
Value: !Join ["", ["ec2-", !Join ["-", !Split [".", !Ref BoxAddress]], ".@@COMPUTE_DOMAIN@@"]]
|
|
266
|
+
BoxSecurityGroupId:
|
|
267
|
+
Value: !Ref BoxSecurityGroup
|
|
268
|
+
@@ECR_OUTPUTS@@
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Roll @@STACK@@'s app box: render the compose files, push them over SSM, start the containers and check
|
|
3
|
+
# them. Exits non-zero if the roll fails or the box does not answer as expected.
|
|
4
|
+
#
|
|
5
|
+
@@USAGE@@
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
8
|
+
BOX_STACK=@@BOX_STACK@@
|
|
9
|
+
RDS_STACK=@@RDS_STACK@@
|
|
10
|
+
REGION=@@REGION@@
|
|
11
|
+
DIR=/opt/@@STACK@@
|
|
12
|
+
out() { aws cloudformation describe-stacks --stack-name "$1" --query "Stacks[0].Outputs[?OutputKey==\`$2\`].OutputValue" --output text; }
|
|
13
|
+
|
|
14
|
+
BOX=$(out "$BOX_STACK" InstanceId)
|
|
15
|
+
DB_HOST=$(out "$RDS_STACK" DbEndpoint)
|
|
16
|
+
DB_SECRET=$(out "$RDS_STACK" DbSecretArn)
|
|
17
|
+
[ -n "$BOX" ] && [ "$BOX" != None ] || { echo "no instance in stack $BOX_STACK" >&2; exit 1; }
|
|
18
|
+
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
|
|
19
|
+
|
|
20
|
+
WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
|
|
21
|
+
( cd "$WORK" && bash "$HERE/render-compose.sh" "$DB_HOST" "$DB_SECRET" "$@" )
|
|
22
|
+
|
|
23
|
+
# Run a script on the box over SSM, wait for it, print its output, return its status.
|
|
24
|
+
run_on_box() {
|
|
25
|
+
local cid st i
|
|
26
|
+
cid=$(aws ssm send-command --instance-ids "$BOX" --document-name AWS-RunShellScript --timeout-seconds 900 \
|
|
27
|
+
--parameters "$1" --query 'Command.CommandId' --output text)
|
|
28
|
+
sleep 3
|
|
29
|
+
for i in $(seq 1 150); do
|
|
30
|
+
st=$(aws ssm get-command-invocation --command-id "$cid" --instance-id "$BOX" --query Status --output text)
|
|
31
|
+
[ "$st" != InProgress ] && [ "$st" != Pending ] && break
|
|
32
|
+
sleep 5
|
|
33
|
+
done
|
|
34
|
+
aws ssm get-command-invocation --command-id "$cid" --instance-id "$BOX" --query StandardOutputContent --output text
|
|
35
|
+
[ "$st" = Success ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
B64() { base64 < "$1" | tr -d '\n'; }
|
|
39
|
+
ROLL=$(jq -n --arg compose "$(B64 "$WORK/compose.json")" --arg secrets "$(B64 "$WORK/secrets.json")" \
|
|
40
|
+
--arg caddy "$(B64 "$HERE/Caddyfile")" --arg fetch "$(B64 "$HERE/fetch-secrets.sh")" \
|
|
41
|
+
--arg registry "$ACCOUNT.dkr.ecr.$REGION.amazonaws.com" --arg dir "$DIR" '
|
|
42
|
+
{commands: ["cloud-init status --wait >/dev/null 2>&1 || true",
|
|
43
|
+
"set -e", "mkdir -p \($dir)/caddy-extra && cd \($dir)", "umask 077",
|
|
44
|
+
"echo \($compose) | base64 -d > compose.json; echo \($secrets) | base64 -d > secrets.json",
|
|
45
|
+
"echo \($caddy) | base64 -d > Caddyfile; echo \($fetch) | base64 -d > fetch-secrets.sh",
|
|
46
|
+
"bash fetch-secrets.sh",
|
|
47
|
+
"aws ecr get-login-password --region '"$REGION"' | docker login --username AWS --password-stdin \($registry) >/dev/null",
|
|
48
|
+
"docker compose -f compose.json pull --quiet",
|
|
49
|
+
"docker compose -f compose.json up -d --remove-orphans",
|
|
50
|
+
"sleep 20; docker compose -f compose.json ps --format \"table {{.Service}}\\t{{.Status}}\""],
|
|
51
|
+
executionTimeout: ["900"]}')
|
|
52
|
+
|
|
53
|
+
echo "==> rolling $BOX_STACK ($BOX) at $(date -u +%H:%M:%SZ)"
|
|
54
|
+
run_on_box "$ROLL" || { echo "==> the roll did not succeed" >&2; exit 1; }
|
|
55
|
+
|
|
56
|
+
echo "==> checking the box"
|
|
57
|
+
CHECK=$(cat <<'CHECK_EOF'
|
|
58
|
+
cd @@DIR@@
|
|
59
|
+
BAD=0
|
|
60
|
+
@@HEALTH_CHECKS@@
|
|
61
|
+
DOWN=$(docker compose -f compose.json ps --format "{{.Service}} {{.Status}}" | grep -vE " Up " || true)
|
|
62
|
+
if [ -z "$DOWN" ]; then echo "ok every container is up"; else echo "FAIL containers not up: $DOWN"; BAD=1; fi
|
|
63
|
+
[ "$BAD" = 0 ]
|
|
64
|
+
CHECK_EOF
|
|
65
|
+
)
|
|
66
|
+
CHECK_JSON=$(jq -n --arg c "$(printf '%s' "$CHECK" | base64 | tr -d '\n')" '{commands: ["echo \($c) | base64 -d | bash"]}')
|
|
67
|
+
run_on_box "$CHECK_JSON" || { echo "==> the box is NOT healthy after the roll" >&2; exit 1; }
|
|
68
|
+
echo "==> box roll done ($(date -u +%H:%M:%SZ))"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Runs ON the box. Resolves secrets.json into per-service env files that compose reads, using the
|
|
3
|
+
# instance role, so secret values never pass through SSM command text.
|
|
4
|
+
#
|
|
5
|
+
# secrets.json: [{"service":"web","name":"AUTH_SECRET","valueFrom":"myapp/auth"}]
|
|
6
|
+
# valueFrom is a secret name (its whole string is the value) or a full ARN, optionally with a JSON
|
|
7
|
+
# key (arn:...:secret:NAME:jsonkey::) to pick one field of a JSON secret.
|
|
8
|
+
set -eu
|
|
9
|
+
cd "$(dirname "$0")"
|
|
10
|
+
umask 077
|
|
11
|
+
rm -f ./*.secrets.env
|
|
12
|
+
jq -r '.[] | [.service, .name, .valueFrom] | @tsv' secrets.json | while IFS=$'\t' read -r svc name from; do
|
|
13
|
+
case "$from" in
|
|
14
|
+
arn:*) id=$(echo "$from" | cut -d: -f1-7); region=$(echo "$from" | cut -d: -f4); jkey=$(echo "$from" | cut -d: -f8) ;;
|
|
15
|
+
*) id=$from; region=""; jkey="" ;;
|
|
16
|
+
esac
|
|
17
|
+
val=$(aws secretsmanager get-secret-value ${region:+--region "$region"} --secret-id "$id" --query SecretString --output text)
|
|
18
|
+
if [ -n "$jkey" ]; then val=$(printf %s "$val" | jq -r --arg k "$jkey" '.[$k]'); fi
|
|
19
|
+
[ -n "$val" ] && [ "$val" != null ] || { echo "empty secret for $svc/$name" >&2; exit 1; }
|
|
20
|
+
printf '%s=%s\n' "$name" "$val" >> "$svc.secrets.env"
|
|
21
|
+
done
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: "2010-09-09"
|
|
2
|
+
Description: >
|
|
3
|
+
Plain RDS Postgres for @@STACK@@, read by the app box (box.yaml). Private subnets only; the box's own
|
|
4
|
+
stack admits itself on 5432, so nothing else can reach it. The master password is managed by RDS in
|
|
5
|
+
Secrets Manager ({username, password}), which the box reads at deploy time.
|
|
6
|
+
|
|
7
|
+
Parameters:
|
|
8
|
+
VpcId:
|
|
9
|
+
Type: AWS::EC2::VPC::Id
|
|
10
|
+
BastionSecurityGroupId:
|
|
11
|
+
Type: String
|
|
12
|
+
Default: ""
|
|
13
|
+
Description: >
|
|
14
|
+
A security group allowed to reach the database on 5432, for the bastion that restore-to-rds.sh
|
|
15
|
+
tunnels through. Blank admits nothing but the app box.
|
|
16
|
+
PrivateSubnetIds:
|
|
17
|
+
Type: List<AWS::EC2::Subnet::Id>
|
|
18
|
+
Description: At least two subnets in different availability zones (an RDS subnet group needs them).
|
|
19
|
+
InstanceClass:
|
|
20
|
+
Type: String
|
|
21
|
+
Default: @@DB_CLASS@@
|
|
22
|
+
AllocatedStorageGb:
|
|
23
|
+
Type: Number
|
|
24
|
+
Default: @@STORAGE_GB@@
|
|
25
|
+
DatabaseName:
|
|
26
|
+
Type: String
|
|
27
|
+
Default: @@DB_NAME@@
|
|
28
|
+
EngineVersion:
|
|
29
|
+
Type: String
|
|
30
|
+
Default: "@@ENGINE_VERSION@@"
|
|
31
|
+
Description: A major version ("16") lets RDS pick its current minor release.
|
|
32
|
+
BackupRetentionDays:
|
|
33
|
+
Type: Number
|
|
34
|
+
Default: @@BACKUP_DAYS@@
|
|
35
|
+
ExistingAlertTopicArn:
|
|
36
|
+
Type: String
|
|
37
|
+
Default: ""
|
|
38
|
+
Description: An SNS topic that already has subscribers. Blank creates a new topic.
|
|
39
|
+
AlertEmail:
|
|
40
|
+
Type: String
|
|
41
|
+
Default: ""
|
|
42
|
+
Description: Subscribed to a new topic (AWS sends one confirmation link). Ignored when ExistingAlertTopicArn is set.
|
|
43
|
+
Rehearsal:
|
|
44
|
+
Type: String
|
|
45
|
+
Default: "false"
|
|
46
|
+
AllowedValues: ["true", "false"]
|
|
47
|
+
Description: >
|
|
48
|
+
true is a throwaway copy (no deletion protection, no final snapshot). false is the real
|
|
49
|
+
database: deletion protection on and a snapshot taken on delete.
|
|
50
|
+
|
|
51
|
+
Conditions:
|
|
52
|
+
NewTopic: !Equals [!Ref ExistingAlertTopicArn, ""]
|
|
53
|
+
NewTopicWithEmail: !And [!Condition NewTopic, !Not [!Equals [!Ref AlertEmail, ""]]]
|
|
54
|
+
IsRehearsal: !Equals [!Ref Rehearsal, "true"]
|
|
55
|
+
HasBastion: !Not [!Equals [!Ref BastionSecurityGroupId, ""]]
|
|
56
|
+
|
|
57
|
+
Resources:
|
|
58
|
+
DbSecurityGroup:
|
|
59
|
+
Type: AWS::EC2::SecurityGroup
|
|
60
|
+
Properties:
|
|
61
|
+
GroupDescription: @@STACK@@ RDS, 5432 from the app box (the box stack adds the rule) and a bastion if given
|
|
62
|
+
VpcId: !Ref VpcId
|
|
63
|
+
SecurityGroupIngress:
|
|
64
|
+
- !If
|
|
65
|
+
- HasBastion
|
|
66
|
+
- IpProtocol: tcp
|
|
67
|
+
FromPort: 5432
|
|
68
|
+
ToPort: 5432
|
|
69
|
+
SourceSecurityGroupId: !Ref BastionSecurityGroupId
|
|
70
|
+
Description: Bastion for restore-to-rds.sh
|
|
71
|
+
- !Ref AWS::NoValue
|
|
72
|
+
|
|
73
|
+
DbSubnetGroup:
|
|
74
|
+
Type: AWS::RDS::DBSubnetGroup
|
|
75
|
+
Properties:
|
|
76
|
+
DBSubnetGroupDescription: @@STACK@@ RDS subnets
|
|
77
|
+
SubnetIds: !Ref PrivateSubnetIds
|
|
78
|
+
|
|
79
|
+
Database:
|
|
80
|
+
Type: AWS::RDS::DBInstance
|
|
81
|
+
DeletionPolicy: !If [IsRehearsal, Delete, Snapshot]
|
|
82
|
+
UpdateReplacePolicy: !If [IsRehearsal, Delete, Snapshot]
|
|
83
|
+
Properties:
|
|
84
|
+
Engine: postgres
|
|
85
|
+
EngineVersion: !Ref EngineVersion
|
|
86
|
+
DBInstanceClass: !Ref InstanceClass
|
|
87
|
+
AllocatedStorage: !Ref AllocatedStorageGb
|
|
88
|
+
StorageType: gp3
|
|
89
|
+
StorageEncrypted: true
|
|
90
|
+
DBName: !Ref DatabaseName
|
|
91
|
+
MasterUsername: postgres
|
|
92
|
+
ManageMasterUserPassword: true
|
|
93
|
+
DBSubnetGroupName: !Ref DbSubnetGroup
|
|
94
|
+
VPCSecurityGroups: [!Ref DbSecurityGroup]
|
|
95
|
+
PubliclyAccessible: false
|
|
96
|
+
MultiAZ: false
|
|
97
|
+
BackupRetentionPeriod: !Ref BackupRetentionDays
|
|
98
|
+
DeletionProtection: !If [IsRehearsal, false, true]
|
|
99
|
+
AutoMinorVersionUpgrade: true
|
|
100
|
+
CopyTagsToSnapshot: true
|
|
101
|
+
Tags:
|
|
102
|
+
- Key: Name
|
|
103
|
+
Value: @@STACK@@-rds
|
|
104
|
+
|
|
105
|
+
AlertTopic:
|
|
106
|
+
Type: AWS::SNS::Topic
|
|
107
|
+
Condition: NewTopic
|
|
108
|
+
Properties:
|
|
109
|
+
DisplayName: @@STACK@@-alerts
|
|
110
|
+
|
|
111
|
+
AlertSubscription:
|
|
112
|
+
Type: AWS::SNS::Subscription
|
|
113
|
+
Condition: NewTopicWithEmail
|
|
114
|
+
Properties:
|
|
115
|
+
TopicArn: !Ref AlertTopic
|
|
116
|
+
Protocol: email
|
|
117
|
+
Endpoint: !Ref AlertEmail
|
|
118
|
+
|
|
119
|
+
# The journal only grows, and RDS stops accepting writes when storage is full.
|
|
120
|
+
StorageLowAlarm:
|
|
121
|
+
Type: AWS::CloudWatch::Alarm
|
|
122
|
+
Properties:
|
|
123
|
+
AlarmDescription: RDS free storage under 3 GB
|
|
124
|
+
Namespace: AWS/RDS
|
|
125
|
+
MetricName: FreeStorageSpace
|
|
126
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
127
|
+
Statistic: Minimum
|
|
128
|
+
Period: 300
|
|
129
|
+
EvaluationPeriods: 3
|
|
130
|
+
Threshold: 3221225472
|
|
131
|
+
ComparisonOperator: LessThanThreshold
|
|
132
|
+
TreatMissingData: breaching
|
|
133
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
134
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
135
|
+
|
|
136
|
+
CpuHighAlarm:
|
|
137
|
+
Type: AWS::CloudWatch::Alarm
|
|
138
|
+
Properties:
|
|
139
|
+
AlarmDescription: "RDS CPU above 80% for 15 minutes (a burstable class may be out of credits)"
|
|
140
|
+
Namespace: AWS/RDS
|
|
141
|
+
MetricName: CPUUtilization
|
|
142
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
143
|
+
Statistic: Average
|
|
144
|
+
Period: 300
|
|
145
|
+
EvaluationPeriods: 3
|
|
146
|
+
Threshold: 80
|
|
147
|
+
ComparisonOperator: GreaterThanThreshold
|
|
148
|
+
TreatMissingData: notBreaching
|
|
149
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
150
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
151
|
+
|
|
152
|
+
MemoryLowAlarm:
|
|
153
|
+
Type: AWS::CloudWatch::Alarm
|
|
154
|
+
Properties:
|
|
155
|
+
AlarmDescription: RDS freeable memory under 100 MB for 15 minutes
|
|
156
|
+
Namespace: AWS/RDS
|
|
157
|
+
MetricName: FreeableMemory
|
|
158
|
+
Dimensions: [{ Name: DBInstanceIdentifier, Value: !Ref Database }]
|
|
159
|
+
Statistic: Minimum
|
|
160
|
+
Period: 300
|
|
161
|
+
EvaluationPeriods: 3
|
|
162
|
+
Threshold: 104857600
|
|
163
|
+
ComparisonOperator: LessThanThreshold
|
|
164
|
+
TreatMissingData: notBreaching
|
|
165
|
+
AlarmActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
166
|
+
OKActions: [!If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]]
|
|
167
|
+
|
|
168
|
+
Outputs:
|
|
169
|
+
AlertTopicArn:
|
|
170
|
+
Description: Pass as AlertTopicArn to the box stack.
|
|
171
|
+
Value: !If [NewTopic, !Ref AlertTopic, !Ref ExistingAlertTopicArn]
|
|
172
|
+
DbEndpoint:
|
|
173
|
+
Value: !GetAtt Database.Endpoint.Address
|
|
174
|
+
DbName:
|
|
175
|
+
Value: !Ref DatabaseName
|
|
176
|
+
DbSecretArn:
|
|
177
|
+
Description: RDS-managed master secret ({username, password}).
|
|
178
|
+
Value: !GetAtt Database.MasterUserSecret.SecretArn
|
|
179
|
+
DbSecurityGroupId:
|
|
180
|
+
Description: Pass as DbSecurityGroupId to the box stack.
|
|
181
|
+
Value: !Ref DbSecurityGroup
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Render compose.json and secrets.json for @@STACK@@'s app box from an ECS task definition.
|
|
3
|
+
#
|
|
4
|
+
# render-compose.sh <db-host> <db-secret-arn> [task-definition]
|
|
5
|
+
#
|
|
6
|
+
# The task definition (a family or family:revision; default: the latest active revision of @@FAMILY@@)
|
|
7
|
+
# is the source of truth for each container's image, environment and secrets, so the box starts with
|
|
8
|
+
# what the task would have. A container the box runs must be named in it. DB_HOST and DB_SECRET_ARN
|
|
9
|
+
# are replaced with the RDS stack's values where the task defines them, and every container runs on
|
|
10
|
+
# the box's own network, so the proxy reaches each one on 127.0.0.1:<port>. Secrets (name + valueFrom)
|
|
11
|
+
# are written to secrets.json for fetch-secrets.sh to resolve on the box.
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
14
|
+
DB_HOST=${1:?rds endpoint}
|
|
15
|
+
DB_SECRET=${2:?rds secret arn}
|
|
16
|
+
TD=${3:-@@FAMILY@@}
|
|
17
|
+
|
|
18
|
+
DEF=$(aws ecs describe-task-definition --task-definition "$TD" --query 'taskDefinition.containerDefinitions' --output json)
|
|
19
|
+
echo "rendering from task definition $TD" >&2
|
|
20
|
+
|
|
21
|
+
jq --argjson def "$DEF" --arg host "$DB_HOST" --arg secret "$DB_SECRET" '
|
|
22
|
+
def log: {driver: "json-file", options: {"max-size": "10m", "max-file": "3"}};
|
|
23
|
+
def env(c): ((c.environment // []) | map({(.name): .value}) | add // {})
|
|
24
|
+
| (if has("DB_HOST") then .DB_HOST = $host | .DB_SECRET_ARN = $secret else . end);
|
|
25
|
+
. as $in
|
|
26
|
+
| {services: (
|
|
27
|
+
($in.services | with_entries(.value |= (. as $s
|
|
28
|
+
| ($def | map(select(.name == $s.name)) | first) as $c
|
|
29
|
+
| if $c == null then error("task definition has no container named " + $s.name) else
|
|
30
|
+
{image: $c.image, network_mode: "host", restart: "unless-stopped", logging: log,
|
|
31
|
+
environment: ({PORT: ($s.port | tostring)} + env($c))}
|
|
32
|
+
+ (if (($c.secrets // []) | length) > 0 then {env_file: [($s.name + ".secrets.env")]} else {} end)
|
|
33
|
+
end)))
|
|
34
|
+
+ {caddy: ({image: "@@PROXY_IMAGE@@", network_mode: "host", restart: "unless-stopped",
|
|
35
|
+
volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./caddy-extra:/etc/caddy/extra:ro"], logging: log}
|
|
36
|
+
+ (if $in.origin then {env_file: ["caddy.secrets.env"]} else {} end))}
|
|
37
|
+
+ (if $in.tunnel then {cloudflared: {image: $in.tunnel.image, network_mode: "host", restart: "unless-stopped",
|
|
38
|
+
command: ["tunnel", "--no-autoupdate", "--url", $in.tunnel.url, "run"],
|
|
39
|
+
env_file: ["cloudflared.secrets.env"], logging: log}} else {} end))}' \
|
|
40
|
+
"$HERE/services.json" > compose.json
|
|
41
|
+
|
|
42
|
+
jq --argjson def "$DEF" '. as $in
|
|
43
|
+
| [ ($in.services | keys[]) as $n | $def[] | select(.name == $n) | (.secrets // [])[]
|
|
44
|
+
| {service: $n, name: .name, valueFrom: .valueFrom} ]
|
|
45
|
+
+ [ (if $in.origin then {service: "caddy", name: "ORIGIN_SECRET", valueFrom: $in.origin.secret} else empty end),
|
|
46
|
+
(if $in.tunnel then {service: "cloudflared", name: "TUNNEL_TOKEN", valueFrom: $in.tunnel.token_secret} else empty end) ]' \
|
|
47
|
+
"$HERE/services.json" > secrets.json
|
|
48
|
+
|
|
49
|
+
echo "rendered compose.json and secrets.json ($(jq '.services | length' compose.json) services)"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Render compose.json and secrets.json for @@STACK@@'s app box from services.json.
|
|
3
|
+
#
|
|
4
|
+
# render-compose.sh <db-host> <db-secret-arn> [name=tag ...]
|
|
5
|
+
#
|
|
6
|
+
# Tags default to "latest". Every container gets DB_HOST, DB_NAME, DB_SECRET_ARN and PORT, and runs on the
|
|
7
|
+
# box's own network, so the proxy reaches each one on 127.0.0.1:<port>. Secrets named in services.json
|
|
8
|
+
# are written to secrets.json for fetch-secrets.sh to resolve on the box.
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
11
|
+
DB_HOST=${1:?rds endpoint}
|
|
12
|
+
DB_SECRET=${2:?rds secret arn}
|
|
13
|
+
shift 2
|
|
14
|
+
|
|
15
|
+
REGION=@@REGION@@
|
|
16
|
+
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
|
|
17
|
+
ECR="$ACCOUNT.dkr.ecr.$REGION.amazonaws.com"
|
|
18
|
+
TAGS=$(printf '%s\n' "$@" | jq -R 'select(length > 0) | split("=") | {(.[0]): .[1]}' | jq -s 'add // {}')
|
|
19
|
+
|
|
20
|
+
jq --arg ecr "$ECR" --argjson tags "$TAGS" --arg host "$DB_HOST" --arg secret "$DB_SECRET" --arg db "@@DB_NAME@@" '
|
|
21
|
+
def log: {driver: "json-file", options: {"max-size": "10m", "max-file": "3"}};
|
|
22
|
+
. as $in
|
|
23
|
+
| {services: (
|
|
24
|
+
($in.services | with_entries(.value |= (
|
|
25
|
+
{image: ($ecr + "/" + .repository + ":" + ($tags[.name] // "latest")),
|
|
26
|
+
network_mode: "host", restart: "unless-stopped", logging: log,
|
|
27
|
+
environment: ({PORT: (.port | tostring), DB_HOST: $host, DB_NAME: $db, DB_SECRET_ARN: $secret} + .env)}
|
|
28
|
+
+ (if (.secrets | length) > 0 then {env_file: [(.name + ".secrets.env")]} else {} end))))
|
|
29
|
+
+ {caddy: ({image: "@@PROXY_IMAGE@@", network_mode: "host", restart: "unless-stopped",
|
|
30
|
+
volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro", "./caddy-extra:/etc/caddy/extra:ro"], logging: log}
|
|
31
|
+
+ (if $in.origin then {env_file: ["caddy.secrets.env"]} else {} end))}
|
|
32
|
+
+ (if $in.tunnel then {cloudflared: {image: $in.tunnel.image, network_mode: "host", restart: "unless-stopped",
|
|
33
|
+
command: ["tunnel", "--no-autoupdate", "--url", $in.tunnel.url, "run"],
|
|
34
|
+
env_file: ["cloudflared.secrets.env"], logging: log}} else {} end))}' \
|
|
35
|
+
"$HERE/services.json" > compose.json
|
|
36
|
+
|
|
37
|
+
jq '. as $in
|
|
38
|
+
| [ ($in.services[] | . as $v | ($v.secrets | to_entries[]) | {service: $v.name, name: .key, valueFrom: .value}),
|
|
39
|
+
(if $in.origin then {service: "caddy", name: "ORIGIN_SECRET", valueFrom: $in.origin.secret} else empty end),
|
|
40
|
+
(if $in.tunnel then {service: "cloudflared", name: "TUNNEL_TOKEN", valueFrom: $in.tunnel.token_secret} else empty end) ]' \
|
|
41
|
+
"$HERE/services.json" > secrets.json
|
|
42
|
+
|
|
43
|
+
echo "rendered compose.json and secrets.json ($(jq '.services | length' compose.json) services)"
|