IaC AWS

Terraform Module: AWS CodeCommit — governed Git repos with notifications and approval rules

Quick take — Provision AWS CodeCommit repositories as code with a reusable Terraform module: KMS encryption, default branch, approval-rule templates, EventBridge notifications and IAM-scoped access 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 "codecommit" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-codecommit?ref=v1.0.0"

  repository_name = "..."  # Repository name (1-100 chars; letters, numbers, `.`, `_…
}

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

What this module is

AWS CodeCommit is a fully managed, Git-compatible source-control service that hosts private repositories inside your AWS account. Because it lives behind IAM and KMS, it is a natural fit for teams that want their Git history governed by the same account boundaries, encryption keys and CloudTrail audit trail as the rest of their AWS estate — no separate SaaS seats, no outbound code egress.

The trouble with clicking “Create repository” in the console is that every repo ends up subtly different: one has a default branch of master, another main; one is encrypted with the AWS-managed key, another with a customer-managed CMK; pull-request approval rules and CodeStar notifications get bolted on by hand and drift immediately. This module wraps aws_codecommit_repository together with the two sub-resources you almost always end up wanting in production — an aws_codecommit_approval_rule_template (with its association) for enforced PR reviews, and an aws_codestarnotifications_notification_rule to push pull-request and commit events to an SNS topic or chat. Everything is var-driven, so a repository is one consistent module block instead of a dozen console fields.

A note on lifecycle: CodeCommit has no “delete protection” flag the way RDS does, and a terraform destroy will remove the repository and its entire Git history. Treating these repos as code — reviewed, tagged, reproducible — is exactly why a thin, opinionated module pays off.

When to use it

If you only need public OSS hosting, rich code review UX, or a large third-party integration marketplace, GitHub/GitLab are the better tools — CodeCommit’s strength is being the AWS-native, IAM-governed option.

Module structure

terraform-module-aws-codecommit/
├── versions.tf      # provider + Terraform version pins
├── main.tf          # repository + approval rule template + notification rule
├── variables.tf     # var-driven inputs with validation
└── outputs.tf       # id, arn, clone URLs, approval template id

versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

main.tf

locals {
  # CodeStar notifications require the full event-type IDs. Map the friendly
  # toggles in var.notification_events onto the API identifiers.
  notification_event_type_ids = [
    for e in var.notification_events :
    lookup(
      {
        pull_request_created          = "codecommit-repository-pull-request-created"
        pull_request_source_updated   = "codecommit-repository-pull-request-source-updated"
        pull_request_status_changed   = "codecommit-repository-pull-request-status-changed"
        pull_request_merged           = "codecommit-repository-pull-request-merged"
        comments_on_pull_requests     = "codecommit-repository-comments-on-pull-requests"
        comments_on_commits           = "codecommit-repository-comments-on-commits"
        branches_and_tags_created     = "codecommit-repository-branches-and-tags-created"
        branches_and_tags_deleted     = "codecommit-repository-branches-and-tags-deleted"
        approval_status_changed       = "codecommit-repository-approvals-status-changed"
        approval_rule_override        = "codecommit-repository-approvals-rule-override"
      },
      e,
      e
    )
  ]

  common_tags = merge(
    {
      ManagedBy = "terraform"
      Module    = "terraform-module-aws-codecommit"
    },
    var.tags
  )
}

resource "aws_codecommit_repository" "this" {
  repository_name = var.repository_name
  description     = var.description
  default_branch  = var.default_branch
  kms_key_id      = var.kms_key_id

  tags = local.common_tags
}

# Optional: a reusable approval-rule template that enforces a minimum number of
# pull-request approvers, optionally restricted to a set of IAM principals.
resource "aws_codecommit_approval_rule_template" "this" {
  count = var.enable_approval_rule ? 1 : 0

  name        = coalesce(var.approval_rule_template_name, "${var.repository_name}-approval")
  description = "Require ${var.approval_required_count} approval(s) for pull requests on ${var.repository_name}"

  content = jsonencode({
    Version               = "2018-11-08"
    DestinationReferences = var.approval_destination_references
    Statements = [
      {
        Type                    = "Approvers"
        NumberOfApprovalsNeeded = var.approval_required_count
        ApprovalPoolMembers     = var.approval_pool_members
      }
    ]
  })
}

resource "aws_codecommit_approval_rule_template_association" "this" {
  count = var.enable_approval_rule ? 1 : 0

  approval_rule_template_name = aws_codecommit_approval_rule_template.this[0].name
  repository_name             = aws_codecommit_repository.this.repository_name
}

# Optional: push repository events (PR opened, merged, comments, etc.) to one or
# more targets (SNS topic and/or AWS Chatbot Slack channel).
resource "aws_codestarnotifications_notification_rule" "this" {
  count = length(var.notification_targets) > 0 ? 1 : 0

  name           = "${var.repository_name}-notifications"
  resource       = aws_codecommit_repository.this.arn
  detail_type    = var.notification_detail_type
  event_type_ids = local.notification_event_type_ids
  status         = "ENABLED"

  dynamic "target" {
    for_each = var.notification_targets
    content {
      type    = target.value.type
      address = target.value.address
    }
  }

  tags = local.common_tags
}

variables.tf

variable "repository_name" {
  description = "Name of the CodeCommit repository (1-100 chars; letters, numbers, '.', '_', '-')."
  type        = string

  validation {
    condition     = can(regex("^[A-Za-z0-9._-]{1,100}$", var.repository_name))
    error_message = "repository_name must be 1-100 characters using only letters, numbers, '.', '_' or '-'."
  }
}

variable "description" {
  description = "Human-readable description of the repository (max 1000 chars)."
  type        = string
  default     = null

  validation {
    condition     = var.description == null || length(var.description) <= 1000
    error_message = "description must be 1000 characters or fewer."
  }
}

variable "default_branch" {
  description = "Default branch for the repository. Note: the branch must exist (i.e. have a commit) before this takes effect."
  type        = string
  default     = "main"
}

variable "kms_key_id" {
  description = "ARN or key ID of a customer-managed KMS key used to encrypt the repository. Leave null to use the AWS-managed CodeCommit key."
  type        = string
  default     = null
}

# ---- Approval rule template ------------------------------------------------

variable "enable_approval_rule" {
  description = "Create and associate a pull-request approval-rule template with the repository."
  type        = bool
  default     = false
}

variable "approval_rule_template_name" {
  description = "Name for the approval-rule template. Defaults to '<repository_name>-approval'."
  type        = string
  default     = null
}

variable "approval_required_count" {
  description = "Number of approvals required to satisfy the rule (1-25)."
  type        = number
  default     = 1

  validation {
    condition     = var.approval_required_count >= 1 && var.approval_required_count <= 25
    error_message = "approval_required_count must be between 1 and 25."
  }
}

variable "approval_pool_members" {
  description = "List of approver pool members (IAM ARNs or 'arn:aws:sts::<account>:assumed-role/<role>/*' patterns). Empty list means any authenticated user can approve."
  type        = list(string)
  default     = []
}

variable "approval_destination_references" {
  description = "Branch references the approval rule applies to, e.g. ['refs/heads/main']. Empty list applies the rule to all branches."
  type        = list(string)
  default     = ["refs/heads/main"]
}

# ---- Notifications ---------------------------------------------------------

variable "notification_targets" {
  description = "Targets to notify on repository events. type is 'SNS' or 'AWSChatbotSlack'; address is the SNS topic ARN or Chatbot configuration ARN."
  type = list(object({
    type    = string
    address = string
  }))
  default = []

  validation {
    condition = alltrue([
      for t in var.notification_targets : contains(["SNS", "AWSChatbotSlack"], t.type)
    ])
    error_message = "Each notification target type must be either 'SNS' or 'AWSChatbotSlack'."
  }
}

variable "notification_events" {
  description = "Friendly event names to subscribe to (e.g. pull_request_created, pull_request_merged, comments_on_pull_requests). Unknown values are passed through as raw event-type IDs."
  type        = list(string)
  default     = ["pull_request_created", "pull_request_merged", "pull_request_source_updated"]
}

variable "notification_detail_type" {
  description = "Level of detail in notifications: 'BASIC' or 'FULL'."
  type        = string
  default     = "FULL"

  validation {
    condition     = contains(["BASIC", "FULL"], var.notification_detail_type)
    error_message = "notification_detail_type must be 'BASIC' or 'FULL'."
  }
}

variable "tags" {
  description = "Additional tags to apply to the repository and notification rule."
  type        = map(string)
  default     = {}
}

outputs.tf

output "repository_id" {
  description = "The system-generated unique ID of the CodeCommit repository."
  value       = aws_codecommit_repository.this.repository_id
}

output "repository_name" {
  description = "The name of the CodeCommit repository."
  value       = aws_codecommit_repository.this.repository_name
}

output "arn" {
  description = "The ARN of the CodeCommit repository (use this as the source for CodePipeline / notification rules)."
  value       = aws_codecommit_repository.this.arn
}

output "clone_url_http" {
  description = "HTTPS clone URL (use with git-remote-codecommit or HTTPS Git credentials)."
  value       = aws_codecommit_repository.this.clone_url_http
}

output "clone_url_ssh" {
  description = "SSH clone URL (use with an IAM-uploaded SSH public key)."
  value       = aws_codecommit_repository.this.clone_url_ssh
}

output "approval_rule_template_id" {
  description = "ID of the created approval-rule template, or null if disabled."
  value       = try(aws_codecommit_approval_rule_template.this[0].approval_rule_template_id, null)
}

output "notification_rule_arn" {
  description = "ARN of the CodeStar notification rule, or null if no targets were configured."
  value       = try(aws_codestarnotifications_notification_rule.this[0].arn, null)
}

How to use it

# An SNS topic the CI/CD team subscribes to for PR activity.
resource "aws_sns_topic" "repo_events" {
  name = "platform-repo-events"
}

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

  repository_name = "orders-service"
  description     = "Source for the orders microservice; deployed via CodePipeline."
  default_branch  = "main"
  kms_key_id      = aws_kms_key.codecommit.arn

  # Enforce two reviewers from the platform team on PRs into main.
  enable_approval_rule    = true
  approval_required_count = 2
  approval_pool_members = [
    "arn:aws:sts::123456789012:assumed-role/platform-engineer/*"
  ]
  approval_destination_references = ["refs/heads/main"]

  notification_targets = [
    { type = "SNS", address = aws_sns_topic.repo_events.arn }
  ]
  notification_events = [
    "pull_request_created",
    "pull_request_source_updated",
    "pull_request_merged",
    "comments_on_pull_requests",
  ]

  tags = {
    Team        = "platform"
    Environment = "prod"
    CostCenter  = "cc-4412"
  }
}

# Downstream: wire the repository into a CodePipeline source stage using the
# module's outputs (repository_name for the source action, arn for permissions).
resource "aws_codepipeline" "orders" {
  name     = "orders-service-pipeline"
  role_arn = aws_iam_role.pipeline.arn

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

  stage {
    name = "Source"
    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeCommit"
      version          = "1"
      output_artifacts = ["source_output"]

      configuration = {
        RepositoryName = module.codecommit.repository_name
        BranchName     = "main"
        # Use EventBridge (not polling) to trigger on push.
        PollForSourceChanges = "false"
      }
    }
  }

  # ... build and deploy stages ...
}

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

include "root" {
  path = find_in_parent_folders()
}

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

inputs = {
  repository_name = "..."
}

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

cd live/prod/codecommit && 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
repository_name string n/a Yes Repository name (1-100 chars; letters, numbers, ., _, -).
description string null No Repository description (max 1000 chars).
default_branch string "main" No Default branch; must already have a commit to take effect.
kms_key_id string null No Customer-managed KMS key ARN/ID; null uses the AWS-managed key.
enable_approval_rule bool false No Create and associate a PR approval-rule template.
approval_rule_template_name string null No Approval template name; defaults to <repository_name>-approval.
approval_required_count number 1 No Number of approvals required (1-25).
approval_pool_members list(string) [] No IAM ARNs/patterns allowed to approve; empty = any authenticated user.
approval_destination_references list(string) ["refs/heads/main"] No Branch refs the rule applies to; empty = all branches.
notification_targets list(object({type, address})) [] No SNS / AWSChatbotSlack targets for event notifications.
notification_events list(string) ["pull_request_created", "pull_request_merged", "pull_request_source_updated"] No Friendly event names to subscribe to.
notification_detail_type string "FULL" No Notification detail level: BASIC or FULL.
tags map(string) {} No Extra tags for the repository and notification rule.

Outputs

Name Description
repository_id System-generated unique ID of the repository.
repository_name Name of the repository.
arn Repository ARN (source for CodePipeline and notification rules).
clone_url_http HTTPS clone URL (git-remote-codecommit or HTTPS Git credentials).
clone_url_ssh SSH clone URL (IAM-uploaded SSH public key).
approval_rule_template_id ID of the approval-rule template, or null if disabled.
notification_rule_arn ARN of the CodeStar notification rule, or null if no targets.

Enterprise scenario

A regulated fintech runs all microservice source inside AWS so code never leaves their audited account boundary. Their platform team calls this module from a for_each over a service catalog, creating ~80 CodeCommit repositories each encrypted with a per-OU customer-managed KMS key, each enforcing a two-approver rule on refs/heads/main, and each emitting pull-request events to an SNS topic that fans out to the team’s Slack via AWS Chatbot. Because the approval template and encryption are codified, an auditor can confirm from one Terraform plan that every repository meets the “minimum two reviewers, CMK-encrypted” control — and a new repo inherits the controls automatically on its first apply.

Best practices

TerraformAWSCodeCommitModuleIaC
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