Created
May 29, 2026 11:54
-
-
Save tinovyatkin/9cc73acccff2543692ab08c5bf280d44 to your computer and use it in GitHub Desktop.
CDK construct which clones CodeCommit repo to an EFS FS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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