P5 — Hands-On Deployment: Cloud Functions, Secrets, and Basic CI/CD
Learning objective
Deploy your containerised Claude service to a cloud function or managed container service, configure secrets management using the cloud provider's native service, verify the deployment via curl, and run a basic automated test on each commit using GitHub Actions.
Why this matters
The gap between "runs on my machine" and "runs in production" is infrastructure. This week closes that gap for a minimal AI service. Every production AI deployment you architect in Modules 2, 3, and 4 follows the same pattern you build here — the sophistication increases, but the model (containerise → push → deploy → configure secrets → monitor → automate) is consistent. After this week, you will be able to verify that your architectural designs are implementable by running them yourself.
Pre-work
- AWS Lambda getting started at
docs.aws.amazon.com/lambda— read the Python deployment guide (~45 min) - AWS Secrets Manager getting started at
docs.aws.amazon.com/secretsmanager— read the first-use guide (~20 min) - GitHub Actions quickstart at
docs.github.com/actions/quickstart(~20 min) - Install AWS CLI:
pip install awscli && aws configure(enter your AWS access key and secret from the AWS console IAM section)
Core concept explained
Serverless vs containerised deployment
Two patterns dominate AI service deployment. Serverless functions (AWS Lambda, Azure Functions, GCP Cloud Run functions) execute on-demand with no idle cost and scale automatically, but have execution time limits (15 minutes for Lambda) and cold-start latency. Managed container services (AWS ECS/Fargate, Azure Container Apps, GCP Cloud Run) run containers persistently with predictable latency but incur idle costs. For most AI services: synchronous, user-facing interactions → managed containers; batch and event-triggered processing → serverless.
Secrets management in cloud
AWS Secrets Manager stores encrypted key-value pairs. A Lambda function or container retrieves secrets at startup using the AWS SDK, not from environment variables or config files. The IAM role attached to the compute resource controls which secrets it can access. This is the enterprise-grade pattern — rotation, auditing, and access control in one place.
import boto3
import json
def get_secret(secret_name: str, region: str = "us-east-1") -> dict:
"""Retrieve a secret from AWS Secrets Manager."""
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])GitHub Actions for CI/CD
A GitHub Actions workflow is a YAML file in .github/workflows/ that runs on events (push, pull request). For an AI service, the minimum viable pipeline is: on push to main → run tests → build Docker image → push to registry → (optionally) deploy to cloud. This eliminates the manual deployment step and ensures tests run on every change.
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5 # Check marketplace.github.com/actions/setup-python for the current major version
with:
python-version: '3.14'
- run: pip install -r requirements.txt
- run: python test_prompt_utils.py
- run: python test_claude_client.py
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t cca-claude-service:${{ github.sha }} .
- name: Push to GHCR
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push cca-claude-service:${{ github.sha }}Note on Lambda deployment patterns. This week uses the .zip package approach, which teaches the Lambda fundamentals clearly and works well for lightweight functions. For dependency-heavy Python workloads (such as agents with large SDK dependencies), Lambda container image deployment is now the preferred production pattern — it reuses the Dockerfile you built in Week P4 and avoids the 250MB unzipped package limit. The architectural decision between zip and container image deployment is covered in Module 3.
Common mistake: Adding ANTHROPIC_API_KEY as a plain environment variable in the GitHub Actions workflow YAML file. GitHub Actions has a Secrets store (Settings → Secrets → Actions) — API keys go there, accessed as ${{ secrets.SECRET_NAME }}. Never put secret values in the YAML file itself; the YAML is checked into version control.
Step-by-step exercise — Tier 3 (Independent)
What Tier 3 means for this week: the steps below describe what to build and the expected outcome. They do not provide commands you can run verbatim — they describe the task and you determine how to accomplish it using the Core Concept patterns, AWS documentation, and your Week P3 and P4 experience. When you are stuck, the correct first move is to re-read the relevant Core Concept section. The correct second move is to consult the official AWS or GitHub Actions documentation linked in the pre-work. Looking up documentation is not failure — it is professional practice. What Tier 3 is preparing you for is Module 1, where every exercise works at this level.
Deliverable: Your Claude service deployed to AWS Lambda (or Azure Functions), retrieving its API key from Secrets Manager, verified working via curl, with a GitHub Actions workflow that runs your tests and builds the Docker image on every push.
Step 1 — Store the API key in AWS Secrets Manager
aws secretsmanager create-secret \
--name "cca-prep/anthropic-api-key" \
--secret-string '{"ANTHROPIC_API_KEY":"sk-ant-..."}'Step 2 — Create an IAM role for Lambda
In the AWS console, create an IAM role with the AWSLambdaBasicExecutionRole managed policy plus a custom inline policy allowing secretsmanager:GetSecretValue on your specific secret ARN. Note the role ARN — you will specify this in the Lambda deployment.
Step 3 — Adapt main.py for Lambda
Lambda functions use a handler function pattern rather than a running HTTP server. Create lambda_handler.py:
import os
import json
import boto3
from claude_client import ClaudeClient
def load_config_from_secrets_manager():
"""Load configuration from AWS Secrets Manager on cold start."""
secret_name = os.environ.get("SECRET_NAME", "cca-prep/anthropic-api-key")
client = boto3.client("secretsmanager", region_name=os.environ.get("AWS_REGION", "us-east-1"))
response = client.get_secret_value(SecretId=secret_name)
secrets = json.loads(response["SecretString"])
os.environ["ANTHROPIC_API_KEY"] = secrets["ANTHROPIC_API_KEY"]
# Load config at cold start, not on every invocation
load_config_from_secrets_manager()
claude_client = ClaudeClient()
def handler(event, context):
"""AWS Lambda handler."""
try:
body = json.loads(event.get("body", "{}"))
system_prompt = body.get("system_prompt", "You are a helpful assistant.")
user_message = body.get("user_message", "")
if not user_message:
return {"statusCode": 400, "body": json.dumps({"error": "user_message required"})}
result = claude_client.complete(system_prompt, user_message)
status_code = 200 if result["success"] else 502
return {"statusCode": status_code, "body": json.dumps(result)}
except Exception as e:
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}Step 4 — Deploy to Lambda
# Package the function with dependencies
# Note: --target installs to ./package directory, not the system path,
# so --break-system-packages is not required here (unlike general pip installs)
pip install --target ./package -r requirements.txt
cd package && zip -r ../deployment.zip . && cd ..
zip deployment.zip lambda_handler.py claude_client.py prompt_utils.py
# Create the Lambda function
aws lambda create-function \
--function-name cca-claude-service \
--runtime python3.14 \
--role arn:aws:iam::YOUR_ACCOUNT_ID:role/cca-lambda-role \
--handler lambda_handler.handler \
--zip-file fileb://deployment.zip \
--environment Variables="{SECRET_NAME=cca-prep/anthropic-api-key}" \
--timeout 30Step 5 — Add a function URL and test
aws lambda add-permission \
--function-name cca-claude-service \
--action lambda:InvokeFunctionUrl \
--principal "*" \
--function-url-auth-type NONE \
--statement-id public-url
aws lambda create-function-url-config \
--function-name cca-claude-service \
--auth-type NONETest with curl:
curl -X POST "YOUR_FUNCTION_URL" \
-H "Content-Type: application/json" \
-d '{"system_prompt": "Reply in one sentence.", "user_message": "What is 2+2?"}'Step 6 — Set up GitHub Actions
Add ANTHROPIC_API_KEY to your GitHub repository secrets. Create .github/workflows/ci.yml following the template from the core concept section. Push to main and verify the workflow runs successfully in the GitHub Actions tab.
Step 7 — Document the deployment
Create DEPLOYMENT.md in your repository covering: the architecture (what runs where), the secrets management approach, how to run locally, how to run tests, and how to deploy an update. Write it as if a new engineer joining your project needs to understand the system in 10 minutes.
Prerequisite Track Capstone — Minimum Viable AI Service
Definition of done for the prerequisite track:
You have a public GitHub repository (cca-claude-service or similar) containing:
prompt_utils.py,claude_client.py,main.py,lambda_handler.py— all tested and committedDockerfileand.dockerignore— builds successfully and runs locallyrequirements.txt— complete and reproducible.github/workflows/ci.yml— runs on every push and passesDEPLOYMENT.md— a clear deployment guide- A deployed Lambda function (or equivalent cloud function) that responds correctly to curl
And you can demonstrate all of the following without reference to documentation:
- Run the test suite locally and explain what each test verifies
- Build the Docker image and run it with an injected API key
- Explain why the API key is in Secrets Manager and not in an environment variable in the Lambda configuration
- Show the GitHub Actions run history and explain what the pipeline does on each push
- Read the CloudWatch log output from a Lambda invocation and identify the interaction record
Reflection
Under a new ## Week P5 heading in journal.md, write 4–6 sentences answering the following. Be specific.
You have now built and deployed an AI service end-to-end: API integration → containerised service → Lambda function → GitHub Actions pipeline. Looking back at the five weeks: which single capability gap — if you had not closed it before Module 1 — would have caused the most difficulty in the Module 3 deployment exercises? And: in the AI system you are most likely to architect in your current or target role, would you deploy it as a serverless function or a managed container service? Name the specific constraint (SLA, event frequency, idle cost, or cold start) that drives that decision.
Commit the journal: git add journal.md && git commit -m "week P5 reflection — prerequisite track complete"
Self-check questions
Q1. Your Lambda function's cold start time increases from 800ms to 4,200ms after you move API key loading from a hardcoded string to Secrets Manager. What is the correct architectural response?
Q2. Your GitHub Actions workflow fails on the build job but passes on the test job. The error is permission denied when pushing to the container registry. What is the most likely cause?
Q3. You need to update your deployed Lambda function with a new version of claude_client.py. What is the correct production sequence?
Progression gate
No standalone progression-gate section in the source for this week — self-attestation still available below.