diff --git a/scripts/README.md b/scripts/README.md index f6c031d..0f4da61 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -18,8 +18,13 @@ default tag on the resources it creates: | [`hackforla/incubator`](https://github.com/hackforla/incubator) | `managed-by = terraform-incubator` | | [`hackforla/devops-security`](https://github.com/hackforla/devops-security) | `managed-by = terraform-devops-security` | -So the absence of that tag is the signal that nothing manages a resource. This -script sweeps the account, reads the tag, and buckets everything three ways. +A third value, `managed-by = exempt`, marks a resource that is deliberately +outside Terraform. Unlike the two above it is applied by hand rather than by a +provider, because by definition no Terraform run will ever touch the resource. + +So the absence of any of those values is the signal that nothing manages a +resource. This script sweeps the account, reads the tag, and buckets everything +four ways. This is a **tag sweep, not a Terraform state diff**. Read [Blind spots](#blind-spots) before treating the output as an inventory. @@ -58,9 +63,17 @@ under-reports. positives. Currently: IAM groups (AWS exposes no group tagging API at all), AWS-managed KMS keys, AWS service-linked IAM roles, and the AWS-owned `FARGATE` / `FARGATE_SPOT` ECS capacity providers. +- **`exempt`** — carrying `managed-by = exempt`, and likewise excluded from the + ratio. These are resources Terraform deliberately does not manage, so counting + them as unmanaged would make them permanent false positives in the same way + untaggable ones would. The clearest case is `hackforla/devops-security`'s own + CI identity and state backend: Terraform managing the credentials and the + bucket it unlocks is a lockout risk. Note the difference from `untaggable` — + that bucket is a fact about AWS, this one is an assertion someone made by + hand, and nothing here checks it. - **Tagged with an unrecognised `managed-by` value** — appears only when - something stamped a provenance tag that is neither repo's. Worth investigating - when it shows up. + something stamped a provenance tag that is neither repo's and is not `exempt`. + Worth investigating when it shows up. ### Why it does not just use the Resource Groups Tagging API diff --git a/scripts/aws-terraform-coverage.ps1 b/scripts/aws-terraform-coverage.ps1 index 4e81bff..0bd97b7 100644 --- a/scripts/aws-terraform-coverage.ps1 +++ b/scripts/aws-terraform-coverage.ps1 @@ -22,7 +22,9 @@ collectors missed. Resource types that can never carry a tag are reported as their own category - so they do not read as unmanaged forever. + so they do not read as unmanaged forever. Resources deliberately kept out of + Terraform carry `managed-by = exempt`, applied by hand rather than by any + provider, and are excluded from the ratio for the same reason. Read the "Blind spots" section of README.md before treating the output as a complete inventory. @@ -71,6 +73,7 @@ $ErrorActionPreference = 'Stop' $script:TagKey = 'managed-by' $script:IncubatorValue = 'terraform-incubator' $script:DevOpsSecurityValue = 'terraform-devops-security' +$script:ExemptValue = 'exempt' $script:ReadOnlyVerbPattern = '^(describe|list|get)-' # --------------------------------------------------------------------------- @@ -215,6 +218,7 @@ function Get-CoverageBucket { if ($ManagedBy -eq $script:IncubatorValue) { return $script:IncubatorValue } if ($ManagedBy -eq $script:DevOpsSecurityValue) { return $script:DevOpsSecurityValue } + if ($ManagedBy -eq $script:ExemptValue) { return $script:ExemptValue } return 'unmanaged' } @@ -854,8 +858,12 @@ function Write-Section { function Write-CoverageReport { $all = @($script:Resources) - $taggable = @($all | Where-Object { $_.Bucket -ne 'untaggable' }) $untaggable = @($all | Where-Object { $_.Bucket -eq 'untaggable' }) + $exempt = @($all | Where-Object { $_.Bucket -eq $script:ExemptValue }) + # Exempt resources are deliberately outside Terraform, so counting them as + # unmanaged would make them permanent false positives - the same argument that + # keeps untaggable out of the ratio. Both are excluded from it here. + $taggable = @($all | Where-Object { $_.Bucket -ne 'untaggable' -and $_.Bucket -ne $script:ExemptValue }) $incubator = @($taggable | Where-Object { $_.Bucket -eq $script:IncubatorValue }) $security = @($taggable | Where-Object { $_.Bucket -eq $script:DevOpsSecurityValue }) $unmanaged = @($taggable | Where-Object { $_.Bucket -eq 'unmanaged' }) @@ -870,10 +878,12 @@ function Write-CoverageReport { [pscustomobject]@{ Bucket = $script:IncubatorValue; Resources = $incubator.Count } [pscustomobject]@{ Bucket = $script:DevOpsSecurityValue; Resources = $security.Count } [pscustomobject]@{ Bucket = 'unmanaged'; Resources = $unmanaged.Count } + [pscustomobject]@{ Bucket = $script:ExemptValue; Resources = $exempt.Count } ) | Format-Table -AutoSize | Out-String | Write-Host - Write-Host (" {0} of {1} taggable resources carry a managed-by tag ({2}%)." -f $managed, $total, $percent) + Write-Host (" {0} of {1} in-scope resources carry a managed-by tag ({2}%)." -f $managed, $total, $percent) Write-Host (" {0} further resources cannot be tagged at all and are excluded from that ratio." -f $untaggable.Count) + Write-Host (" {0} are tagged managed-by=exempt and are excluded from it as well." -f $exempt.Count) # A managed-by value that is neither repo's means something stamped a # provenance tag we do not recognise, which is worth surfacing on its own. @@ -893,6 +903,10 @@ function Write-CoverageReport { Format-Table -AutoSize | Out-String | Write-Host } + Write-Section 'Exempt (deliberately outside Terraform, not counted as unmanaged)' + if ($exempt.Count -eq 0) { Write-Host ' none' } + else { $exempt | Sort-Object Service, Type, Arn | ForEach-Object { Write-Host " $($_.Arn)" } } + Write-Section 'Untaggable (reported separately, not counted as unmanaged)' if ($untaggable.Count -eq 0) { Write-Host ' none' } else { @@ -923,6 +937,9 @@ function Write-CoverageReport { 'still read as unmanaged until some later apply touches it.'), @('This says nothing about the reverse direction: a resource in Terraform', 'state that no longer exists in AWS.'), + @('managed-by=exempt is applied by hand, so it records an intention rather', + 'than a fact. Nothing here verifies that an exempt resource is genuinely', + 'one Terraform should not manage.'), @('ECS task definitions are reported as the current revision per family,', 'not as every historical revision.') )