IaC AWS

Terraform Module: AWS CodeBuild — One Reusable Build Project for Every Pipeline

Quick take — Build a reusable Terraform module for AWS CodeBuild with aws_codebuild_project: scoped IAM role, KMS-encrypted artifacts, CloudWatch logs, VPC builds, and caching — wired for hashicorp/aws ~> 5.0. 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 "codebuild" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-codebuild?ref=v1.0.0"

  name = "..."  # Project name; prefixes the IAM role, policy, and log gr…
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

AWS CodeBuild is a fully managed continuous-integration service that compiles source code, runs tests, and produces deployable artifacts inside ephemeral, on-demand containers. You hand it a buildspec.yml, a build environment (a managed image like aws/codebuild/amazonlinux2-x86_64-standard:5.0 plus a compute type), and a source, and it spins up a fresh container per build — you pay only for the build minutes consumed. There are no build servers to patch and no idle capacity to fund.

The aws_codebuild_project resource looks simple in a hello-world example, but a production build project is never just the project block. It needs a dedicated, least-privilege IAM service role; a CloudWatch log group with a sensible retention; encrypted artifacts; usually a source-credential or CodeStar connection to GitHub; frequently a cache (S3 or local) to avoid re-downloading dependencies every run; and often a VPC configuration so builds can reach private subnets, RDS, or an internal artifact registry. Copy-pasting all of that across ten repositories is how drift and over-permissioned roles creep in.

This module wraps aws_codebuild_project together with its IAM role, inline scoped policy, and log group into one versioned unit. You pass in the image, compute type, environment variables, and (optionally) a VPC and cache, and you get back a consistent, encrypted, observable build project with a role that can only do what CodeBuild actually needs — logs, artifact bucket access, and ECR pulls — and nothing more.

When to use it

Reach for this module when you want standardized CI build projects across many repositories without re-deriving the IAM role and logging boilerplate each time.

Skip it for trivial scripts that GitHub Actions or a Lambda can run more cheaply, and skip it if you genuinely need a long-lived, stateful build host — CodeBuild containers are ephemeral by design.

Module structure

terraform-module-aws-codebuild/
├── versions.tf      # provider + Terraform version pins
├── main.tf          # IAM role, policy, log group, aws_codebuild_project
├── variables.tf     # var-driven inputs with validation
└── outputs.tf       # project id/arn/name, role arn, log group
# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
# main.tf
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

locals {
  log_group_name = "/aws/codebuild/${var.name}"

  # Default to the AWS-managed CodeBuild CMK alias unless the caller pins a key.
  encryption_key = var.encryption_key_arn != null ? var.encryption_key_arn : "alias/aws/s3"

  tags = merge(
    {
      Name      = var.name
      ManagedBy = "terraform"
      Module    = "terraform-module-aws-codebuild"
    },
    var.tags,
  )
}

# ---------------------------------------------------------------------------
# IAM service role assumed by CodeBuild
# ---------------------------------------------------------------------------
data "aws_iam_policy_document" "assume" {
  statement {
    sid     = "CodeBuildAssume"
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["codebuild.amazonaws.com"]
    }

    # Confused-deputy protection: only THIS account's build projects may assume the role.
    condition {
      test     = "StringEquals"
      variable = "aws:SourceAccount"
      values   = [data.aws_caller_identity.current.account_id]
    }
  }
}

resource "aws_iam_role" "this" {
  name                 = "${var.name}-codebuild-role"
  assume_role_policy   = data.aws_iam_policy_document.assume.json
  permissions_boundary = var.permissions_boundary_arn
  tags                 = local.tags
}

# Least-privilege inline policy: logs, the artifact/cache bucket, report groups,
# ECR pulls, and (when in a VPC) the ENI lifecycle CodeBuild manages on your behalf.
data "aws_iam_policy_document" "permissions" {
  statement {
    sid    = "CloudWatchLogs"
    effect = "Allow"
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]
    resources = [
      "${aws_cloudwatch_log_group.this.arn}:*",
    ]
  }

  statement {
    sid    = "ReportGroups"
    effect = "Allow"
    actions = [
      "codebuild:CreateReportGroup",
      "codebuild:CreateReport",
      "codebuild:UpdateReport",
      "codebuild:BatchPutTestCases",
      "codebuild:BatchPutCodeCoverages",
    ]
    resources = [
      "arn:aws:codebuild:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:report-group/${var.name}-*",
    ]
  }

  statement {
    sid    = "EcrPull"
    effect = "Allow"
    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
    ]
    resources = ["*"]
  }

  # Only emitted when the caller passes an artifact / cache bucket to scope to.
  dynamic "statement" {
    for_each = var.artifact_bucket_arn != null ? [var.artifact_bucket_arn] : []
    content {
      sid    = "ArtifactBucket"
      effect = "Allow"
      actions = [
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:PutObject",
        "s3:GetBucketLocation",
      ]
      resources = [
        statement.value,
        "${statement.value}/*",
      ]
    }
  }

  # ENI management is required for VPC-attached builds.
  dynamic "statement" {
    for_each = var.vpc_config != null ? [1] : []
    content {
      sid    = "VpcEni"
      effect = "Allow"
      actions = [
        "ec2:CreateNetworkInterface",
        "ec2:DescribeNetworkInterfaces",
        "ec2:DeleteNetworkInterface",
        "ec2:DescribeSubnets",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeDhcpOptions",
        "ec2:DescribeVpcs",
      ]
      resources = ["*"]
    }
  }

  dynamic "statement" {
    for_each = var.vpc_config != null ? [1] : []
    content {
      sid       = "VpcEniAttach"
      effect    = "Allow"
      actions   = ["ec2:CreateNetworkInterfacePermission"]
      resources = ["arn:aws:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:network-interface/*"]

      condition {
        test     = "StringEquals"
        variable = "ec2:AuthorizedService"
        values   = ["codebuild.amazonaws.com"]
      }
    }
  }
}

resource "aws_iam_role_policy" "this" {
  name   = "${var.name}-codebuild-permissions"
  role   = aws_iam_role.this.id
  policy = data.aws_iam_policy_document.permissions.json
}

# Optional extra customer-managed policies (e.g. CodeArtifact, Secrets Manager read).
resource "aws_iam_role_policy_attachment" "extra" {
  for_each   = toset(var.additional_policy_arns)
  role       = aws_iam_role.this.id
  policy_arn = each.value
}

# ---------------------------------------------------------------------------
# CloudWatch log group (created up front so we can scope the role to it)
# ---------------------------------------------------------------------------
resource "aws_cloudwatch_log_group" "this" {
  name              = local.log_group_name
  retention_in_days = var.log_retention_in_days
  kms_key_id        = var.log_kms_key_arn
  tags              = local.tags
}

# ---------------------------------------------------------------------------
# The build project
# ---------------------------------------------------------------------------
resource "aws_codebuild_project" "this" {
  name           = var.name
  description    = var.description
  service_role   = aws_iam_role.this.arn
  build_timeout  = var.build_timeout
  queued_timeout = var.queued_timeout
  encryption_key = var.encryption_key_arn

  artifacts {
    type = var.artifacts_type
  }

  environment {
    compute_type                = var.compute_type
    image                       = var.image
    type                        = var.environment_type
    image_pull_credentials_type = var.image_pull_credentials_type
    privileged_mode             = var.privileged_mode

    dynamic "environment_variable" {
      for_each = var.environment_variables
      content {
        name  = environment_variable.value.name
        value = environment_variable.value.value
        type  = environment_variable.value.type
      }
    }
  }

  source {
    type            = var.source_type
    location        = var.source_location
    git_clone_depth = var.git_clone_depth
    buildspec       = var.buildspec

    dynamic "git_submodules_config" {
      for_each = var.fetch_submodules ? [1] : []
      content {
        fetch_submodules = true
      }
    }
  }

  source_version = var.source_version

  # S3 or LOCAL caching to skip re-downloading dependencies each run.
  dynamic "cache" {
    for_each = var.cache != null ? [var.cache] : []
    content {
      type     = cache.value.type
      location = cache.value.location
      modes    = cache.value.modes
    }
  }

  # Attach the build to a VPC when the caller needs private network access.
  dynamic "vpc_config" {
    for_each = var.vpc_config != null ? [var.vpc_config] : []
    content {
      vpc_id             = vpc_config.value.vpc_id
      subnets            = vpc_config.value.subnets
      security_group_ids = vpc_config.value.security_group_ids
    }
  }

  logs_config {
    cloudwatch_logs {
      status     = "ENABLED"
      group_name = aws_cloudwatch_log_group.this.name
    }
  }

  tags = local.tags

  depends_on = [aws_iam_role_policy.this]
}
# variables.tf
variable "name" {
  description = "Name of the CodeBuild project; also used as a prefix for the IAM role, policy, and log group."
  type        = string

  validation {
    condition     = can(regex("^[A-Za-z0-9][A-Za-z0-9_-]{1,254}$", var.name))
    error_message = "name must be 2-255 chars: letters, digits, underscores, and hyphens only."
  }
}

variable "description" {
  description = "Human-readable description shown in the CodeBuild console."
  type        = string
  default     = "Managed by Terraform"
}

variable "compute_type" {
  description = "CodeBuild compute size."
  type        = string
  default     = "BUILD_GENERAL1_SMALL"

  validation {
    condition = contains([
      "BUILD_GENERAL1_SMALL",
      "BUILD_GENERAL1_MEDIUM",
      "BUILD_GENERAL1_LARGE",
      "BUILD_GENERAL1_XLARGE",
      "BUILD_GENERAL1_2XLARGE",
      "BUILD_LAMBDA_1GB",
      "BUILD_LAMBDA_2GB",
      "BUILD_LAMBDA_4GB",
      "BUILD_LAMBDA_8GB",
      "BUILD_LAMBDA_10GB",
    ], var.compute_type)
    error_message = "compute_type must be a valid BUILD_GENERAL1_* or BUILD_LAMBDA_* size."
  }
}

variable "image" {
  description = "Docker image for the build environment, e.g. aws/codebuild/amazonlinux2-x86_64-standard:5.0 or an ECR image URI."
  type        = string
  default     = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
}

variable "environment_type" {
  description = "Build environment type."
  type        = string
  default     = "LINUX_CONTAINER"

  validation {
    condition = contains([
      "LINUX_CONTAINER",
      "LINUX_GPU_CONTAINER",
      "ARM_CONTAINER",
      "WINDOWS_SERVER_2019_CONTAINER",
      "LINUX_LAMBDA_CONTAINER",
      "ARM_LAMBDA_CONTAINER",
    ], var.environment_type)
    error_message = "environment_type must be one of the supported CodeBuild environment types."
  }
}

variable "image_pull_credentials_type" {
  description = "How CodeBuild authenticates to pull the build image: CODEBUILD (AWS-managed) or SERVICE_ROLE (private ECR)."
  type        = string
  default     = "CODEBUILD"

  validation {
    condition     = contains(["CODEBUILD", "SERVICE_ROLE"], var.image_pull_credentials_type)
    error_message = "image_pull_credentials_type must be CODEBUILD or SERVICE_ROLE."
  }
}

variable "privileged_mode" {
  description = "Set true to run the Docker daemon inside the build (required for docker build / docker-in-docker)."
  type        = bool
  default     = false
}

variable "environment_variables" {
  description = "Environment variables exposed to the build. Use type PARAMETER_STORE or SECRETS_MANAGER for sensitive values (value = the parameter/secret name)."
  type = list(object({
    name  = string
    value = string
    type  = optional(string, "PLAINTEXT")
  }))
  default = []

  validation {
    condition = alltrue([
      for e in var.environment_variables :
      contains(["PLAINTEXT", "PARAMETER_STORE", "SECRETS_MANAGER"], e.type)
    ])
    error_message = "Each environment variable type must be PLAINTEXT, PARAMETER_STORE, or SECRETS_MANAGER."
  }
}

variable "source_type" {
  description = "Source provider type."
  type        = string
  default     = "CODEPIPELINE"

  validation {
    condition = contains([
      "CODECOMMIT", "CODEPIPELINE", "GITHUB", "GITHUB_ENTERPRISE",
      "BITBUCKET", "S3", "NO_SOURCE",
    ], var.source_type)
    error_message = "source_type must be a supported CodeBuild source type."
  }
}

variable "source_location" {
  description = "Source location (e.g. https://github.com/org/repo.git). Leave null when source_type is CODEPIPELINE or NO_SOURCE."
  type        = string
  default     = null
}

variable "source_version" {
  description = "Source version to build (branch, tag, or commit). Null builds the default branch."
  type        = string
  default     = null
}

variable "buildspec" {
  description = "Inline buildspec YAML, or a path to a buildspec file in the source. Null uses buildspec.yml at the repo root."
  type        = string
  default     = null
}

variable "git_clone_depth" {
  description = "Git clone depth; 1 means a shallow clone. Set 0 for a full clone."
  type        = number
  default     = 1
}

variable "fetch_submodules" {
  description = "Whether to recursively fetch git submodules."
  type        = bool
  default     = false
}

variable "artifacts_type" {
  description = "Where build output goes. Use CODEPIPELINE inside a pipeline, NO_ARTIFACTS for test-only builds, or S3."
  type        = string
  default     = "CODEPIPELINE"

  validation {
    condition     = contains(["CODEPIPELINE", "NO_ARTIFACTS", "S3"], var.artifacts_type)
    error_message = "artifacts_type must be CODEPIPELINE, NO_ARTIFACTS, or S3."
  }
}

variable "build_timeout" {
  description = "Minutes before an in-progress build is forcibly stopped (5-480)."
  type        = number
  default     = 60

  validation {
    condition     = var.build_timeout >= 5 && var.build_timeout <= 480
    error_message = "build_timeout must be between 5 and 480 minutes."
  }
}

variable "queued_timeout" {
  description = "Minutes a build may sit queued before it is failed (5-480)."
  type        = number
  default     = 480

  validation {
    condition     = var.queued_timeout >= 5 && var.queued_timeout <= 480
    error_message = "queued_timeout must be between 5 and 480 minutes."
  }
}

variable "encryption_key_arn" {
  description = "KMS key ARN used to encrypt build output artifacts. Null uses the AWS-managed S3 key."
  type        = string
  default     = null
}

variable "cache" {
  description = "Optional build cache. type = S3 (location = bucket/prefix) or LOCAL (modes = LOCAL_DOCKER_LAYER_CACHE / LOCAL_SOURCE_CACHE / LOCAL_CUSTOM_CACHE)."
  type = object({
    type     = string
    location = optional(string)
    modes    = optional(list(string))
  })
  default = null

  validation {
    condition     = var.cache == null ? true : contains(["S3", "LOCAL", "NO_CACHE"], var.cache.type)
    error_message = "cache.type must be S3, LOCAL, or NO_CACHE."
  }
}

variable "vpc_config" {
  description = "Optional VPC attachment so builds can reach private resources."
  type = object({
    vpc_id             = string
    subnets            = list(string)
    security_group_ids = list(string)
  })
  default = null
}

variable "artifact_bucket_arn" {
  description = "ARN of an S3 bucket the build role may read/write (artifacts or S3 cache). Null skips the S3 policy statement."
  type        = string
  default     = null
}

variable "log_retention_in_days" {
  description = "CloudWatch log retention for build logs."
  type        = number
  default     = 30

  validation {
    condition = contains([
      0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653,
    ], var.log_retention_in_days)
    error_message = "log_retention_in_days must be a value CloudWatch Logs accepts (0 = never expire)."
  }
}

variable "log_kms_key_arn" {
  description = "Optional KMS key ARN to encrypt the CloudWatch log group."
  type        = string
  default     = null
}

variable "permissions_boundary_arn" {
  description = "Optional IAM permissions boundary applied to the build service role."
  type        = string
  default     = null
}

variable "additional_policy_arns" {
  description = "Extra IAM policy ARNs to attach to the build role (e.g. CodeArtifact read, Secrets Manager access)."
  type        = list(string)
  default     = []
}

variable "tags" {
  description = "Additional tags merged onto every resource the module creates."
  type        = map(string)
  default     = {}
}
# outputs.tf
output "id" {
  description = "The CodeBuild project ID (its name)."
  value       = aws_codebuild_project.this.id
}

output "arn" {
  description = "ARN of the CodeBuild project (use in CodePipeline / EventBridge targets)."
  value       = aws_codebuild_project.this.arn
}

output "name" {
  description = "Name of the CodeBuild project."
  value       = aws_codebuild_project.this.name
}

output "badge_url" {
  description = "Public build badge URL (empty unless badge is enabled on the project)."
  value       = aws_codebuild_project.this.badge_url
}

output "service_role_arn" {
  description = "ARN of the IAM service role CodeBuild assumes; attach extra policies here if needed."
  value       = aws_iam_role.this.arn
}

output "service_role_name" {
  description = "Name of the IAM service role."
  value       = aws_iam_role.this.name
}

output "log_group_name" {
  description = "CloudWatch log group that receives build logs."
  value       = aws_cloudwatch_log_group.this.name
}

output "log_group_arn" {
  description = "ARN of the CloudWatch log group."
  value       = aws_cloudwatch_log_group.this.arn
}

How to use it

A container-image build that runs docker build, pushes to ECR, caches Docker layers in S3, and runs as the build stage of a CodePipeline:

module "codebuild" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-codebuild?ref=v1.0.0"

  name            = "orders-api-build"
  description     = "Builds and pushes the orders-api container image"
  compute_type    = "BUILD_GENERAL1_MEDIUM"
  image           = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
  privileged_mode = true # required for docker build

  source_type    = "CODEPIPELINE"
  artifacts_type = "CODEPIPELINE"
  buildspec      = "ci/buildspec.yml"

  environment_variables = [
    { name = "AWS_DEFAULT_REGION", value = "ap-south-1" },
    { name = "ECR_REPO", value = aws_ecr_repository.orders.repository_url },
    # Pulled from Secrets Manager at build time, never stored in state as plaintext.
    { name = "NPM_TOKEN", value = "orders-api/npm-token", type = "SECRETS_MANAGER" },
  ]

  cache = {
    type     = "S3"
    location = "${aws_s3_bucket.build_cache.bucket}/orders-api"
  }

  artifact_bucket_arn   = aws_s3_bucket.build_cache.arn
  encryption_key_arn    = aws_kms_key.cicd.arn
  log_retention_in_days = 90

  # Let the build push to ECR beyond the read-only pull baked into the module.
  additional_policy_arns = [aws_iam_policy.ecr_push.arn]

  tags = {
    Team        = "payments"
    Environment = "prod"
  }
}

# Downstream: wire the project into a CodePipeline build stage using its name output.
resource "aws_codepipeline" "orders" {
  name     = "orders-api"
  role_arn = aws_iam_role.pipeline.arn

  artifact_store {
    location = aws_s3_bucket.artifacts.bucket
    type     = "S3"
  }

  # ... source stage omitted ...

  stage {
    name = "Build"
    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      input_artifacts  = ["source_output"]
      output_artifacts = ["build_output"]

      configuration = {
        ProjectName = module.codebuild.name # <-- module output
      }
    }
  }
}

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/codebuild/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-codebuild?ref=v1.0.0"
}

inputs = {
  name = "..."
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/codebuild && 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
name string Yes Project name; prefixes the IAM role, policy, and log group.
description string "Managed by Terraform" No Description shown in the console.
compute_type string "BUILD_GENERAL1_SMALL" No Compute size; validated against BUILD_GENERAL1_* and BUILD_LAMBDA_* values.
image string "aws/codebuild/amazonlinux2-x86_64-standard:5.0" No Build image (managed image or ECR URI).
environment_type string "LINUX_CONTAINER" No LINUX/ARM/Windows/GPU/Lambda container type.
image_pull_credentials_type string "CODEBUILD" No CODEBUILD or SERVICE_ROLE (private ECR images).
privileged_mode bool false No Enable Docker-in-Docker for docker build.
environment_variables list(object) [] No Build env vars; type PLAINTEXT / PARAMETER_STORE / SECRETS_MANAGER.
source_type string "CODEPIPELINE" No CODECOMMIT / CODEPIPELINE / GITHUB / BITBUCKET / S3 / NO_SOURCE.
source_location string null No Repo URL; null for CODEPIPELINE/NO_SOURCE.
source_version string null No Branch, tag, or commit to build.
buildspec string null No Inline buildspec YAML or path; null uses buildspec.yml.
git_clone_depth number 1 No Clone depth; 0 = full clone.
fetch_submodules bool false No Recursively fetch git submodules.
artifacts_type string "CODEPIPELINE" No CODEPIPELINE / NO_ARTIFACTS / S3.
build_timeout number 60 No Build timeout in minutes (5-480).
queued_timeout number 480 No Queue timeout in minutes (5-480).
encryption_key_arn string null No KMS key for artifact encryption; null = AWS-managed S3 key.
cache object null No S3 or LOCAL build cache config.
vpc_config object null No VPC attachment (vpc_id, subnets, security_group_ids).
artifact_bucket_arn string null No S3 bucket ARN the role may read/write.
log_retention_in_days number 30 No CloudWatch log retention (0 = never expire).
log_kms_key_arn string null No KMS key to encrypt the log group.
permissions_boundary_arn string null No IAM permissions boundary for the build role.
additional_policy_arns list(string) [] No Extra IAM policy ARNs for the build role.
tags map(string) {} No Extra tags merged onto all resources.

Outputs

Name Description
id CodeBuild project ID (its name).
arn Project ARN for CodePipeline / EventBridge targets.
name Project name.
badge_url Public build badge URL (empty unless badge enabled).
service_role_arn ARN of the IAM service role CodeBuild assumes.
service_role_name Name of the IAM service role.
log_group_name CloudWatch log group receiving build logs.
log_group_arn ARN of the CloudWatch log group.

Enterprise scenario

A fintech platform team runs 40+ microservices, each in its own repository with an identical CI contract: lint, test, build a container, push to ECR. They instantiate this module once per service from a for_each over a service catalog, all pinned to ?ref=v1.2.0, so every build project lands with the same KMS-encrypted artifacts, 90-day log retention, a permissions boundary that caps the build role, and Docker-layer caching in a shared S3 bucket. When the security team needs to tighten the ECR scope or bump the managed image to a patched version, they ship a new module tag and roll it out repo by repo through pull requests — no hand-edited IAM roles, and terraform plan shows exactly which projects change.

Best practices

TerraformAWSCodeBuildModuleIaC
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