From ec54b54a1c66f0ed6fa814ceda538f18e8453284 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 6 Apr 2026 13:50:32 -0400 Subject: [PATCH] feat: Lambda delegates EKS repos to CodeBuild + terraform-eks-deployment - app.py: add start_codebuild_build() and poll_codebuild_build() helpers - app.py: EKS deployment path (is_eks_deployment=True) now starts CodeBuild project 'eks-terragrunt-repo-creator', polls until SUCCEEDED/FAILED, and sends cfn-response accordingly; non-EKS path unchanged - deploy/main.tf: add aws_codebuild_project.eks_repo_creator resource (NO_SOURCE, uses buildspec.yml from terraform-eks-deployment) CODEBUILD_PROJECT_NAME injected into Lambda environment - deploy/variables.tf: codebuild_project_name, codebuild_role_arn, codebuild_vpc_id - deploy/terraform.tfvars: set CodeBuild project name, role ARN, VPC ID --- deploy/main.tf | 47 ++++++++++++++ deploy/terraform.tfvars | 7 +++ deploy/variables.tf | 19 ++++++ template_automation/app.py | 123 ++++++++++++++++++++++++++++++++++++- 4 files changed, 195 insertions(+), 1 deletion(-) diff --git a/deploy/main.tf b/deploy/main.tf index 5bd55d5..c83b325 100644 --- a/deploy/main.tf +++ b/deploy/main.tf @@ -58,6 +58,9 @@ module "eks_terragrunt_repo_generator" { # Census CA cert is not in the container's certifi bundle; keep false until # the image is rebuilt with the Census CA cert baked in. VERIFY_SSL = "false" + + # Name of the CodeBuild project that runs terraform-eks-deployment for EKS repos + CODEBUILD_PROJECT_NAME = var.codebuild_project_name } ) } @@ -65,6 +68,50 @@ module "eks_terragrunt_repo_generator" { tags = var.tags } +# ── CodeBuild project: EKS repo creator ────────────────────────────────────── +# This project is triggered by the Lambda and runs terraform-eks-deployment +# (tf init + tf apply) to create the EKS cluster GitHub repository. + +# Inline the buildspec from terraform-eks-deployment so both repos share the +# same build steps without requiring a separate S3 upload. +locals { + repo_creator_buildspec = file("${path.module}/../../terraform-eks-deployment/buildspec.yml") +} + +resource "aws_codebuild_project" "eks_repo_creator" { + name = var.codebuild_project_name + description = "Runs terraform-eks-deployment to create EKS cluster repos on GitHub Enterprise" + build_timeout = 15 + service_role = var.codebuild_role_arn + + source { + type = "NO_SOURCE" + buildspec = local.repo_creator_buildspec + } + + environment { + compute_type = "BUILD_GENERAL1_SMALL" + image = "aws/codebuild/amazonlinux2-x86_64-standard:3.0" + type = "LINUX_CONTAINER" + privileged_mode = false + } + + dynamic "vpc_config" { + for_each = var.enable_vpc ? [1] : [] + content { + vpc_id = var.codebuild_vpc_id + subnets = var.subnet_ids + security_group_ids = var.security_group_ids + } + } + + artifacts { + type = "NO_ARTIFACTS" + } + + tags = var.tags +} + # Outputs output "lambda_function_arn" { description = "ARN of the deployed Lambda function - use this as ServiceToken in CloudFormation" diff --git a/deploy/terraform.tfvars b/deploy/terraform.tfvars index 6c265e0..fbf8a4b 100644 --- a/deploy/terraform.tfvars +++ b/deploy/terraform.tfvars @@ -32,6 +32,13 @@ image_tag = "latest" enable_vpc = true subnet_ids = ["subnet-0b1992a84536c581b"] security_group_ids = ["sg-0641c697588b9aa6b"] +codebuild_vpc_id = "vpc-00576a396ec570b94" + +# ── CodeBuild: EKS repo creator ─────────────────────────────────────────── +# CodeBuild project triggered by the Lambda to run terraform-eks-deployment. +# Reuses the existing CodeBuild packer role (has S3 + VPC + CloudWatch perms). +codebuild_project_name = "eks-terragrunt-repo-creator" +codebuild_role_arn = "arn:aws-us-gov:iam::229685449397:role/CodeBuildPackerRole-eks-terragrunt-repo-generator-builder" # ── Tags ───────────────────────────────────────────────────────────────── tags = { diff --git a/deploy/variables.tf b/deploy/variables.tf index 34f015f..cbad6c4 100644 --- a/deploy/variables.tf +++ b/deploy/variables.tf @@ -61,6 +61,25 @@ variable "additional_env_vars" { default = {} } +# ───────────────────────────────────────────────────────────────────────────── +variable "codebuild_project_name" { + description = "Name of the CodeBuild project that the Lambda triggers to run terraform-eks-deployment" + type = string + default = "eks-terragrunt-repo-creator" +} + +variable "codebuild_role_arn" { + description = "IAM role ARN for the CodeBuild repo-creator project" + type = string + default = "arn:aws-us-gov:iam::229685449397:role/CodeBuildPackerRole-eks-terragrunt-repo-generator-builder" +} + +variable "codebuild_vpc_id" { + description = "VPC ID for CodeBuild project (required when enable_vpc = true)" + type = string + default = null +} + variable "tags" { description = "Tags to apply to all resources" type = map(string) diff --git a/template_automation/app.py b/template_automation/app.py index a63faf3..5b1ee1d 100644 --- a/template_automation/app.py +++ b/template_automation/app.py @@ -509,6 +509,94 @@ def _mapper(path: str) -> Optional[str]: ) return _mapper +def start_codebuild_build(cfn_input: "CloudFormationResourceInput", github_token: str, request_id: str) -> str: + """Start a CodeBuild build that runs terraform-eks-deployment to create the EKS cluster repo. + + All Terraform input variables are passed as environment variable overrides + (TF_VAR_* convention) so the build requires no pre-written tfvars file. + The GitHub provider is configured via GITHUB_TOKEN / GITHUB_OWNER / + GITHUB_BASE_URL environment variables recognised by integrations/github. + + Returns the CodeBuild build ID. + """ + project_name = os.environ.get("CODEBUILD_PROJECT_NAME", "eks-terragrunt-repo-creator") + region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-gov-west-1")) + cb = boto3.client("codebuild", region_name=region) + + cluster_config_json = json.dumps({ + "account_name": cfn_input.account_name or "", + "aws_account_id": cfn_input.aws_account_id or "", + "environment_abbr": cfn_input.environment_abbr or (cfn_input.environment or "dev"), + "vpc_name": cfn_input.vpc_name or "", + "vpc_domain_name": cfn_input.vpc_domain_name or "", + "cluster_mailing_list": cfn_input.cluster_mailing_list or "", + "organization": cfn_input.organization_path or "census:ocio:csvd", + }) + + finops_json = json.dumps({ + "project_name": cfn_input.finops_project_name or "", + "project_number": cfn_input.finops_project_number or "", + "project_role": "", + }) + + # The Terraform integrations/github provider reads GITHUB_BASE_URL as the + # GitHub Enterprise *web* URL (e.g. https://github.e.it.census.gov). + github_api_url = os.environ.get("GITHUB_API", "https://github.e.it.census.gov/api/v3/") + github_base_url = github_api_url.rstrip("/").removesuffix("/api/v3") + github_owner = os.environ.get("GITHUB_ORG_NAME", "SCT-Engineering") + + env_overrides = [ + {"name": "TF_VAR_name", "value": cfn_input.project_name, "type": "PLAINTEXT"}, + {"name": "TF_VAR_environment", "value": cfn_input.environment or "dev", "type": "PLAINTEXT"}, + {"name": "TF_VAR_region", "value": cfn_input.aws_region or region, "type": "PLAINTEXT"}, + {"name": "TF_VAR_cluster_config", "value": cluster_config_json, "type": "PLAINTEXT"}, + {"name": "TF_VAR_finops", "value": finops_json, "type": "PLAINTEXT"}, + {"name": "GITHUB_TOKEN", "value": github_token, "type": "PLAINTEXT"}, + {"name": "GITHUB_OWNER", "value": github_owner, "type": "PLAINTEXT"}, + {"name": "GITHUB_BASE_URL", "value": github_base_url, "type": "PLAINTEXT"}, + ] + + logger.info(f"[{request_id}] Starting CodeBuild project '{project_name}' for EKS repo creation") + logger.info(f"[{request_id}] TF vars: name={cfn_input.project_name}, env={cfn_input.environment}, " + f"region={cfn_input.aws_region}, vpc={cfn_input.vpc_name}") + + response = cb.start_build( + projectName=project_name, + environmentVariablesOverride=env_overrides, + ) + build_id = response["build"]["id"] + logger.info(f"[{request_id}] CodeBuild build started: {build_id}") + return build_id + + +def poll_codebuild_build(build_id: str, request_id: str, timeout_minutes: int = 12) -> tuple: + """Poll a CodeBuild build until it completes or the Lambda deadline approaches. + + Returns ``(status, logs_url)`` where status is one of: + SUCCEEDED, FAILED, FAULT, TIMED_OUT, STOPPED, or LAMBDA_TIMEOUT. + """ + region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-gov-west-1")) + cb = boto3.client("codebuild", region_name=region) + deadline = time.time() + timeout_minutes * 60 + poll_interval = 20 # seconds + + while time.time() < deadline: + response = cb.batch_get_builds(ids=[build_id]) + build = response["builds"][0] + status = build["buildStatus"] + logs_url = build.get("logs", {}).get("deepLink", "N/A") + phase = build.get("currentPhase", "UNKNOWN") + + logger.info(f"[{request_id}] CodeBuild status: {status} (phase: {phase})") + + if status != "IN_PROGRESS": + logger.info(f"[{request_id}] CodeBuild build complete: {status}. Logs: {logs_url}") + return status, logs_url + + time.sleep(poll_interval) + + logger.warning(f"[{request_id}] CodeBuild poll timed out after {timeout_minutes} minutes") + return "LAMBDA_TIMEOUT", "" def lambda_handler(event: dict, context) -> dict: """Process CloudFormation Custom Resource events to create new repositories from templates. @@ -622,7 +710,40 @@ def lambda_handler(event: dict, context) -> dict: logger.info(f"[{request_id}] Input validation successful:") logger.info(f"[{request_id}] - project_name: {cfn_input.project_name}") logger.info(f"[{request_id}] - owning_team: {cfn_input.owning_team}") - + logger.info(f"[{request_id}] - is_eks_deployment: {cfn_input.is_eks_deployment}") + + # ── EKS deployment: delegate entirely to CodeBuild + terraform-eks-deployment ── + # terraform-eks-deployment handles repo creation, template cloning, and HCL + # rendering in a single 'tf apply'. The Lambda's only job is to start the + # build, wait for it, and relay the result back to CloudFormation. + if cfn_input.is_eks_deployment: + logger.info(f"[{request_id}] EKS deployment detected – delegating to CodeBuild") + github_token = get_secret(os.environ["GITHUB_TOKEN_SECRET_NAME"]) + build_id = start_codebuild_build(cfn_input, github_token, request_id) + build_status, logs_url = poll_codebuild_build(build_id, request_id) + + if build_status == "SUCCEEDED": + github_api = os.environ.get("GITHUB_API", "https://github.e.it.census.gov/api/v3/") + repo_base = github_api.rstrip("/").removesuffix("/api/v3") + github_org = os.environ.get("GITHUB_ORG_NAME", "SCT-Engineering") + repo_url = f"{repo_base}/{github_org}/{cfn_input.project_name}" + response_data = { + "RepositoryUrl": repo_url, + "repository_url": repo_url, + "RepositoryName": cfn_input.project_name, + "repository_name": cfn_input.project_name, + "CodeBuildBuildId": build_id, + } + physical_resource_id = f"{cfn_input.project_name}-repository" + send_cfn_response(event, context, "SUCCESS", response_data, physical_resource_id) + return {"statusCode": 200, "body": json.dumps(response_data)} + else: + reason = (f"CodeBuild build {build_status}. " + f"Build ID: {build_id}. Logs: {logs_url}") + logger.error(f"[{request_id}] {reason}") + send_cfn_response(event, context, "FAILED", {}, reason=reason) + return {"statusCode": 500, "body": json.dumps({"error": reason})} + # Convert to template settings format template_settings = cfn_input.to_template_settings() logger.info(f"[{request_id}] - template_settings: {json.dumps(template_settings, default=str)}")