@friggframework/devtools 2.0.0--canary.474.86c5119.0 → 2.0.0--canary.474.898a56c.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/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +762 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +154 -1
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +20 -5
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +645 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +174 -91
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +330 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +108 -14
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
- package/package.json +6 -6
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Build Template vs Deployed Template Analysis
|
|
2
|
+
|
|
3
|
+
**CRITICAL DISCOVERY**: The local build template and deployed CloudFormation template are DIFFERENT!
|
|
4
|
+
|
|
5
|
+
## The Discrepancy
|
|
6
|
+
|
|
7
|
+
### Local Build Template (.serverless/cloudformation-template-update-stack.json)
|
|
8
|
+
|
|
9
|
+
**Contains VPC Resources:**
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"Resources": {
|
|
13
|
+
"FriggVPC": { "Type": "AWS::EC2::VPC" },
|
|
14
|
+
"FriggPrivateSubnet1": { "Type": "AWS::EC2::Subnet" },
|
|
15
|
+
"FriggPrivateSubnet2": { "Type": "AWS::EC2::Subnet" },
|
|
16
|
+
"FriggPublicSubnet": { "Type": "AWS::EC2::Subnet" },
|
|
17
|
+
"FriggPublicSubnet2": { "Type": "AWS::EC2::Subnet" },
|
|
18
|
+
"FriggLambdaSecurityGroup": { "Type": "AWS::EC2::SecurityGroup" },
|
|
19
|
+
"FriggVPCEndpointSecurityGroup": { "Type": "AWS::EC2::SecurityGroup" }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Lambda VPC Config uses Refs:**
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"VpcConfig": {
|
|
28
|
+
"SecurityGroupIds": [{ "Ref": "FriggLambdaSecurityGroup" }],
|
|
29
|
+
"SubnetIds": [
|
|
30
|
+
{ "Ref": "FriggPrivateSubnet1" },
|
|
31
|
+
{ "Ref": "FriggPrivateSubnet2" }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Deployed CloudFormation Template (in AWS)
|
|
38
|
+
|
|
39
|
+
**Does NOT contain VPC Resources:**
|
|
40
|
+
- ✅ Has Lambda functions
|
|
41
|
+
- ✅ Has SQS queues
|
|
42
|
+
- ✅ Has IAM roles
|
|
43
|
+
- ❌ **NO VPC resources** (FriggVPC, FriggPrivateSubnet1, FriggPrivateSubnet2, etc.)
|
|
44
|
+
|
|
45
|
+
**Lambda VPC Config uses Hardcoded Physical IDs:**
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"VpcConfig": {
|
|
49
|
+
"SecurityGroupIds": ["sg-07c01370e830b6ad6"], // Hardcoded physical ID
|
|
50
|
+
"SubnetIds": [
|
|
51
|
+
"subnet-00ab9e0502e66aac3", // Hardcoded physical ID
|
|
52
|
+
"subnet-00d085a52937aaf91" // Hardcoded physical ID
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## What This Means
|
|
59
|
+
|
|
60
|
+
### 1. VPC Resources Were Removed from Stack
|
|
61
|
+
|
|
62
|
+
At some point, the VPC resources were removed from CloudFormation management:
|
|
63
|
+
- VPC was created by CloudFormation originally
|
|
64
|
+
- VPC was later removed from the template/stack (but physical resources left in AWS)
|
|
65
|
+
- Lambda functions still reference the VPC subnets/SG by physical ID
|
|
66
|
+
|
|
67
|
+
### 2. Local Build != Deployed State
|
|
68
|
+
|
|
69
|
+
**If you deploy the local build template now:**
|
|
70
|
+
- CloudFormation will try to CREATE new VPC resources (FriggVPC, subnets, SG)
|
|
71
|
+
- CloudFormation will FAIL because resources with those logical IDs already exist physically
|
|
72
|
+
- OR it will create NEW resources with different physical IDs
|
|
73
|
+
- Lambda functions will reference the NEW resources (via Ref)
|
|
74
|
+
|
|
75
|
+
### 3. Import Operation is Complex
|
|
76
|
+
|
|
77
|
+
When you import `vpc-0eadd96976d29ede7`:
|
|
78
|
+
- You need to map it to the logical ID `FriggVPC` in the template
|
|
79
|
+
- You need to import ALL related resources:
|
|
80
|
+
- `FriggPrivateSubnet1` → `subnet-00ab9e0502e66aac3`
|
|
81
|
+
- `FriggPrivateSubnet2` → `subnet-00d085a52937aaf91`
|
|
82
|
+
- `FriggLambdaSecurityGroup` → `sg-07c01370e830b6ad6`
|
|
83
|
+
- etc.
|
|
84
|
+
|
|
85
|
+
## CloudFormation Import Process
|
|
86
|
+
|
|
87
|
+
### How Import Works
|
|
88
|
+
|
|
89
|
+
**Q: Will it know to grab the right VPC?**
|
|
90
|
+
|
|
91
|
+
**A:** ❌ **NO, you must explicitly tell it which physical ID maps to which logical ID**
|
|
92
|
+
|
|
93
|
+
CloudFormation import requires:
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"Resources": [
|
|
97
|
+
{
|
|
98
|
+
"ResourceType": "AWS::EC2::VPC",
|
|
99
|
+
"LogicalResourceId": "FriggVPC",
|
|
100
|
+
"ResourceIdentifier": {
|
|
101
|
+
"VpcId": "vpc-0eadd96976d29ede7" // You specify this
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"ResourceType": "AWS::EC2::Subnet",
|
|
106
|
+
"LogicalResourceId": "FriggPrivateSubnet1",
|
|
107
|
+
"ResourceIdentifier": {
|
|
108
|
+
"SubnetId": "subnet-00ab9e0502e66aac3" // You specify this
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
// ... more resources
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Q: Will it clear or delete the old resources?**
|
|
117
|
+
|
|
118
|
+
**A:** ❌ **NO, you must manually delete unused resources**
|
|
119
|
+
|
|
120
|
+
CloudFormation import:
|
|
121
|
+
- ✅ Adds existing resources to the stack (doesn't create or delete anything)
|
|
122
|
+
- ✅ Updates Lambda Refs to point to imported resources
|
|
123
|
+
- ❌ Does NOT delete the 2 unused VPCs
|
|
124
|
+
- ❌ Does NOT clean up orphaned resources
|
|
125
|
+
|
|
126
|
+
## The Right Approach
|
|
127
|
+
|
|
128
|
+
### Step 1: Update Local Build Template to Match Deployed State
|
|
129
|
+
|
|
130
|
+
**OPTION A: Remove VPC from local template (quick fix)**
|
|
131
|
+
|
|
132
|
+
Remove VPC resources from `serverless.yml`:
|
|
133
|
+
```yaml
|
|
134
|
+
# Comment out or remove:
|
|
135
|
+
# resources:
|
|
136
|
+
# Resources:
|
|
137
|
+
# FriggVPC: ...
|
|
138
|
+
# FriggPrivateSubnet1: ...
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Then use hardcoded subnet/SG IDs:
|
|
142
|
+
```yaml
|
|
143
|
+
provider:
|
|
144
|
+
vpc:
|
|
145
|
+
securityGroupIds:
|
|
146
|
+
- sg-07c01370e830b6ad6
|
|
147
|
+
subnetIds:
|
|
148
|
+
- subnet-00ab9e0502e66aac3
|
|
149
|
+
- subnet-00d085a52937aaf91
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**OPTION B: Import VPC resources to stack (proper fix)**
|
|
153
|
+
|
|
154
|
+
1. Create import template with mappings
|
|
155
|
+
2. Run CloudFormation import operation
|
|
156
|
+
3. Redeploy with local template
|
|
157
|
+
|
|
158
|
+
### Step 2: Decision Point
|
|
159
|
+
|
|
160
|
+
**CRITICAL QUESTION: Do you want CloudFormation to manage the VPC?**
|
|
161
|
+
|
|
162
|
+
**If YES (recommended for Frigg framework):**
|
|
163
|
+
- ✅ Import `vpc-0eadd96976d29ede7` and its resources
|
|
164
|
+
- ✅ CloudFormation will manage VPC lifecycle
|
|
165
|
+
- ✅ Template and reality stay in sync
|
|
166
|
+
- ✅ Proper infrastructure as code
|
|
167
|
+
|
|
168
|
+
**If NO (simpler but less controlled):**
|
|
169
|
+
- ✅ Remove VPC from local template
|
|
170
|
+
- ✅ Use hardcoded subnet/SG IDs in serverless.yml
|
|
171
|
+
- ✅ Manually manage VPC outside CloudFormation
|
|
172
|
+
- ⚠️ Template drift will always exist
|
|
173
|
+
|
|
174
|
+
## Recommended Action (CloudFormation Import)
|
|
175
|
+
|
|
176
|
+
### Phase 1: Prepare Import Template
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# 1. Get the local template
|
|
180
|
+
cd /Users/sean/Documents/GitHub/quo--frigg/backend
|
|
181
|
+
|
|
182
|
+
# 2. Create import-resources.json
|
|
183
|
+
cat > import-resources.json <<EOF
|
|
184
|
+
[
|
|
185
|
+
{
|
|
186
|
+
"ResourceType": "AWS::EC2::VPC",
|
|
187
|
+
"LogicalResourceId": "FriggVPC",
|
|
188
|
+
"ResourceIdentifier": { "VpcId": "vpc-0eadd96976d29ede7" }
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"ResourceType": "AWS::EC2::Subnet",
|
|
192
|
+
"LogicalResourceId": "FriggPrivateSubnet1",
|
|
193
|
+
"ResourceIdentifier": { "SubnetId": "subnet-00ab9e0502e66aac3" }
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"ResourceType": "AWS::EC2::Subnet",
|
|
197
|
+
"LogicalResourceId": "FriggPrivateSubnet2",
|
|
198
|
+
"ResourceIdentifier": { "SubnetId": "subnet-00d085a52937aaf91" }
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"ResourceType": "AWS::EC2::SecurityGroup",
|
|
202
|
+
"LogicalResourceId": "FriggLambdaSecurityGroup",
|
|
203
|
+
"ResourceIdentifier": { "GroupId": "sg-07c01370e830b6ad6" }
|
|
204
|
+
}
|
|
205
|
+
// ... add other resources (public subnets, SGs, etc.)
|
|
206
|
+
]
|
|
207
|
+
EOF
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Phase 2: Create Change Set for Import
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
aws cloudformation create-change-set \
|
|
214
|
+
--stack-name quo-integrations-dev \
|
|
215
|
+
--change-set-name import-vpc-resources \
|
|
216
|
+
--change-set-type IMPORT \
|
|
217
|
+
--resources-to-import file://import-resources.json \
|
|
218
|
+
--template-body file://.serverless/cloudformation-template-update-stack.json \
|
|
219
|
+
--capabilities CAPABILITY_IAM \
|
|
220
|
+
--region us-east-1
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Phase 3: Review and Execute
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Review the change set
|
|
227
|
+
aws cloudformation describe-change-set \
|
|
228
|
+
--stack-name quo-integrations-dev \
|
|
229
|
+
--change-set-name import-vpc-resources \
|
|
230
|
+
--region us-east-1
|
|
231
|
+
|
|
232
|
+
# Execute if looks good
|
|
233
|
+
aws cloudformation execute-change-set \
|
|
234
|
+
--stack-name quo-integrations-dev \
|
|
235
|
+
--change-set-name import-vpc-resources \
|
|
236
|
+
--region us-east-1
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Phase 4: Update Lambda VPC Configs
|
|
240
|
+
|
|
241
|
+
After import, CloudFormation will:
|
|
242
|
+
- ✅ Recognize VPC resources are in the stack
|
|
243
|
+
- ✅ Lambda Refs will resolve to imported physical IDs
|
|
244
|
+
- ✅ Next deploy will update Lambda VPC configs from default VPC back to Frigg VPC
|
|
245
|
+
|
|
246
|
+
### Phase 5: Clean Up
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Delete unused VPCs
|
|
250
|
+
aws ec2 delete-vpc --vpc-id vpc-0e2351eac99adcb83 --region us-east-1
|
|
251
|
+
aws ec2 delete-vpc --vpc-id vpc-020a0365610c05f0b --region us-east-1
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Alternative: Simpler Approach (Remove VPC from Template)
|
|
255
|
+
|
|
256
|
+
If you don't want CloudFormation to manage VPC:
|
|
257
|
+
|
|
258
|
+
### Step 1: Update serverless.yml
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
provider:
|
|
262
|
+
name: aws
|
|
263
|
+
vpc:
|
|
264
|
+
# Hardcode the VPC resources
|
|
265
|
+
securityGroupIds:
|
|
266
|
+
- sg-07c01370e830b6ad6
|
|
267
|
+
subnetIds:
|
|
268
|
+
- subnet-00ab9e0502e66aac3
|
|
269
|
+
- subnet-00d085a52937aaf91
|
|
270
|
+
|
|
271
|
+
# Remove VPC resource definitions
|
|
272
|
+
# resources:
|
|
273
|
+
# Resources:
|
|
274
|
+
# FriggVPC: ...
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Step 2: Deploy
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
serverless deploy --stage dev
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Step 3: Clean Up
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# Delete all 3 orphaned VPCs
|
|
287
|
+
aws ec2 delete-vpc --vpc-id vpc-0eadd96976d29ede7
|
|
288
|
+
aws ec2 delete-vpc --vpc-id vpc-0e2351eac99adcb83
|
|
289
|
+
aws ec2 delete-vpc --vpc-id vpc-020a0365610c05f0b
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Recommendation
|
|
293
|
+
|
|
294
|
+
**For Frigg Framework consistency: Import VPC resources**
|
|
295
|
+
|
|
296
|
+
Why:
|
|
297
|
+
- ✅ Maintains infrastructure as code principles
|
|
298
|
+
- ✅ VPC lifecycle managed by CloudFormation
|
|
299
|
+
- ✅ Consistent with Frigg framework design
|
|
300
|
+
- ✅ Template matches deployed state
|
|
301
|
+
- ✅ `frigg doctor` will show 100/100 health
|
|
302
|
+
|
|
303
|
+
**Trade-offs:**
|
|
304
|
+
- ⚠️ More complex import process
|
|
305
|
+
- ⚠️ Must map all VPC resources correctly
|
|
306
|
+
- ⚠️ One-time effort but proper long-term solution
|
|
307
|
+
|
|
308
|
+
## Next Steps for Relationship Analysis
|
|
309
|
+
|
|
310
|
+
The relationship analysis should:
|
|
311
|
+
|
|
312
|
+
1. **Parse local build template** (not just deployed template)
|
|
313
|
+
2. **Detect CloudFormation Refs** in Lambda VPC configs
|
|
314
|
+
3. **Resolve Refs to physical IDs** from deployed stack
|
|
315
|
+
4. **Match orphaned resources** against resolved physical IDs
|
|
316
|
+
5. **Identify import mapping**:
|
|
317
|
+
- `FriggVPC` (logical) → `vpc-0eadd96976d29ede7` (physical)
|
|
318
|
+
- `FriggPrivateSubnet1` → `subnet-00ab9e0502e66aac3`
|
|
319
|
+
- etc.
|
|
320
|
+
|
|
321
|
+
This will enable `frigg repair --import` to:
|
|
322
|
+
- Show correct VPC to import
|
|
323
|
+
- Generate import-resources.json automatically
|
|
324
|
+
- Handle CloudFormation import operation
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Orphan Detection: Relationship Analysis & Multiple Resource Warning System
|
|
2
|
+
|
|
3
|
+
## Problem Statement
|
|
4
|
+
|
|
5
|
+
When `frigg doctor` detects multiple orphaned resources of the same type (e.g., 3 VPCs, 10 subnets), users cannot easily determine:
|
|
6
|
+
1. Which orphaned resources are actually relevant to import
|
|
7
|
+
2. Which orphaned resources are old/unused and should be deleted
|
|
8
|
+
3. Whether any orphaned resources are being actively referenced by drifted resources
|
|
9
|
+
|
|
10
|
+
## Real-World Example: acme-integrations-dev Stack
|
|
11
|
+
|
|
12
|
+
### AWS Reality (Verified 2025-10-27)
|
|
13
|
+
|
|
14
|
+
**CloudFormation Stack State:**
|
|
15
|
+
- Stack Name: `acme-integrations-dev`
|
|
16
|
+
- VPCs in stack: **0** (stack doesn't manage VPC)
|
|
17
|
+
- Lambda functions: 16 (all with VPC configuration drift)
|
|
18
|
+
|
|
19
|
+
**Lambda Functions Configuration:**
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"VpcId": "vpc-01f21101d4ed6db59", // AWS Default VPC (172.31.0.0/16)
|
|
23
|
+
"SubnetIds": [
|
|
24
|
+
"subnet-020d32e3ca398a041", // In default VPC
|
|
25
|
+
"subnet-0c186318804aba790" // In default VPC
|
|
26
|
+
],
|
|
27
|
+
"SecurityGroupIds": [
|
|
28
|
+
"sg-0aca40438d17344c4" // In default VPC
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Orphaned Resources Detected:**
|
|
34
|
+
|
|
35
|
+
**3 VPCs (ALL with Frigg CloudFormation tags):**
|
|
36
|
+
1. `vpc-0eadd96976d29ede7` (10.0.0.0/16)
|
|
37
|
+
- Tags: `aws:cloudformation:stack-name=acme-integrations-dev`, `aws:cloudformation:logical-id=FriggVPC`
|
|
38
|
+
- Status: **NOT in CloudFormation stack, NOT used by Lambdas**
|
|
39
|
+
- Conclusion: **Old unused VPC, should be deleted**
|
|
40
|
+
|
|
41
|
+
2. `vpc-0e2351eac99adcb83` (10.0.0.0/16)
|
|
42
|
+
- Tags: `aws:cloudformation:stack-name=acme-integrations-dev`, `aws:cloudformation:logical-id=FriggVPC`
|
|
43
|
+
- Status: **NOT in CloudFormation stack, NOT used by Lambdas**
|
|
44
|
+
- Conclusion: **Old unused VPC, should be deleted**
|
|
45
|
+
|
|
46
|
+
3. `vpc-020a0365610c05f0b` (10.0.0.0/16)
|
|
47
|
+
- Tags: `aws:cloudformation:stack-name=acme-integrations-dev`, `aws:cloudformation:logical-id=FriggVPC`
|
|
48
|
+
- Status: **NOT in CloudFormation stack, NOT used by Lambdas**
|
|
49
|
+
- Conclusion: **Old unused VPC, should be deleted**
|
|
50
|
+
|
|
51
|
+
**10 Subnets:** All belong to the 3 orphaned VPCs (not the default VPC being used)
|
|
52
|
+
|
|
53
|
+
**3 Security Groups:** All belong to the 3 orphaned VPCs (not the default VPC being used)
|
|
54
|
+
|
|
55
|
+
### Key Insights
|
|
56
|
+
|
|
57
|
+
1. **CloudFormation tags are misleading**: All 3 VPCs have identical CFN tags but NONE are in the stack
|
|
58
|
+
2. **Lambda drift is expected**: Lambdas use default VPC which has NO Frigg tags
|
|
59
|
+
3. **All orphaned resources are unused**: None of the 16 orphaned resources are being referenced
|
|
60
|
+
4. **Old deployment artifacts**: The orphaned VPCs are likely from previous Frigg deployments that failed cleanup
|
|
61
|
+
|
|
62
|
+
## Solution Design
|
|
63
|
+
|
|
64
|
+
### Phase 1: Relationship Analysis
|
|
65
|
+
|
|
66
|
+
Analyze connections between:
|
|
67
|
+
- **Drift Issues** → Resources being referenced in actual values
|
|
68
|
+
- **Orphaned Resources** → Resources detected but not in stack
|
|
69
|
+
- **Resource Hierarchies** → VPCs contain subnets/SGs, subnets belong to VPCs
|
|
70
|
+
|
|
71
|
+
### Phase 2: Extract Referenced Resource IDs
|
|
72
|
+
|
|
73
|
+
From property mismatch drift issues, extract resource IDs that are **actually being used**:
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// Example drift issue:
|
|
77
|
+
{
|
|
78
|
+
propertyPath: "VpcConfig.SubnetIds",
|
|
79
|
+
expectedValue: "subnet-old-1,subnet-old-2",
|
|
80
|
+
actualValue: "subnet-020d32e3ca398a041,subnet-0c186318804aba790"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Extract: ["subnet-020d32e3ca398a041", "subnet-0c186318804aba790"]
|
|
84
|
+
// These are the subnets actually being used (even if not orphaned)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Property Paths to Analyze:**
|
|
88
|
+
- `VpcConfig.SubnetIds` → Subnet IDs
|
|
89
|
+
- `VpcConfig.SecurityGroupIds` → Security Group IDs
|
|
90
|
+
- `VpcId` → VPC IDs
|
|
91
|
+
- `SubnetId` → Subnet IDs (for other resources)
|
|
92
|
+
|
|
93
|
+
### Phase 3: Build Resource Relationship Graph
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
{
|
|
97
|
+
orphanedResources: [
|
|
98
|
+
{
|
|
99
|
+
physicalId: "vpc-0eadd96976d29ede7",
|
|
100
|
+
resourceType: "AWS::EC2::VPC",
|
|
101
|
+
metadata: {
|
|
102
|
+
isReferencedByDrift: false, // NOT referenced in any drift
|
|
103
|
+
containsReferencedResources: false, // No subnets/SGs in this VPC are referenced
|
|
104
|
+
relatedOrphans: [ // Other orphans in this VPC
|
|
105
|
+
"subnet-0ad31b5ee6814b8fa",
|
|
106
|
+
"sg-03abddb7fb50aeaff"
|
|
107
|
+
],
|
|
108
|
+
priority: "LOW" // Unused, should delete not import
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
physicalId: "subnet-020d32e3ca398a041",
|
|
113
|
+
resourceType: "AWS::EC2::Subnet",
|
|
114
|
+
vpcId: "vpc-01f21101d4ed6db59", // In default VPC (not orphaned)
|
|
115
|
+
metadata: {
|
|
116
|
+
isReferencedByDrift: true, // ✅ Referenced by 16 Lambda functions
|
|
117
|
+
referencedBy: [ // Which resources reference this
|
|
118
|
+
"acme-integrations-dev-attio",
|
|
119
|
+
"acme-integrations-dev-auth",
|
|
120
|
+
// ... 14 more
|
|
121
|
+
],
|
|
122
|
+
priority: "N/A" // Not orphaned, just drift reference
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Phase 4: Multi-Resource Warning System
|
|
130
|
+
|
|
131
|
+
When multiple resources of the same type are detected, show contextual warnings:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
⚠ WARNING: Multiple VPCs detected (3 orphaned)
|
|
135
|
+
|
|
136
|
+
Analysis:
|
|
137
|
+
• vpc-0eadd96976d29ede7 - No active references, contains 3 orphaned subnets [UNUSED]
|
|
138
|
+
• vpc-0e2351eac99adcb83 - No active references, contains 4 orphaned subnets [UNUSED]
|
|
139
|
+
• vpc-020a0365610c05f0b - No active references, contains 3 orphaned subnets [UNUSED]
|
|
140
|
+
|
|
141
|
+
Recommendation:
|
|
142
|
+
❌ Do NOT import these VPCs - they are old deployment artifacts
|
|
143
|
+
✅ Lambda functions use default VPC (vpc-01f21101d4ed6db59) - drift is expected
|
|
144
|
+
🧹 Consider cleaning up orphaned VPCs to reduce clutter:
|
|
145
|
+
$ aws ec2 delete-vpc --vpc-id vpc-0eadd96976d29ede7
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Implementation Plan
|
|
149
|
+
|
|
150
|
+
### 1. Add Relationship Analysis to AWSResourceDetector
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
class AWSResourceDetector {
|
|
154
|
+
/**
|
|
155
|
+
* Find orphaned resources with relationship metadata
|
|
156
|
+
*/
|
|
157
|
+
async findOrphanedResourcesWithRelationships({
|
|
158
|
+
stackIdentifier,
|
|
159
|
+
stackResources,
|
|
160
|
+
driftIssues = []
|
|
161
|
+
}) {
|
|
162
|
+
// 1. Find orphaned resources (existing logic)
|
|
163
|
+
const orphans = await this.findOrphanedResources({
|
|
164
|
+
stackIdentifier,
|
|
165
|
+
stackResources
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// 2. Extract referenced resource IDs from drift
|
|
169
|
+
const referencedIds = this._extractReferencedResourceIds(driftIssues);
|
|
170
|
+
|
|
171
|
+
// 3. Enrich orphans with relationship metadata
|
|
172
|
+
return this._enrichWithRelationshipMetadata(orphans, referencedIds);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Extract resource IDs being referenced in drift actualValue
|
|
177
|
+
*/
|
|
178
|
+
_extractReferencedResourceIds(driftIssues) {
|
|
179
|
+
const referenced = {
|
|
180
|
+
vpcIds: new Set(),
|
|
181
|
+
subnetIds: new Set(),
|
|
182
|
+
securityGroupIds: new Set()
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
for (const issue of driftIssues) {
|
|
186
|
+
if (issue.type !== 'PROPERTY_MISMATCH') continue;
|
|
187
|
+
|
|
188
|
+
const { propertyPath, actualValue } = issue.mismatch;
|
|
189
|
+
|
|
190
|
+
// Extract subnet IDs from VpcConfig.SubnetIds
|
|
191
|
+
if (propertyPath === 'VpcConfig.SubnetIds') {
|
|
192
|
+
const subnetIds = actualValue.split(',');
|
|
193
|
+
subnetIds.forEach(id => referenced.subnetIds.add(id));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Extract SG IDs from VpcConfig.SecurityGroupIds
|
|
197
|
+
if (propertyPath === 'VpcConfig.SecurityGroupIds') {
|
|
198
|
+
const sgIds = actualValue.split(',');
|
|
199
|
+
sgIds.forEach(id => referenced.securityGroupIds.add(id));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
vpcIds: Array.from(referenced.vpcIds),
|
|
205
|
+
subnetIds: Array.from(referenced.subnetIds),
|
|
206
|
+
securityGroupIds: Array.from(referenced.securityGroupIds)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Add relationship metadata to orphaned resources
|
|
212
|
+
*/
|
|
213
|
+
_enrichWithRelationshipMetadata(orphans, referencedIds) {
|
|
214
|
+
return orphans.map(orphan => {
|
|
215
|
+
const metadata = {
|
|
216
|
+
isReferencedByDrift: false,
|
|
217
|
+
referencedBy: [],
|
|
218
|
+
relatedOrphans: [],
|
|
219
|
+
priority: 'LOW'
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Check if this orphan is actually referenced
|
|
223
|
+
if (orphan.resourceType === 'AWS::EC2::Subnet') {
|
|
224
|
+
metadata.isReferencedByDrift = referencedIds.subnetIds.includes(orphan.physicalId);
|
|
225
|
+
} else if (orphan.resourceType === 'AWS::EC2::SecurityGroup') {
|
|
226
|
+
metadata.isReferencedByDrift = referencedIds.securityGroupIds.includes(orphan.physicalId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (metadata.isReferencedByDrift) {
|
|
230
|
+
metadata.priority = 'HIGH';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { ...orphan, metadata };
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Analyze orphan summary for warnings
|
|
239
|
+
*/
|
|
240
|
+
analyzeOrphanSummary(orphans) {
|
|
241
|
+
const summary = {
|
|
242
|
+
warnings: [],
|
|
243
|
+
multipleResourceTypes: []
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Group by resource type
|
|
247
|
+
const grouped = {};
|
|
248
|
+
for (const orphan of orphans) {
|
|
249
|
+
grouped[orphan.resourceType] = grouped[orphan.resourceType] || [];
|
|
250
|
+
grouped[orphan.resourceType].push(orphan);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for multiples
|
|
254
|
+
for (const [type, resources] of Object.entries(grouped)) {
|
|
255
|
+
if (resources.length > 1) {
|
|
256
|
+
const shortType = type.replace('AWS::EC2::', '');
|
|
257
|
+
summary.warnings.push(
|
|
258
|
+
`Multiple ${shortType}s detected (${resources.length}). Review relationships before importing.`
|
|
259
|
+
);
|
|
260
|
+
summary.multipleResourceTypes.push(type);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return summary;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 2. Update Health Check to Use Relationship Analysis
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
// domains/health/application/use-cases/check-stack-health-use-case.js
|
|
273
|
+
|
|
274
|
+
class CheckStackHealthUseCase {
|
|
275
|
+
async execute({ stackName, region }) {
|
|
276
|
+
// ... existing drift detection ...
|
|
277
|
+
|
|
278
|
+
// Enhanced orphan detection with relationships
|
|
279
|
+
const orphanedResources = await this.resourceDetector.findOrphanedResourcesWithRelationships({
|
|
280
|
+
stackIdentifier,
|
|
281
|
+
stackResources,
|
|
282
|
+
driftIssues: issues // Pass drift issues for analysis
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Analyze for warnings
|
|
286
|
+
const orphanSummary = this.resourceDetector.analyzeOrphanSummary(orphanedResources);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
// ... existing fields ...
|
|
290
|
+
orphanedResources,
|
|
291
|
+
orphanAnalysis: orphanSummary
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### 3. Update Frigg Doctor Output
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
// Show relationship warnings
|
|
301
|
+
if (orphanAnalysis.warnings.length > 0) {
|
|
302
|
+
console.log('\n⚠ ORPHAN ANALYSIS WARNINGS:\n');
|
|
303
|
+
for (const warning of orphanAnalysis.warnings) {
|
|
304
|
+
console.log(` ${warning}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Show orphan details with metadata
|
|
309
|
+
console.log('\n CRITICAL ISSUES:');
|
|
310
|
+
for (const orphan of orphanedResources) {
|
|
311
|
+
console.log(` [ORPHANED_RESOURCE] ${orphan.physicalId}`);
|
|
312
|
+
console.log(` Resource: ${orphan.resourceType}`);
|
|
313
|
+
|
|
314
|
+
if (orphan.metadata) {
|
|
315
|
+
if (orphan.metadata.isReferencedByDrift) {
|
|
316
|
+
console.log(` ✅ ACTIVELY USED - Referenced by ${orphan.metadata.referencedBy.length} drifted resources`);
|
|
317
|
+
console.log(` Fix: Import to CloudFormation stack`);
|
|
318
|
+
} else {
|
|
319
|
+
console.log(` ❌ UNUSED - Not referenced by any stack resources`);
|
|
320
|
+
console.log(` Fix: Consider deleting instead of importing`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### 4. Update Frigg Repair to Handle Multiples
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
// frigg repair --import <stack-name>
|
|
330
|
+
|
|
331
|
+
if (orphanAnalysis.multipleResourceTypes.includes('AWS::EC2::VPC')) {
|
|
332
|
+
console.log('\n⚠ WARNING: Multiple VPCs detected');
|
|
333
|
+
console.log('Please review and select which VPC to import:\n');
|
|
334
|
+
|
|
335
|
+
const vpcs = orphanedResources.filter(o => o.resourceType === 'AWS::EC2::VPC');
|
|
336
|
+
for (let i = 0; i < vpcs.length; i++) {
|
|
337
|
+
const vpc = vpcs[i];
|
|
338
|
+
console.log(` ${i + 1}. ${vpc.physicalId} (${vpc.cidrBlock})`);
|
|
339
|
+
console.log(` Referenced: ${vpc.metadata.isReferencedByDrift ? 'Yes' : 'No'}`);
|
|
340
|
+
console.log(` Related Resources: ${vpc.metadata.relatedOrphans.length} subnets/SGs\n`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Prompt user to select or skip
|
|
344
|
+
const selection = await promptUser('Select VPC number to import (or "skip" to skip all): ');
|
|
345
|
+
|
|
346
|
+
if (selection === 'skip') {
|
|
347
|
+
console.log('Skipping VPC import');
|
|
348
|
+
// Remove VPCs from import list
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Testing Strategy
|
|
354
|
+
|
|
355
|
+
### Test 1: Detect Unused Orphans
|
|
356
|
+
✅ 3 VPCs with CFN tags but NOT in stack, NOT referenced → mark as unused
|
|
357
|
+
|
|
358
|
+
### Test 2: Detect Referenced Non-Orphans
|
|
359
|
+
✅ Subnets in default VPC referenced by Lambdas but NOT orphaned → show in analysis
|
|
360
|
+
|
|
361
|
+
### Test 3: Multi-Resource Warning
|
|
362
|
+
✅ Multiple VPCs detected → show warning + require user selection
|
|
363
|
+
|
|
364
|
+
### Test 4: VPC Hierarchy Analysis
|
|
365
|
+
✅ VPC contains subnets → link orphaned subnets to orphaned VPC
|
|
366
|
+
|
|
367
|
+
## Benefits
|
|
368
|
+
|
|
369
|
+
1. **Prevents Bad Imports**: Users won't accidentally import unused old VPCs
|
|
370
|
+
2. **Clarifies Drift**: Explains why drift exists (using default VPC vs Frigg VPC)
|
|
371
|
+
3. **Cleanup Guidance**: Identifies resources that should be deleted, not imported
|
|
372
|
+
4. **Informed Decisions**: Shows relationships between resources before action
|
|
373
|
+
5. **Reduces Errors**: Requires explicit selection when multiple resources exist
|
|
374
|
+
|
|
375
|
+
## Next Steps
|
|
376
|
+
|
|
377
|
+
1. ✅ Write TDD tests for relationship analysis
|
|
378
|
+
2. ⬜ Implement `findOrphanedResourcesWithRelationships` method
|
|
379
|
+
3. ⬜ Implement `_extractReferencedResourceIds` helper
|
|
380
|
+
4. ⬜ Implement `_enrichWithRelationshipMetadata` helper
|
|
381
|
+
5. ⬜ Implement `analyzeOrphanSummary` method
|
|
382
|
+
6. ⬜ Update `CheckStackHealthUseCase` to use new method
|
|
383
|
+
7. ⬜ Update `frigg doctor` output to show warnings
|
|
384
|
+
8. ⬜ Update `frigg repair` to handle multiple resources
|
|
385
|
+
9. ⬜ Test with real acme-integrations-dev data
|
|
386
|
+
10. ⬜ Document relationship analysis in HEALTH.md
|