Skip to content

Commit

Permalink
feat: Lambda delegates EKS repos to CodeBuild + terraform-eks-deployment
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
Your Name committed Apr 6, 2026
1 parent a79cee4 commit ec54b54
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 1 deletion.
47 changes: 47 additions & 0 deletions deploy/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,60 @@ 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
}
)
}

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"
Expand Down
7 changes: 7 additions & 0 deletions deploy/terraform.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
19 changes: 19 additions & 0 deletions deploy/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
123 changes: 122 additions & 1 deletion template_automation/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)}")
Expand Down

0 comments on commit ec54b54

Please sign in to comment.