【AI agent with CICD】Automating AgentCore Runtime Deployment with GitHub Actions

記事タイトルとURLをコピーする

For those who are interested, here is the Japanese version of this blog.

blog.serverworks.co.jp

Hello! I'm Law, who loves my cat Ringo-chan(リンゴちゃん).

In this article, I'll share how we achieved complete automation of AI agent deployment to Amazon Bedrock AgentCore Runtime using AWS CDK L2 Construct.

Notice
The @aws-cdk/aws-bedrock-agentcore-alpha package used in this article is an alpha release. Please carefully consider its use in production environments at your own responsibility. Also, the API may change.

Introduction

Amazon Bedrock AgentCore Runtime is a managed service that allows you to run AI agents serverlessly. However, traditional deployment methods had the following challenges:

  • Explicitly defining numerous AWS resources such as ECR repositories, CodeBuild, Lambda, and custom resources
  • Managing complex build pipeline dependencies
  • Detailed configuration of IAM roles and policies

By leveraging AWS CDK L2 Construct and GitHub Actions, we achieved complete automation with minimal resource definitions.

Solution Architecture

Below is an architecture diagram showing the overall flow from GitHub Actions to Bedrock AgentCore Runtime deployment:

Architecture diagram of the current project

Key Components:

  1. GitHub Actions: Access AWS with OIDC authentication, build Docker images
  2. AWS CDK: Define infrastructure as code, deploy CloudFormation stacks
  3. ECR: Store images in shared repository created by CDK bootstrap
  4. IAM: Automatically create execution roles and permissions for AgentCore Runtime
  5. AgentCore Runtime: Run containers and invoke Bedrock models

Project Overview

Structure

.
├── .github/
│   └── workflows/
│       └── deploy.yml    # CI/CD pipeline
├── agent/
│   ├── agent.py          # Strands agent
│   ├── Dockerfile        # ARM64 container
│   └── requirements.txt  # Python dependencies
├── lib/
│   └── stack.ts          # CDK stack (using L2 construct)
└── bin/
    └── app.ts            # CDK app

Technology Stack

  • AWS CDK: L2 Construct (@aws-cdk/aws-bedrock-agentcore-alpha)
  • GitHub Actions: CI/CD pipeline
  • Docker Buildx + QEMU: ARM64 cross-platform build
  • OIDC Authentication: No long-term credentials required

Power of L2 Construct: Comparison of Required Resource Definitions

Before: L1 Construct - Explicitly Define 6 AWS Resources

With traditional L1 Construct, you needed to explicitly define all of the following resources to deploy AgentCore Runtime:

// 1. ECR Repository
const repository = new ecr.Repository(this, 'Repository', {
  repositoryName: 'simple-agent',
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

// 2. CodeBuild Project (for Docker build)
const buildProject = new codebuild.Project(this, 'BuildProject', {
  environment: {
    buildImage: codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0,
    privileged: true,
  },
  buildSpec: codebuild.BuildSpec.fromObject({
    version: '0.2',
    phases: {
      pre_build: {
        commands: [
          'echo Logging in to Amazon ECR...',
          'aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login ...',
        ],
      },
      build: {
        commands: [
          'docker build --platform linux/arm64 -t $IMAGE_REPO_NAME:$IMAGE_TAG .',
          'docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr...',
        ],
      },
      post_build: {
        commands: ['docker push $AWS_ACCOUNT_ID.dkr.ecr...'],
      },
    },
  }),
});

// 3. Lambda Function (for CodeBuild trigger)
const buildTrigger = new lambda.Function(this, 'BuildTrigger', {
  runtime: lambda.Runtime.PYTHON_3_12,
  handler: 'index.handler',
  code: lambda.Code.fromInline(`
import boto3
import cfnresponse

def handler(event, context):
    codebuild = boto3.client('codebuild')
    # Build start logic (50+ lines)
    ...
  `),
  timeout: cdk.Duration.minutes(5),
});

// 4. Custom Resource (execute build on deployment)
const triggerBuild = new cdk.CustomResource(this, 'TriggerBuild', {
  serviceToken: buildTrigger.functionArn,
  properties: {
    ProjectName: buildProject.projectName,
    Timestamp: Date.now(),
  },
});

// 5. IAM Role (for AgentCore Runtime)
const agentRole = new iam.Role(this, 'AgentRole', {
  assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess'),
  ],
});

repository.grantPull(agentRole);

// 6. AgentCore Runtime (L1 Construct)
const agentRuntime = new cdk.CfnResource(this, 'AgentRuntime', {
  type: 'AWS::BedrockAgentCore::Runtime',
  properties: {
    AgentRuntimeName: 'simpleagent',
    RoleArn: agentRole.roleArn,
    AgentRuntimeArtifact: {
      ContainerConfiguration: {
        ContainerUri: `${repository.repositoryUri}:latest`,
      },
    },
  },
});

// Explicitly set dependencies
agentRuntime.node.addDependency(triggerBuild);

Resources that need explicit definition:

  1. ECR Repository
  2. CodeBuild Project
  3. Lambda Function
  4. Custom Resource
  5. IAM Role
  6. AgentCore Runtime

After: L2 Construct - Define Only 1 Resource

With L2 Construct, you only need to define AgentCore Runtime:

import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';

const runtime = new agentcore.Runtime(this, 'AgentRuntime', {
  runtimeName: 'simpleagent2',
  agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset('./agent', {
    platform: cdk.aws_ecr_assets.Platform.LINUX_ARM64,
  }),
  networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
  protocolConfiguration: agentcore.ProtocolType.HTTP,
});

// Add Bedrock invocation permissions
runtime.addToRolePolicy(
  new iam.PolicyStatement({
    actions: ["bedrock:InvokeModel*"],
    resources: [`arn:aws:bedrock:*::foundation-model/*`],
  })
);

Note: With L2 Construct, Docker container builds are performed in the environment where cdk deploy is executed (local machine or CI/CD runner), not in CodeBuild.

Resources that need explicit definition: 1. AgentCore Runtime (only!)

Automatically created and managed resources:

  • ECR Repository (uses shared repository pre-created by CDK bootstrap)
  • Docker image build and push (automatically executed by DockerImageAsset)
  • IAM Role (automatically created by Runtime)
  • ECR pull permissions (automatically granted)
  • Dependency resolution (automatic)

What's Great About This?

1. Clear Infrastructure Intent

  • L1: Write implementation details of "how to build"
  • L2: Write only "what to deploy"

2. Reduced Maintenance Burden

  • L1: Manage all of CodeBuild's buildspec, Lambda's build logic, and IAM policies
  • L2: Manage only AgentCore Runtime configuration

3. Automatic Application of Best Practices

  • L1: Implement IAM permissions, dependencies, and error handling yourself
  • L2: Automatically apply patterns verified by the CDK team

4. Flexibility for Changes

  • L1: Changing build method requires modifying multiple resources
  • L2: Just change fromAsset() options

Internal Operation of fromAsset() and DockerImageAsset

The fromAsset() of L2 Construct operates with the following processing flow:

1. Constructor Execution (new Runtime())

// fromAsset() just returns an AssetImage instance
agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset('./agent', {
  platform: cdk.aws_ecr_assets.Platform.LINUX_ARM64,
})

At this point, DockerImageAsset is not created.

2. CloudFormation Template Generation (cdk synth)

Lazy evaluation is registered via Lazy.any() inside the Runtime constructor:

// Internal processing of runtime.js (inside constructor)
const cfnProps = {
  agentRuntimeName: this.agentRuntimeName,
  roleArn: this.role.roleArn,
  agentRuntimeArtifact: Lazy.any({
    produce: () => this.renderAgentRuntimeArtifact()  // Register lazy evaluation
  }),
  // ...
};

When cdk synth is executed, produce() is called at CloudFormation template generation timing, and renderAgentRuntimeArtifact() is executed:

// Implementation of renderAgentRuntimeArtifact()
renderAgentRuntimeArtifact() {
  this.agentRuntimeArtifact.bind(this, this);  // Call bind() here
  const config = this.agentRuntimeArtifact._render();
  // ...
}

3. Details of bind() Method

The bind() method is called on the AssetImage instance created by fromAsset() and creates the actual DockerImageAsset:

// Implementation of runtime-artifact.js
class AssetImage extends AgentRuntimeArtifact {
  private asset?: assets.DockerImageAsset;
  private bound = false;

  public bind(scope: Construct, runtime: Runtime): void {
    // Create DockerImageAsset (first time only)
    if (!this.asset) {
      const hash = md5hash(this.directory);  // Calculate hash from directory path
      this.asset = new assets.DockerImageAsset(scope, `AgentRuntimeArtifact${hash}`, {
        directory: this.directory,
        ...this.options,  // Options like platform: LINUX_ARM64
      });
    }

    // Grant ECR pull permissions (first time only)
    if (!this.bound) {
      this.asset.repository.grantPull(runtime.role);
      this.bound = true;
    }
  }
}

What the bind() method does:

  • Hash Calculation: Generate hash from directory path with md5hash(directory)
  • DockerImageAsset Creation: Create unique resource including hash in ID
  • Record Asset Information: Record build information in cdk.out/manifest.json
  • Grant IAM Permissions: Automatically grant ECR pull permissions to Runtime's IAM role
  • Ensure Idempotency: Safe even if called multiple times with bound flag

Important: Lazy evaluation is registered with Lazy.any(), and actual processing is performed inside the bind() method.

4. Deployment (cdk deploy)

The cdk-assets tool reads manifest.json:

  • Build Docker image
  • Authenticate to ECR
  • Push image
  • Deploy CloudFormation stack

Important Point: Docker build is executed during cdk deploy, not during cdk synth. If the hash doesn't change, rebuild can be skipped.

Two Approaches to Image Building

The method adopted this time uses CDK's fromAsset() to automatically build and push images during cdk deploy. However, another approach is also possible:

Approach 1: CDK Integrated (Adopted in This Article)

agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset('./agent', {
  platform: cdk.aws_ecr_assets.Platform.LINUX_ARM64,
})
  • Pros: Simple, minimal resource definitions
  • Cons: Build time is included in cdk deploy

Approach 2: Pre-Build Type

Define ECR repository independently with CDK, call CodeBuild from GitHub Actions to build and push image, then execute cdk deploy:

const repository = new ecr.Repository(this, 'Repository');
const buildProject = new codebuild.Project(this, 'BuildProject', { /* ... */ });

agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromEcrRepository(
  repository,
  'latest'
)
# GitHub Actions
- name: Build and push image
  run: aws codebuild start-build --project-name $PROJECT_NAME
- name: Deploy CDK stack
  run: npx cdk deploy --require-approval never
  • Pros: Separation of build and deployment, flexible control of build cache
  • Cons: More resource definitions, more complex pipeline

Both approaches are valid, but choose Approach 1 if you prioritize simplicity, and Approach 2 if you want fine-grained control over the build process.

ARM64 Build Challenges and Solutions

AgentCore Runtime requires ARM64 architecture, but how do you build on GitHub Actions (x86_64)?

Solution: QEMU + Docker Buildx

- name: Set up QEMU
  uses: docker/setup-qemu-action@v3  # ARM64 emulation
  
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3  # Cross-platform build
  
- name: Deploy CDK stack
  run: npx cdk deploy --require-approval never

Explicitly specify platform in Dockerfile as well:

FROM --platform=linux/arm64 python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY agent.py .

EXPOSE 8080
CMD ["uvicorn", "agent:app", "--host", "0.0.0.0", "--port", "8080"]

CI/CD Pipeline

Secure with OIDC Authentication

Adopted OIDC authentication instead of traditional Access Key/Secret Key:

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
    aws-region: us-west-2

Benefits:

  • No need to manage long-term credentials
  • Automatically issue temporary credentials
  • Access only from specific repositories and branches

Deployment Flow

Code Change → Create PR → Review → Merge to main → Auto Deploy (5 min)

Avoid unnecessary builds with path filters:

on:
  push:
    branches:
      - main
    paths:
      - 'folder/subfolder/**'

Gotchas

1. Bedrock Permissions Not Automatically Granted

The IAM role automatically created by L2 Construct does not include permissions to call Bedrock API.

// Need to add manually
runtime.addToRolePolicy(
  new iam.PolicyStatement({
    actions: ["bedrock:InvokeModel*"],
    resources: [`arn:aws:bedrock:*::foundation-model/*`],
  })
);

2. Cross-Region Access

Even if Runtime is deployed to US West (Oregon, us-west-2), it may call Claude Sonnet 4 in US East (N. Virginia, us-east-1). Handle this by using wildcard (*) for region:

resources: [`arn:aws:bedrock:*::foundation-model/*`]

3. CDK Bootstrap Required

L2 Construct uses CDK's asset publishing system, so cdk bootstrap is required in advance:

cdk bootstrap aws://ACCOUNT_ID/us-west-2

Summary

By leveraging AWS CDK L2 Construct, we were able to greatly simplify the complex deployment of AgentCore Runtime.

What We Learned:

  • High level of abstraction in L2 Construct
  • Lazy evaluation mechanism with fromAsset() and bind() methods
  • Processing flow during CloudFormation generation using Lazy.any()
  • Implementation of ARM64 cross-platform build
  • Practical use of OIDC authentication

References


Bonus

For those wondering who Ringo-chan(リンゴちゃん) is, here's a photo. Christmas is still about 2 months away, but the temperature has dropped suddenly, so please be careful not to catch a cold!

Ringo-chan (Christmas ver.)

ロータッヘイ(執筆記事の一覧)

24卒入社の香港人です。
2025 Japan All AWS Certifications Engineers
リンゴちゃん(デボンレックス)にいつも癒されています。