Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ permissions:

env:
AWS_REGION: us-east-1
ECR_REPOSITORY: ad-buyer-system
ECS_CLUSTER: ad-buyer-${{ inputs.environment || 'staging' }}-cluster
ECS_SERVICE: ad-buyer-${{ inputs.environment || 'staging' }}-service
ECR_REPOSITORY: ad-buyer-system/${{ inputs.environment || 'staging' }}
ECS_CLUSTER: ad-buyer-system-${{ inputs.environment || 'staging' }}-cluster
ECS_SERVICE: ad-buyer-system-${{ inputs.environment || 'staging' }}-service

jobs:
deploy:
Expand All @@ -52,8 +52,10 @@ jobs:
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
run: |
docker build \
--secret id=github_token,env=GITHUB_TOKEN \
-f infra/docker/Dockerfile \
-t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
-t $ECR_REGISTRY/$ECR_REPOSITORY:latest \
Expand Down
115 changes: 115 additions & 0 deletions infra/aws/terraform/github_oidc.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Author: Green Mountain Systems AI Inc.
# Donated to IAB Tech Lab

# GitHub Actions OIDC provider + deploy role.
#
# This allows GitHub Actions to assume an AWS IAM role via OIDC (no static
# long-lived credentials stored in GitHub secrets). After `terraform apply`,
# copy the `github_deploy_role_arn` output into the repo's GitHub secret:
# Settings → Secrets → Actions → AWS_DEPLOY_ROLE_ARN

variable "github_repo" {
description = "GitHub repository in owner/repo format (e.g. IABTechLab/buyer-agent)"
type = string
default = "IABTechLab/buyer-agent"
}

resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

locals {
oidc_provider_arn = aws_iam_openid_connect_provider.github.arn
}

data "aws_iam_policy_document" "github_deploy_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]

principals {
type = "Federated"
identifiers = [local.oidc_provider_arn]
}

condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}

# Allow the deploy job on main or any workflow_dispatch from the repo.
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:${var.github_repo}:*"]
}
}
}

resource "aws_iam_role" "github_deploy" {
name = "ad-buyer-system-github-deploy"
assume_role_policy = data.aws_iam_policy_document.github_deploy_assume.json
description = "Assumed by GitHub Actions via OIDC to deploy buyer-agent to ECS"

tags = {
Project = "ad-buyer-system"
ManagedBy = "terraform"
}
}

data "aws_iam_policy_document" "github_deploy_permissions" {
# ECR — push images
statement {
effect = "Allow"
actions = [
"ecr:GetAuthorizationToken",
]
resources = ["*"]
}

statement {
effect = "Allow"
actions = [
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
]
resources = [module.compute.ecr_repository_arn]
}

# ECS — trigger rolling deploy + wait for stability
statement {
effect = "Allow"
actions = [
"ecs:UpdateService",
"ecs:DescribeServices",
"ecs:DescribeTasks",
"ecs:DescribeTaskDefinition",
"ecs:ListTasks",
]
resources = ["*"]
condition {
test = "ArnLike"
variable = "ecs:cluster"
values = [module.compute.ecs_cluster_arn]
}
}
}

resource "aws_iam_role_policy" "github_deploy" {
name = "ad-buyer-system-github-deploy-policy"
role = aws_iam_role.github_deploy.id
policy = data.aws_iam_policy_document.github_deploy_permissions.json
}

output "github_deploy_role_arn" {
description = "ARN to set as AWS_DEPLOY_ROLE_ARN in GitHub Actions secrets"
value = aws_iam_role.github_deploy.arn
}
10 changes: 5 additions & 5 deletions infra/aws/terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ terraform {
}

backend "s3" {
bucket = "ad-buyer-system-terraform-state"
key = "terraform.tfstate"
region = "us-east-1"
dynamodb_table = "ad-buyer-system-terraform-lock"
encrypt = true
bucket = "ad-buyer-system-terraform-state"
key = "terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}

Expand Down
10 changes: 10 additions & 0 deletions infra/aws/terraform/modules/compute/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@ output "ecr_repository_url" {
value = aws_ecr_repository.this.repository_url
}

output "ecr_repository_arn" {
description = "ARN of the ECR repository (used by the GitHub deploy IAM role)"
value = aws_ecr_repository.this.arn
}

output "ecs_cluster_name" {
description = "Name of the ECS cluster"
value = aws_ecs_cluster.this.name
}

output "ecs_cluster_arn" {
description = "ARN of the ECS cluster (used by the GitHub deploy IAM role)"
value = aws_ecs_cluster.this.arn
}

output "ecs_service_name" {
description = "Name of the ECS service"
value = aws_ecs_service.this.name
Expand Down
5 changes: 5 additions & 0 deletions infra/aws/terraform/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ output "ecs_cluster_name" {
value = module.compute.ecs_cluster_name
}

output "ecs_cluster_arn" {
description = "ARN of the ECS cluster"
value = module.compute.ecs_cluster_arn
}

output "ecs_service_name" {
description = "Name of the ECS service"
value = module.compute.ecs_service_name
Expand Down
37 changes: 26 additions & 11 deletions infra/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
# =============================================================================
# Stage 1: Build dependencies (cached layer)
# Stage 2: Production runtime (minimal image)
#
# Private git deps (iab-agentic-primitives) require a GitHub token.
# Pass it at build time via BuildKit secret — never baked into any layer:
#
# docker build \
# --secret id=github_token,env=GITHUB_TOKEN \
# -f infra/docker/Dockerfile -t buyer-agent:local .
#
# In GitHub Actions the GITHUB_TOKEN is passed automatically (see deploy.yml).
# =============================================================================

# ---------------------------------------------------------------------------
Expand All @@ -14,21 +23,27 @@ WORKDIR /build

RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
git \
&& rm -rf /var/lib/apt/lists/*

# Copy only dependency metadata first (cache-friendly)
COPY pyproject.toml ./

# Install dependencies into a virtual env so we can copy it cleanly
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -e . 2>/dev/null; true
RUN pip install --no-cache-dir --upgrade pip

# Copy source and install all deps.
# The GitHub token is mounted as a BuildKit secret — used only during this
# RUN step and never written to any image layer.
COPY pyproject.toml README.md ./
COPY src ./src

# Now copy source and install the package itself
COPY . .
RUN pip install --no-cache-dir -e .
RUN --mount=type=secret,id=github_token \
export GH_TOKEN=$(cat /run/secrets/github_token) && \
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="url.https://${GH_TOKEN}@github.com/.insteadOf" \
GIT_CONFIG_VALUE_0="https://github.com/" \
pip install --no-cache-dir -e .

# ---------------------------------------------------------------------------
# Stage 2 — Runtime
Expand All @@ -43,7 +58,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN groupadd --gid 1000 buyer && \
useradd --uid 1000 --gid buyer --create-home buyer

# Copy virtual env from builder
# Copy virtual env from builder (all deps already resolved, no re-clone needed)
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

Expand All @@ -53,8 +68,8 @@ COPY --from=builder /build/src ./src
COPY --from=builder /build/pyproject.toml ./
COPY --from=builder /build/README.md ./

# Install the package in the runtime venv
RUN pip install --no-cache-dir -e .
# Register the editable package — deps already in venv, skip resolution
RUN pip install --no-cache-dir --no-deps -e .

# Create data directory for SQLite (owned by non-root user)
RUN mkdir -p /app/data && chown buyer:buyer /app/data
Expand Down
Loading