Skip to content

Instantly share code, notes, and snippets.

@tinovyatkin
Created May 29, 2026 11:54
Show Gist options
  • Select an option

  • Save tinovyatkin/9cc73acccff2543692ab08c5bf280d44 to your computer and use it in GitHub Desktop.

Select an option

Save tinovyatkin/9cc73acccff2543692ab08c5bf280d44 to your computer and use it in GitHub Desktop.
CDK construct which clones CodeCommit repo to an EFS FS
"""
EfsRepoSeeder — a reusable AWS CDK (v2, Python) L3 construct.
Performs a ONE-TIME `git clone` of a CodeCommit repository into a directory on
an EFS filesystem, during CloudFormation deployment.
Design (per the verified native integrations):
Custom Resource (onEvent Lambda)
└─ on Create → codebuild.start_build(...) then RETURN IMMEDIATELY.
The Lambda passes the CFN ResponseURL + the request
identity fields into the build as env-var overrides.
└─ on Update/Delete → respond SUCCESS immediately (idempotent no-op;
the clone is a one-time seed, not lifecycle-managed).
CodeBuild Project
├─ source = native Source.codeCommit(repository) (IAM auth, no creds)
├─ fileSystemLocations = native EFS mount via FileSystemLocation.efs(...)
├─ vpc / subnets / SG = same network as the EFS mount targets
└─ buildspec: copy the auto-checked-out repo into the EFS mount, then
curl a SUCCESS/FAILED response to the CFN ResponseURL.
A bash trap guarantees a response is sent even on failure,
so the stack never hangs to the 1-hour resource timeout.
Why this shape:
* CodeBuild has no 15-min limit, so a 20-min clone is fine.
* The onEvent Lambda only *starts* the build and returns, so it never
approaches Lambda's 15-min ceiling — no Provider isComplete poller needed.
* CodeBuild's managed CodeCommit checkout lands in $CODEBUILD_SRC_DIR (the
build workspace), NOT on EFS — so the buildspec copies it onto the mount.
Sources (verified):
* CodeBuild EFS mounts (FileSystemLocation.efs):
https://constructs.dev/packages/aws-cdk-lib/v/2.229.1?submodule=aws_codebuild&lang=python
* CodeBuild CodeCommit source:
same page, "CodeCommitSource" section
* CFN custom resource response contract (Status/RequestId/StackId/
LogicalResourceId/PhysicalResourceId, presigned ResponseURL):
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref.html
"""
from typing import Optional
from aws_cdk import (
CustomResource,
Duration,
Size,
aws_codebuild as codebuild,
aws_codecommit as codecommit,
aws_ec2 as ec2,
aws_efs as efs,
aws_iam as iam,
aws_lambda as lambda_,
aws_logs as logs,
custom_resources as cr,
)
from constructs import Construct
class EfsRepoSeeder(Construct):
"""One-time clone of a CodeCommit repo into a directory on an EFS filesystem."""
def __init__(
self,
scope: Construct,
construct_id: str,
*,
file_system: efs.IFileSystem,
repository: codecommit.IRepository,
vpc: ec2.IVpc,
# The access point gives the build a POSIX identity + a chrooted root on EFS.
# Strongly recommended over mounting the raw filesystem root.
access_point: efs.IAccessPoint,
# Relative path *within the access point root* to clone into (e.g. "repo").
target_subdir: str = "repo",
branch: Optional[str] = None,
# Mount point inside the CodeBuild container.
mount_point: str = "/mnt/efs",
build_timeout: Duration = Duration.minutes(30),
compute_type: codebuild.ComputeType = codebuild.ComputeType.SMALL,
) -> None:
super().__init__(scope, construct_id)
# ---- Security group shared by the build and reused for EFS ingress ----
build_sg = ec2.SecurityGroup(
self,
"BuildSg",
vpc=vpc,
description="EfsRepoSeeder CodeBuild egress",
allow_all_outbound=True,
)
# EFS needs NFS (2049) ingress from the build's SG.
file_system.connections.allow_default_port_from(
build_sg, "EfsRepoSeeder CodeBuild NFS access"
)
# EFS DNS-form location string required by FileSystemLocation.efs():
# <fs-id>.efs.<region>.amazonaws.com:/ (we mount the AP root via the
# access point's POSIX root, then clone into target_subdir beneath it).
from aws_cdk import Stack
region = Stack.of(self).region
efs_location = f"{file_system.file_system_id}.efs.{region}.amazonaws.com:/"
repo_full_path = f"{mount_point}/{target_subdir}"
clone_ref_flags = f"--branch {branch}" if branch else ""
buildspec = codebuild.BuildSpec.from_object(
{
"version": "0.2",
"phases": {
"build": {
"commands": [
# Guarantee we ALWAYS signal CFN, even on failure.
'echo "Starting EFS repo seed"',
'STATUS="FAILED"',
'REASON="Build failed before completion. See CodeBuild logs: $CODEBUILD_BUILD_ARN"',
"trap 'send_response' EXIT",
"send_response() {"
' PAYLOAD=$(printf \'{"Status":"%s","Reason":"%s",'
'"PhysicalResourceId":"%s","StackId":"%s",'
'"RequestId":"%s","LogicalResourceId":"%s","Data":{}}\''
' "$STATUS" "$REASON" "$PHYS_ID" "$CFN_STACK_ID"'
' "$CFN_REQUEST_ID" "$CFN_LOGICAL_ID");'
" curl -sS -X PUT -H 'Content-Type:' "
' -d "$PAYLOAD" "$CFN_RESPONSE_URL" || true;'
"}",
# If this isn't a Create (no response URL passed), skip.
'if [ -z "$CFN_RESPONSE_URL" ]; then'
' echo "No CFN_RESPONSE_URL; nothing to do."; STATUS="SUCCESS";'
' REASON="noop"; exit 0;'
"fi",
f'PHYS_ID="efs-repo-seed-{construct_id}"',
# Idempotency: if already seeded, succeed without recloning.
f'if [ -d "{repo_full_path}/.git" ]; then'
f' echo "Repo already present at {repo_full_path}; skipping clone.";'
' STATUS="SUCCESS"; REASON="Already seeded"; exit 0;'
"fi",
f'mkdir -p "{repo_full_path}"',
# CodeBuild already checked the repo out into
# $CODEBUILD_SRC_DIR using IAM auth. Copy it onto EFS
# (dotglob so .git and dotfiles come along).
"shopt -s dotglob",
f'cp -a "$CODEBUILD_SRC_DIR/." "{repo_full_path}/"',
# Validate the copy produced a real git repo.
f'test -d "{repo_full_path}/.git"',
f'echo "Seeded repo into {repo_full_path}"',
'STATUS="SUCCESS"',
'REASON="Repository cloned to EFS successfully"',
]
}
},
}
)
project = codebuild.Project(
self,
"CloneProject",
project_name=None,
source=codebuild.Source.code_commit(
repository=repository,
branch_or_ref=branch,
),
build_spec=buildspec,
timeout=build_timeout,
vpc=vpc,
subnet_selection=ec2.SubnetSelection(
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
),
security_groups=[build_sg],
environment=codebuild.BuildEnvironment(
build_image=codebuild.LinuxBuildImage.STANDARD_7_0,
compute_type=compute_type,
privileged=False,
),
file_system_locations=[
codebuild.FileSystemLocation.efs(
identifier="efsseed",
location=efs_location,
mount_point=mount_point,
# Mount via the access point so the build writes with the
# AP's enforced POSIX uid/gid and chrooted root.
mount_options=(
"nfsvers=4.1,rsize=1048576,wsize=1048576,"
"hard,timeo=600,retrans=2"
),
)
],
)
# CodeBuild needs to read the repo (IAM auth for git) ...
repository.grant_pull(project)
# ... and to describe/mount the EFS access point.
project.role.add_to_principal_policy(
iam.PolicyStatement(
actions=[
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
"elasticfilesystem:DescribeMountTargets",
],
resources=[file_system.file_system_arn],
conditions={
"StringEquals": {
"elasticfilesystem:AccessPointArn": access_point.access_point_arn
}
},
)
)
# ---- onEvent Lambda: start the build, pass CFN identity, return now ----
on_event = lambda_.Function(
self,
"OnEvent",
runtime=lambda_.Runtime.PYTHON_3_13,
handler="index.on_event",
timeout=Duration.minutes(2),
log_retention=logs.RetentionDays.ONE_WEEK,
code=lambda_.Code.from_inline(_ON_EVENT_SRC),
environment={"PROJECT_NAME": project.project_name},
)
project.grant_start_build(on_event)
provider = cr.Provider(
self,
"Provider",
on_event_handler=on_event,
# No isComplete handler: CodeBuild itself sends the CFN response,
# so we do NOT want the Provider polling framework involved.
log_retention=logs.RetentionDays.ONE_WEEK,
)
self.resource = CustomResource(
self,
"Resource",
service_token=provider.service_token,
resource_type="Custom::EfsRepoSeed",
properties={
"RepositoryName": repository.repository_name,
"TargetSubdir": target_subdir,
"Branch": branch or "",
},
)
self.resource.node.add_dependency(file_system)
# Inline handler source for the onEvent Lambda.
# IMPORTANT: this Lambda forwards the *raw* CFN ResponseURL + identity to
# CodeBuild and returns WITHOUT sending a response itself on Create — the build
# sends it. For Update/Delete it responds SUCCESS immediately (one-time seed).
_ON_EVENT_SRC = r'''
import json
import os
import urllib.request
import boto3
cb = boto3.client("codebuild")
def _respond(event, status, reason):
body = json.dumps({
"Status": status,
"Reason": reason,
"PhysicalResourceId": event.get("PhysicalResourceId", "efs-repo-seed"),
"StackId": event["StackId"],
"RequestId": event["RequestId"],
"LogicalResourceId": event["LogicalResourceId"],
"Data": {},
}).encode("utf-8")
req = urllib.request.Request(
event["ResponseURL"], data=body, method="PUT",
headers={"Content-Type": ""},
)
urllib.request.urlopen(req)
def on_event(event, _ctx):
rtype = event["RequestType"]
print("Request:", json.dumps({k: event[k] for k in
("RequestType", "StackId", "RequestId", "LogicalResourceId")}))
# One-time seed: only Create triggers a build. Update/Delete are no-ops.
if rtype != "Create":
_respond(event, "SUCCESS", f"{rtype} is a no-op for one-time seed")
return
# Hand the CFN callback contract to CodeBuild via env-var overrides, then
# return. The build will PUT the response to ResponseURL when the clone
# finishes (success or failure), so this Lambda never waits.
cb.start_build(
projectName=os.environ["PROJECT_NAME"],
environmentVariablesOverride=[
{"name": "CFN_RESPONSE_URL", "value": event["ResponseURL"]},
{"name": "CFN_STACK_ID", "value": event["StackId"]},
{"name": "CFN_REQUEST_ID", "value": event["RequestId"]},
{"name": "CFN_LOGICAL_ID", "value": event["LogicalResourceId"]},
],
)
print("Build started; CodeBuild will signal CloudFormation on completion.")
# NOTE: deliberately no _respond() here on Create.
'''
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment