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
- You want private Git inside the AWS account boundary so source code never leaves your VPC/IAM perimeter and every push/pull is captured in CloudTrail.
- You are building CodePipeline / CodeBuild workflows and want the source stage to be a first-class Terraform resource that pipelines can reference by ARN and clone URL.
- You need to enforce pull-request approvals (e.g. “2 reviewers from the platform team”) declaratively via an approval-rule template instead of relying on people to add rules per-PR.
- Compliance requires customer-managed KMS encryption of repositories and an auditable trail of who can read/write each repo.
- You are standing up many repositories (per microservice, per team) and want naming, tagging, encryption and notifications to be identical and reviewable in pull requests.
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 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/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
- Encrypt with a customer-managed CMK. Pass
kms_key_idso repositories use a key you control and can audit/rotate, rather than the default AWS-managed key — and grant the key’s policy to the IAM principals (and CodePipeline/CodeBuild roles) that must clone the repo, or pushes will fail with an access-denied on the key. - Trigger pipelines with EventBridge, not polling. Set
PollForSourceChanges = "false"in the CodePipeline source action; CodeCommit emitsreferenceCreated/referenceUpdatedevents to EventBridge, which is faster and avoids per-minute polling charges. - Scope access with IAM, not shared credentials. Grant least-privilege
codecommit:GitPull/codecommit:GitPushper repository ARN, and prefergit-remote-codecommit(SigV4) or IAM SSH keys over long-lived HTTPS Git credentials. - Enforce reviews declaratively. Use
enable_approval_rulewith a non-emptyapproval_pool_membersso only the intended IAM roles can satisfy a pull request — an empty pool lets any authenticated user approve, which usually is not what compliance wants. - Name and tag consistently. Drive
repository_namefrom ateam-serviceconvention and always settags(Team, Environment, CostCenter) so repositories are discoverable and CloudTrail/Cost Explorer queries map cleanly to owners. - Treat destroy as data loss. There is no native deletion protection; protect these resources with
prevent_destroyin the calling stack (or state-level safeguards) and back up critical history, sinceterraform destroyremoves the repository and all of its commits.