Prepare Application Artifacts To Be Deployed To AWS | 🏗️ Build A Multi-Environment Serverless App
**Exam Guide:** Developer - Associate
**🏗️ Domain 3:** **_Deployment_**
📘 **Task 1:** _Prepare Application Artifacts To Be Deployed To AWS_
> **Before you can deploy anything to AWS, you need to package it properly. This task covers Lambda deployment packaging (zip vs container), managing dependencies, structuring projects for multi-environment deployment, and using AWS AppConfig for runtime configuration.**
## 📘Concepts
### Lambda Deployment Packaging Options
Option | Max Size | Build Complexity | Cold Start | Best For
---|---|---|---|---
**Zip Package** _(inline editor)_ | 3 MB (editor limit) | None | Fastest | Simple functions, no dependencies
**Zip Package** _(upload)_ | 50 MB compressed / 250 MB uncompressed | Low | Fast | Most Lambda functions
**Zip + Lambda Layers** | 250 MB total (function + all layers) | Medium | Fast | Shared dependencies across functions
**Container Image** | 10 GB | Higher | Slower (first invoke) | ML libraries, large dependencies, custom runtimes
> 💡**If a scenario is about a deployment package exceeding 250 MB, the answer is container images. If it mentions sharing dependencies across multiple functions, the answer is Lambda Layers. Zip is the default for most workloads.**
### Lambda Layers
**Aspect** | _Detail_
---|---
**What They Are** | _Zip archives containing libraries, custom runtimes, or other dependencies_
**Max Layers Per Function** | _5_
**Size Limit** | _250 MB total_ (function code + all layers uncompressed)
**Versioning** | _Each publish creates an immutable version_
**Sharing** | _Can be shared across functions, accounts, or made public_
**Path** | _Contents extracted to`/opt` in the execution environment_
### Dependency Management Strategies
**Strategy** | _How It Works_ | Pros | Cons
---|---|---|---
**Bundle In Zip** | _Install deps into package directory, zip together_ | Simple, self-contained | Larger package, duplicated across functions
**Lambda Layers** | _Package deps as a layer, attach to functions_ | Shared across functions, smaller deploys | Layer version management, 5-layer limit
**Container Image** | _Install deps in Dockerfile_ | Full control, large deps supported | Slower cold starts, ECR management
**sam build** | _SAM resolves deps from requirements.txt automatically_ | Easiest, handles everything | Requires SAM CLI
### Environment-Specific Configuration Approaches
**Approach** | _When Config Is Resolved_ | Best For
---|---|---
**SAM Parameters + samconfig.toml** | _Deploy time_ | Resource names, table names, stage-specific infra
**Lambda Environment Variables** | _Deploy time_ (baked into function config) | Simple key-value config per environment
**SSM Parameter Store** | _Runtime_ (fetched by function) | Config that changes without redeployment
**AWS AppConfig** | _Runtime_ (with gradual rollout) | Feature flags, config that needs validation and rollback
**Secrets Manager** | _Runtime_ (fetched by function) | Credentials that rotate
> 💡 **If the question is _change configuration without redeploying_ , the answer is Parameter Store or AppConfig. If it says _gradual rollout_ or _validate configuration before applying,_ the answer is AppConfig.**
### AWS AppConfig Overview
**Feature** | _Detail_
---|---
**What It Does** | _Deploy configuration changes independently from code, with validation and rollout strategies_
**Deployment Strategies** | _AllAtOnce, Linear, Exponential_
**Validators** | _JSON Schema or Lambda function to validate config before deployment_
**Rollback** | _Automatic rollback if CloudWatch alarm triggers during deployment_
**Integration** | _Lambda extension for caching, or direct API calls_
**Cost** | _Free for configuration retrieval. Charges for feature flag evaluations_
### Project Structure Best Practices
A well-structured SAM project separates handlers, shared code, tests, and configuration:
my-app/
├── template.yaml # SAM/CloudFormation template
├── samconfig.toml # Per-environment deployment config
├── src/
│ ├── handlers/ # One file per Lambda function
│ │ ├── create_order.py
│ │ ├── get_order.py
│ │ └── process_payment.py
│ └── shared/ # Shared utilities across handlers
│ ├── models.py
│ └── utils.py
├── tests/
│ ├── unit/ # Fast, no AWS calls
│ │ ├── test_create_order.py
│ │ └── test_get_order.py
│ └── integration/ # Against deployed resources
│ └── test_api.py
├── events/ # Sample event payloads for testing
│ ├── create_order.json
│ └── get_order.json
└── requirements.txt # Python dependencies
## 🏗️ Build A Multi-Environment Serverless App
Build a **Multi-Environment Serverless App** that demonstrates real-world packaging and deployment patterns:
* A Lambda function packaged with dependencies via zip upload in the console
* A container image built and pushed to ECR
* AWS AppConfig set up with feature flags and a deployment strategy
* A SAM project structured for multi-environment deployment
* A `samconfig.toml` configured for dev, staging, and prod
## Prerequisites
* An AWS Account
* Docker Installed Locally
* SAM CLI Installed
## Part I
### Package a Lambda Function with Dependencies (Zip Upload)
#### Create the Deployment Package Locally
Create a zip file on your local machine and upload it through the console.
> ⚠️ **Common Windows mistake:** Don't create the zip with Explorer (right-click → Send to → Compressed folder). That wraps everything inside a subfolder, so your handler ends up at `package/lambda_function.py` and Lambda throws `No module named 'lambda_function'`. Rather build the zip in CloudShell.
**Step 01:** Create a project directory and install dependencies
mkdir lambda-package && cd lambda-package
# Create requirements.txt
cat > requirements.txt << 'EOF'
requests==2.31.0
boto3>=1.28.0
EOF
# Install dependencies into a package directory
pip install -r requirements.txt -t package/
# Create your function code
cat > package/lambda_function.py << 'EOF'
import json
import requests
import boto3
def lambda_handler(event, context):
"""
Lambda function packaged with external dependencies.
The 'requests' library is bundled in the zip package.
boto3 is included in the Lambda runtime, but pinning
a version in requirements.txt ensures consistency.
"""
# Use the bundled 'requests' library
response = requests.get('https://httpbin.org/json')
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Function with bundled dependencies',
'externalApiStatus': response.status_code,
'requestsVersion': requests.__version__
})
}
EOF
# Create the zip package
cd package && zip -r ../deployment.zip . && cd ..
#### **Upload via the Console**
**Step 02:** Open the **Lambda** console → **Create function**
* **Function name:** `PackagedFunction`
* **Runtime:** `Python 3.12`
Click **Create function**
**Step 03:** In the **Code source** section, click **Update ▼** → **Update from a .zip file**
**Step 04:** **Upload** , select your `deployment.zip` file
**Step 05:** Click **Update**
#### **Test the Function**
**Step 06:** Go to the **Test** tab → create a test event with `{}`
Click **Test**
> ⚠️ Verify the response shows the `requests` library version and a successful API call
>
> 💡**The zip upload limit is 50 MB compressed. If your package exceeds this, upload to S3 first and reference the S3 location. The uncompressed limit is 250 MB.`sam build` handles all of this automatically. It installs dependencies, creates the zip, and uploads to S3.**
## Part II
### Build And Push A Container Image To ECR
#### **Create an ECR Repository**
**Step 01:** Open the **ECR** console
**Step 02:** Click **Create repository**
* **Repository name:** `lambda-container-demo`
* **Image Tag immutability:** `Mutable`
* **▼ Image scanning settings -_deprecated_ :** Scan on push → `Enabled`
Click **Create**
> ⚠️ Click into the repository **Summary** and note the **URI** (e.g., `123456789012.dkr.ecr.us-east-1.amazonaws.com/lambda-container-demo`)
#### **Build and Push the Image Locally**
**Step 03:** Create a `Dockerfile` and function code
# Use the official AWS Lambda Python base image
FROM public.ecr.aws/lambda/python:3.12
# Install dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
# Copy function code
COPY app.py ${LAMBDA_TASK_ROOT}/
# Set the handler
CMD ["app.lambda_handler"]
**Step 04:** Create `app.py`
# app.py
import json
import requests
def lambda_handler(event, context):
"""
Lambda function running as a container image.
Container images support up to 10 GB — useful for
ML libraries, large datasets, or custom runtimes.
"""
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Hello from a container-based Lambda!',
'runtime': 'Container image (Python 3.12)',
'maxPackageSize': '10 GB (vs 250 MB for zip)'
})
}
**Step 05:** Build and push
# Authenticate Docker with ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
# Build the image
docker build -t lambda-container-demo .
# Tag for ECR
docker tag lambda-container-demo:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/lambda-container-demo:latest
# Push to ECR
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/lambda-container-demo:latest
**Alternatively**
# Authenticate Docker with ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
# Build the image — disable attestations so Lambda accepts the manifest
docker buildx build --provenance=false --sbom=false \
-t 123456789012.dkr.ecr.us-east-1.amazonaws.com/lambda-container-demo:latest \
--push .
> ⚠️ **Manifest Format Gotcha:** If you build with a plain `docker build` on a recent Docker Desktop, BuildKit adds provenance and SBOM attestations that produce an **OCI image index**. Lambda rejects this with: _"The image manifest, config or layer media type for the source image ... is not supported."_ Lambda only accepts **Docker Image Manifest V2 Schema 2**. The `--provenance=false --sbom=false` flags above force the compatible format. Alternatively, set `DOCKER_BUILDKIT=0` before building to use the legacy builder, which always produces the compatible manifest.
If you prefer the separate tag-and-push steps (with BuildKit disabled):
# PowerShell: $env:DOCKER_BUILDKIT=0 | CMD: set DOCKER_BUILDKIT=0
docker build -t lambda-container-demo .
docker tag lambda-container-demo:latest \
123456789012.dkr.ecr.us-east-1.amazonaws.com/lambda-container-demo:latest
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/lambda-container-demo:latest
#### **Verify in the Console**
**Step 06:** Go back to the **ECR** console → click into `lambda-container-demo`
> You should see your image with the `latest` tag
#### **Create a Lambda Function from the Container Image**
**Step 07:** Open the **Lambda** console → **Create function**
**Step 08:** Select **Container image**
* **Function name:** `ContainerLambdaDemo`
* **Container image URI:** Click **Browse images** → select `lambda-container-demo` → select the `latest` tag
Click **Create function**
**Step 09:** Test with an empty event `{}`
> 💡 **Container-based Lambda functions use ECR for image storage. The image must implement the Lambda Runtime API. AWS provides base images for all supported runtimes (`public.ecr.aws/lambda/python:3.12`). You can also use arbitrary base images with the Runtime Interface Client.**
## Part III
### Set Up AWS AppConfig with Feature Flags
#### **Create an AppConfig Application**
**Step 01:** Open the **AppConfig** console
**Step 02:** Click **Get started**
**Step 03:** **Select configuration type**
* **Configuration options:** `Feature flag`
* **Configuration profile name:** `feauture-flags`
Click **Next**
**Step 04:** **Specify configuration data**
* **Flag name:** `New Checkout Flow`
* **Flag key:** `new-checkout-flow`
* **Flag description -_Optional_ :** `Enable the redesigned checkout experience`
* **Variants:** `Basic flag`
* **Enabled value:** Toggle `**ON**`
Click **Next**
**Step 05:** **Review and save**
**Save to application**
* **Application name:** `orders-service`
* **Description:** `Feature flags and configuration for the orders service`
Click **Save and continue to deploy**
**Step 06:** **Start deployment**
**Environment:** Click `Create environment`
**Step 07:** **Create environment**
* **Name:** `production`
* **Description:** `Production environment`
Click **Create environment**
#### **Add A Feature Flag**
**Step 08:** Click **Add flag**
* **Flag name:** `Dark Mode`
* **Flag key:** `dark-mode`
* **Description:** `Enable dark mode UI`
* **Variants:** `Basic flag`
* **Enabled value:** Toggle `**OFF**`
Click **Save new version**
#### **Create a Deployment Strategy**
**Step 09:** **Deployment strategies** → **Create deployment strategy**
* **Name:** `GradualRollout`
* **Description:** `Deploy over 10 minutes with bake time`
* **Deployment type:** `Linear ▼`
* **Step percentage:** `20`
* **Deployment time:** `10 minutes`
* **Bake time:** `5 minutes`
Click **Create deployment strategy**
#### **Deploy the Configuration**
**Step 10:** Go to your `orders-service` **application** → `production` **environment**
Click **Start deployment**
**Step 11:** **Start deployment**
* **Configuration profile:** `feature-flags`
* **Hosted configuration version:** `(latest)`
* **Deployment strategy:** `GradualRollout`
Click **Start deployment**
> ⚠️ Watch the deployment progress. It rolls out 20% at a time over 10 minutes
>
> 💡**AppConfig deployment strategies control how fast configuration changes roll out. If a CloudWatch alarm fires during deployment, AppConfig automatically rolls back. This is safer than changing a Parameter Store value directly, which takes effect immediately for all callers.**
## Part IV
### Structure a SAM Project for Multi-Environment Deployment
#### **Step 01:** Initialize the Project
sam init --runtime python3.12 --name multi-env-app --app-template hello-world
cd multi-env-app
#### **Step 02:** Create the Handler Files
> ⚠️ `sam init` creates a `hello_world/` folder, but our template uses a `src/handlers/` structure with two functions. Create these files (delete the generated `hello_world/` folder afterward. It's no longer referenced):
#### **Step 03:** Create `src/handlers/requirements.txt`:
boto3
#### **Step 04:** Create `src/handlers/create_order.py`:
import json
import os
import boto3
import uuid
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])
def lambda_handler(event, context):
body = json.loads(event.get('body', '{}'))
order_id = str(uuid.uuid4())[:8].upper()
table.put_item(Item={
'PK': f'ORDER#{order_id}',
'SK': 'METADATA',
'customerId': body.get('customerId', 'unknown'),
'status': 'created'
})
return {
'statusCode': 201,
'body': json.dumps({'orderId': f'ORD-{order_id}', 'status': 'created'})
}
#### **Step 05:** Create `src/handlers/get_order.py`:
import json
import os
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])
def lambda_handler(event, context):
order_id = event['pathParameters']['orderId']
response = table.get_item(Key={'PK': f'ORDER#{order_id}', 'SK': 'METADATA'})
item = response.get('Item')
if not item:
return {'statusCode': 404, 'body': json.dumps({'error': 'Order not found'})}
return {'statusCode': 200, 'body': json.dumps(item, default=str)}
> ⚠️ The template's `CodeUri: src/handlers/` and `Handler: create_order.lambda_handler` must point to real files. If `src/handlers/` doesn't exist or is missing `requirements.txt`, `sam build` fails with _"source ... does not exist"_ or _"requirements.txt file not found."_ The `CodeUri` is the folder SAM packages; the `Handler` is `filename.function_name` relative to that folder.
#### **Step 06:** Configure the SAM Template with Parameters
Replace the contents of `template.yaml`
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Multi-environment serverless application
Parameters:
Stage:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
TableName:
Type: String
Default: orders
Globals:
Function:
Runtime: python3.12
Timeout: 30
Environment:
Variables:
STAGE: !Ref Stage
TABLE_NAME: !Sub "${Stage}-${TableName}"
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub "${Stage}-${TableName}"
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
CreateOrderFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/
Handler: create_order.lambda_handler
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref OrdersTable
Events:
CreateOrder:
Type: Api
Properties:
Path: /orders
Method: post
GetOrderFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/handlers/
Handler: get_order.lambda_handler
Policies:
- DynamoDBReadPolicy:
TableName: !Ref OrdersTable
Events:
GetOrder:
Type: Api
Properties:
Path: /orders/{orderId}
Method: get
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/"
#### **Step 07:** Configure samconfig.toml for Multiple Environments
# samconfig.toml — one section per environment
# The version key is REQUIRED — SAM CLI won't parse the file without it
version = 0.1
[default.deploy.parameters]
stack_name = "multi-env-app-dev"
resolve_s3 = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Stage=dev"
confirm_changeset = false
[staging.deploy.parameters]
stack_name = "multi-env-app-staging"
resolve_s3 = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Stage=staging"
confirm_changeset = false
[prod.deploy.parameters]
stack_name = "multi-env-app-prod"
resolve_s3 = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Stage=prod"
confirm_changeset = true
#### **Step 08:** Deploy to Different Environments
# Build once — use --use-container to build inside a Lambda-compatible
# Docker image (needs Docker running, but no local Python required)
sam build --use-container
# Or, if you have Python 3.12 installed and on your PATH:
# sam build
# Deploy to dev (default config)
sam deploy
# Deploy to staging
sam deploy --config-env staging
# Deploy to prod (will prompt for changeset confirmation)
sam deploy --config-env prod
> ⚠️ **`sam build` can't find Python?** If you see _"Binary validation failed for python ... did you have python for runtime: python3.12 on your PATH?"_ , it means Python 3.12 isn't installed locally (the `WindowsApps\python.EXE` entries are Microsoft Store stubs, not a real install). Use `sam build --use-container` to build inside a Docker container that already has the correct runtime, or install Python 3.12 from python.org and check "Add python.exe to PATH" during setup. CloudShell also has Python 3.12 and SAM pre-installed.
Each deployment creates isolated resources: `dev-orders` table, `staging-orders` table, `prod-orders` table.
> 💡 **`samconfig.toml` uses config environments (sections like `[staging.deploy.parameters]`) to manage per-environment settings. The `--config-env` flag selects which section to use. Setting `confirm_changeset = true` for production forces you to review changes before applying them.**
## 🏗️ What You Built | 📘 Exam Concepts Recap
**What You Built** | _Exam Concept_
---|---
**Bundled`requests` into a zip and uploaded via console** | _Zip packaging with dependencies, 50/250 MB limits_
**Built a Dockerfile and pushed to ECR** | _Container image packaging (up to 10 GB)_
**Created a Lambda from a container image** | _When to use containers vs zip_
**Created an AppConfig application and environment** | _Runtime configuration management_
**Added feature flags with enabled/disabled toggles** | _Feature flag pattern for decoupling release from deploy_
**Created a Linear deployment strategy with bake time** | _Gradual config rollout with automatic rollback_
**Used SAM parameters and`!Sub` for resource names** | _Environment-specific infrastructure_
**Configured`samconfig.toml` for dev/staging/prod** | _Multi-environment deployment with`--config-env`_
**Set`confirm_changeset = true` for prod** | _Forcing review before production changes_
## ⚠️ Clean Up Protocol
1. **Lambda** → Delete `PackagedFunction` and `ContainerLambdaDemo`
2. **ECR** → Delete the `lambda-container-demo` repository (and all images)
3. **AppConfig** → Delete the deployment, then the configuration profile, environment, and application (in that order)
4. **CloudFormation** → Delete any SAM-deployed stacks (`multi-env-app-dev`, etc.)
5. **IAM** → Delete Lambda execution roles
6. **CloudWatch** → Delete log groups
7. **S3** → Delete any SAM deployment buckets (prefixed with `aws-sam-cli-managed-default`)
## Key Takeaways
1. **Zip packages:** 50 MB compressed / 250 MB uncompressed. **Container images:** up to 10 GB. Use containers for large dependencies.
2. **Lambda Layers** share dependencies across functions. Up to 5 layers, 250 MB total uncompressed.
3. **sam build** resolves dependencies automatically. **sam deploy** packages and deploys. **sam deploy --guided** for first-time setup.
4. **AppConfig** deploys configuration independently from code, with validation, gradual rollout, and automatic rollback.
5. **samconfig.toml** manages multi-environment deployment configs. Use `--config-env` to select the environment.
6. **ECR** stores container images. Use `public.ecr.aws/lambda/` base images for Lambda containers.
7. Use **CloudFormation parameters** and `!Sub` for environment-specific resource names.
8. **SAM policy templates** (`DynamoDBCrudPolicy`, `S3ReadPolicy`) enforce least privilege without writing full IAM policies.
## Additional Resources
* Deploying Lambda functions as .zip file archives
* Managing Lambda dependencies with layers
* What is the AWS Serverless Application Model (AWS SAM)?
* What is AWS AppConfig?
* Create a Lambda function using a container image
* AWS SAM CLI configuration file
🏗️