diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 57a1ed16..4fdd7701 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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: @@ -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 \ diff --git a/infra/aws/terraform/github_oidc.tf b/infra/aws/terraform/github_oidc.tf new file mode 100644 index 00000000..57ed0c7f --- /dev/null +++ b/infra/aws/terraform/github_oidc.tf @@ -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 +} diff --git a/infra/aws/terraform/main.tf b/infra/aws/terraform/main.tf index 45280faa..61bb626b 100644 --- a/infra/aws/terraform/main.tf +++ b/infra/aws/terraform/main.tf @@ -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 } } diff --git a/infra/aws/terraform/modules/compute/outputs.tf b/infra/aws/terraform/modules/compute/outputs.tf index 532a8fd1..e9dbd5d5 100644 --- a/infra/aws/terraform/modules/compute/outputs.tf +++ b/infra/aws/terraform/modules/compute/outputs.tf @@ -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 diff --git a/infra/aws/terraform/outputs.tf b/infra/aws/terraform/outputs.tf index 960db0dd..6646b10f 100644 --- a/infra/aws/terraform/outputs.tf +++ b/infra/aws/terraform/outputs.tf @@ -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 diff --git a/infra/docker/Dockerfile b/infra/docker/Dockerfile index 22a0f993..979c8cea 100644 --- a/infra/docker/Dockerfile +++ b/infra/docker/Dockerfile @@ -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). # ============================================================================= # --------------------------------------------------------------------------- @@ -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 @@ -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" @@ -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