Quick take — Enable Amazon Inspector v2 with Terraform: turn on EC2, ECR, Lambda, and Lambda code scanning per account, wire up delegated-admin org auto-enable, and export the activation state for downstream guardrails. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.
Quickstart (copy-paste)
Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):
provider "aws" {
region = "us-east-1"
}
module "inspector" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-inspector?ref=v1.0.0"
# (no required inputs — all have sensible defaults)
}
Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.
What this module is
Amazon Inspector (the v2 service, exposed in Terraform through the inspector2 resources) is AWS’s continuous vulnerability-management scanner. Once enabled, it automatically discovers EC2 instances, container images in Amazon ECR, and Lambda functions, then scans them against the AWS and third-party CVE databases without you ever scheduling a job. Findings flow into the Inspector console, Security Hub, and EventBridge with a calculated Inspector risk score so you can triage by exploitability rather than raw CVSS.
The catch is that enabling Inspector is not a single boolean. You enable it per account, per scan type — EC2, ECR, LAMBDA, and LAMBDA_CODE are independent toggles, and LAMBDA_CODE (code-level scanning of function source) is a strict superset that bills separately from the standard LAMBDA package scan. In an organization you also have to designate a delegated administrator and set org-wide auto-enable defaults so new accounts get scanned the moment they join. Doing this by hand in the console is exactly the kind of drift-prone, click-heavy work that gets forgotten in one region and leaves a blind spot.
This module wraps aws_inspector2_enabler (plus the org delegated-admin and auto-enable resources) behind a small, var-driven interface. You pass a list of account IDs and the scan types you want, and you get back the activation state as outputs that downstream stacks — Security Hub aggregation, EventBridge finding routers, compliance dashboards — can depend on.
When to use it
- You run a multi-account AWS Organization and need every member account scanned consistently, with new accounts auto-enrolled rather than manually onboarded.
- You ship containers to Amazon ECR and want push-time and continuous re-scanning of images as new CVEs are published, instead of bolting on a third-party scanner.
- You have Lambda-heavy workloads and want both dependency scanning (
LAMBDA) and source-code scanning (LAMBDA_CODE) under one control. - Your compliance regime (PCI-DSS, SOC 2, FedRAMP) requires demonstrable, continuous vulnerability management and you need the enablement itself captured in code and version control.
- You want Inspector state to be a typed dependency for other Terraform — e.g. only create a Security Hub finding aggregator once Inspector is confirmed enabled.
If you only need a one-off scan of a single account in a single region, the console is faster. This module earns its keep at organization scale.
Module structure
terraform-module-aws-inspector/
├── versions.tf # provider + Terraform version pins
├── main.tf # delegated admin, org auto-enable, per-account enabler
├── variables.tf # account IDs, scan types, org toggles, validations
└── outputs.tf # enabler id, enabled types, delegated admin status
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
main.tf
locals {
# The enabler resource takes account IDs as a set. When enabling the
# current account only, callers can leave account_ids empty and we fall
# back to the caller identity.
account_ids = length(var.account_ids) > 0 ? var.account_ids : [data.aws_caller_identity.current.account_id]
}
data "aws_caller_identity" "current" {}
# ---------------------------------------------------------------------------
# Organization delegated administrator (run from the management account).
# Inspector requires a delegated admin before org auto-enable can be set.
# ---------------------------------------------------------------------------
resource "aws_inspector2_delegated_admin_account" "this" {
count = var.delegate_admin && var.delegated_admin_account_id != null ? 1 : 0
account_id = var.delegated_admin_account_id
}
# ---------------------------------------------------------------------------
# Organization-wide auto-enable defaults. Applied from the delegated admin
# account so that NEW member accounts are scanned automatically on join.
# ---------------------------------------------------------------------------
resource "aws_inspector2_organization_configuration" "this" {
count = var.configure_org_auto_enable ? 1 : 0
auto_enable {
ec2 = contains(var.resource_types, "EC2")
ecr = contains(var.resource_types, "ECR")
lambda = contains(var.resource_types, "LAMBDA")
lambda_code = contains(var.resource_types, "LAMBDA_CODE")
}
# Auto-enable cannot be configured until the delegated admin exists.
depends_on = [aws_inspector2_delegated_admin_account.this]
}
# ---------------------------------------------------------------------------
# Per-account activation of the chosen scan types. This is the core toggle:
# enable EC2 / ECR / LAMBDA / LAMBDA_CODE for the supplied account IDs.
# ---------------------------------------------------------------------------
resource "aws_inspector2_enabler" "this" {
account_ids = local.account_ids
resource_types = var.resource_types
depends_on = [aws_inspector2_delegated_admin_account.this]
}
variables.tf
variable "account_ids" {
description = "AWS account IDs to enable Inspector for. Leave empty to enable only the current (caller) account."
type = list(string)
default = []
validation {
condition = alltrue([for id in var.account_ids : can(regex("^[0-9]{12}$", id))])
error_message = "Each account_id must be a 12-digit AWS account ID."
}
}
variable "resource_types" {
description = "Inspector scan types to enable. Allowed: EC2, ECR, LAMBDA, LAMBDA_CODE. LAMBDA_CODE bills separately from LAMBDA."
type = list(string)
default = ["EC2", "ECR"]
validation {
condition = length(var.resource_types) > 0
error_message = "At least one resource type must be specified."
}
validation {
condition = alltrue([
for t in var.resource_types : contains(["EC2", "ECR", "LAMBDA", "LAMBDA_CODE"], t)
])
error_message = "resource_types may only contain EC2, ECR, LAMBDA, or LAMBDA_CODE."
}
validation {
# LAMBDA_CODE scanning is meaningless without standard LAMBDA scanning.
condition = !contains(var.resource_types, "LAMBDA_CODE") || contains(var.resource_types, "LAMBDA")
error_message = "LAMBDA_CODE requires LAMBDA to also be enabled."
}
}
variable "delegate_admin" {
description = "Whether to register a delegated administrator account for Inspector. Run from the Organization management account."
type = bool
default = false
}
variable "delegated_admin_account_id" {
description = "Account ID to register as the Inspector delegated administrator. Required when delegate_admin is true."
type = string
default = null
validation {
condition = var.delegated_admin_account_id == null || can(regex("^[0-9]{12}$", var.delegated_admin_account_id))
error_message = "delegated_admin_account_id must be a 12-digit AWS account ID or null."
}
}
variable "configure_org_auto_enable" {
description = "Whether to set organization-wide auto-enable defaults so new member accounts are scanned on join. Run from the delegated admin account."
type = bool
default = false
}
outputs.tf
output "enabler_id" {
description = "Identifier of the Inspector enabler resource (comma-separated account IDs and resource types)."
value = aws_inspector2_enabler.this.id
}
output "enabled_account_ids" {
description = "Account IDs for which Inspector scan types were enabled."
value = aws_inspector2_enabler.this.account_ids
}
output "enabled_resource_types" {
description = "Inspector scan types that were enabled (EC2 / ECR / LAMBDA / LAMBDA_CODE)."
value = aws_inspector2_enabler.this.resource_types
}
output "delegated_admin_account_id" {
description = "The registered Inspector delegated administrator account ID, or null if not configured."
value = try(aws_inspector2_delegated_admin_account.this[0].account_id, null)
}
output "org_auto_enable_configured" {
description = "True when organization-wide auto-enable defaults were applied by this module."
value = var.configure_org_auto_enable
}
How to use it
A realistic delegated-admin invocation: the security-tooling account enables EC2, ECR, and full Lambda (package + code) scanning, registers itself as the org delegated admin, and turns on auto-enable so every future member account is covered.
module "inspector" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-inspector?ref=v1.0.0"
account_ids = ["111122223333"] # security-tooling account
resource_types = ["EC2", "ECR", "LAMBDA", "LAMBDA_CODE"]
delegate_admin = true
delegated_admin_account_id = "111122223333"
configure_org_auto_enable = true
}
# Downstream: only stand up the EventBridge rule that routes Inspector
# findings to the security SNS topic once Inspector is actually enabled.
resource "aws_cloudwatch_event_rule" "inspector_findings" {
name = "inspector2-active-findings"
description = "Route active Inspector findings to the security team"
# Reference an output so this rule depends on Inspector being enabled.
event_pattern = jsonencode({
source = ["aws.inspector2"]
"detail-type" = ["Inspector2 Finding"]
detail = {
status = ["ACTIVE"]
}
})
tags = {
EnabledTypes = join(",", module.inspector.enabled_resource_types)
}
}
With Terragrunt
Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.
1. Root config — live/terragrunt.hcl (inherited by every module):
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
# ...s3 state bucket/container + key per path...
}
}
2. Module config — live/prod/inspector/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-inspector?ref=v1.0.0"
}
inputs = {
# (no required inputs)
}
3. Deploy one environment, or roll out all modules together:
cd live/prod/inspector && terragrunt apply # this module
terragrunt run-all apply # every module under live/prod
Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
account_ids |
list(string) |
[] |
No | AWS account IDs to enable Inspector for. Empty enables only the caller account. Each must be 12 digits. |
resource_types |
list(string) |
["EC2", "ECR"] |
No | Scan types to enable: EC2, ECR, LAMBDA, LAMBDA_CODE. LAMBDA_CODE requires LAMBDA. |
delegate_admin |
bool |
false |
No | Register a delegated administrator account. Run from the Organization management account. |
delegated_admin_account_id |
string |
null |
No | Account ID to register as delegated admin. Required when delegate_admin = true. |
configure_org_auto_enable |
bool |
false |
No | Apply org-wide auto-enable defaults so new member accounts are scanned on join. Run from the delegated admin account. |
Outputs
| Name | Description |
|---|---|
enabler_id |
Identifier of the Inspector enabler resource. |
enabled_account_ids |
Account IDs for which Inspector was enabled. |
enabled_resource_types |
Scan types enabled (EC2 / ECR / LAMBDA / LAMBDA_CODE). |
delegated_admin_account_id |
Registered delegated administrator account ID, or null. |
org_auto_enable_configured |
True when org-wide auto-enable defaults were applied. |
Enterprise scenario
A fintech running an AWS Organization with 60+ accounts needs continuous vulnerability scanning evidence for its annual PCI-DSS audit. The platform team deploys this module once from the security-tooling account: it registers that account as the Inspector delegated administrator, enables EC2, ECR, LAMBDA, and LAMBDA_CODE, and flips on org-wide auto-enable. From that point every newly vended account from the account factory is scanned automatically within minutes of creation, and the auditors are handed the Terraform state and plan output as proof that no account can slip through unmonitored.
Best practices
- Enable from the delegated admin, not ad hoc. Register one delegated administrator (typically your security-tooling account) and drive all org auto-enable from there, so member-account scanning is centrally governed and survives account churn — never let teams toggle Inspector by hand in the console.
- Treat
LAMBDA_CODEas a deliberate cost decision. StandardLAMBDApackage scanning is cheap;LAMBDA_CODEdeep code analysis bills separately and per function. Enable it for sensitive workloads, but model the spend before flipping it on org-wide. - Rationalise ECR scanning to control noise and cost. Inspector continuously re-scans pushed images; pair it with ECR lifecycle policies so stale/untagged images are expired and you’re not paying to re-scan dead artifacts or drowning in findings for images you never deploy.
- Route findings out of the console immediately. Wire Inspector into Security Hub and an EventBridge rule (as shown above) filtered on
status = ACTIVEand high Inspector risk score, so critical CVEs page the right team instead of sitting unread. - Keep enablement region-explicit and consistent. Inspector is regional — apply the module per region you operate in via provider aliases, and assert the same
resource_typeseverywhere so you don’t end up with a silent scanning gap in a secondary region. - Pin the module and provider, and review plans. Pin to a tagged module ref (
?ref=v1.0.0) andhashicorp/aws ~> 5.0; because disabling a scan type is a real security regression, always reviewterraform plandiffs onresource_typesbefore applying.