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.
- Per-service build projects — one project per microservice repo, all sharing the same hardened defaults (encryption, log retention, naming convention).
- Container image builds —
privileged_mode = trueplus an ECR push policy so CodeBuild can rundocker buildand push to ECR, typically as the build stage of a CodePipeline. - Builds that need VPC access — integration tests that hit a private RDS instance, or pulling packages from a CodeArtifact/Nexus repo on private subnets.
- Polyglot monorepos — multiple build projects (one per language toolchain) defined from the same module with different images and
buildspecpaths.
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 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/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
- Keep the service role least-privilege and per-project. This module gives each project its own role scoped to its log group and report groups; resist attaching
AdministratorAccessor broads3:*. Grant ECR push, CodeArtifact, or Secrets Manager reads explicitly viaadditional_policy_arns, and apply apermissions_boundary_arnin regulated accounts. - Never put secrets in plaintext env vars. They land in the project definition and Terraform state. Use
type = "SECRETS_MANAGER"orPARAMETER_STOREso CodeBuild resolves them at build time, and reference the secret name rather than the value. - Right-size compute and cache aggressively to control cost. CodeBuild bills per build-minute by compute size — start at
BUILD_GENERAL1_SMALLand only scale up when builds are CPU/memory-bound. Enable S3 orLOCAL_DOCKER_LAYER_CACHEso dependency and layer downloads do not re-run every build; for ARM workloads,ARM_CONTAINERimages are cheaper per minute. - Set realistic timeouts and log retention. A low
build_timeoutstops runaway builds from burning minutes; a finitelog_retention_in_days(not the default “never expire”) keeps CloudWatch Logs storage costs bounded while preserving enough history to debug failures. - Use VPC builds only when you truly need private access. Attaching a VPC adds ENI provisioning time and an
ec2:*block to the role. Reservevpc_configfor builds that must reach RDS, a private CodeArtifact endpoint, or internal services, and give those builds a dedicated security group. - Standardize naming and tagging through the module. Drive
namefrom ateam-service-buildconvention and passtagsfor cost allocation; the module’s mergedManagedBy/Moduletags make it obvious in the console which projects are Terraform-owned versus click-ops.