IaC AWS

Terraform Module: AWS Inspector — one-click continuous vulnerability scanning across accounts

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 typeEC2, 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

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 configlive/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 configlive/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

TerraformAWSInspectorModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading