Deployment Architecture¶
This guide explains how SYNDI deployment works, including the SAM (Serverless Application Model) deployment process, CloudFormation stack management, and build isolation.
Overview¶
SYNDI uses a Makefile-driven SAM deployment approach that provides:
Automated infrastructure provisioning via CloudFormation
Build isolation per environment and organization
Dependency layer caching for faster builds
Automatic rollback on deployment failures
Zero manual configuration - Everything computed from ENV/ORG parameters
Deployment Flow¶
High-Level Process¶
1. Developer runs: make rs-deploy ENV=stage ORG=myorg
↓
2. Makefile computes deployment parameters
↓
3. SAM builds Lambda function + dependency layer
↓
4. Artifacts uploaded to S3 deployment bucket
↓
5. CloudFormation creates/updates stack
↓
6. AWS resources created (Lambda, API Gateway, Cognito, S3, CloudFront)
↓
7. Configuration synced from CloudFormation outputs
↓
8. Deployment verified and tested
Detailed Deployment Steps¶
Step 1: Parameter Computation (Makefile)
# From ENV and ORG, compute:
STACK_NAME = rawscribe-$(ENV)-$(ORG)
ACCOUNT_NUMBER = $(shell aws sts get-caller-identity --query Account --output text)
AWS_REGION = $(shell aws configure get region)
BUILD_DIR = .aws-sam-$(ENV)-$(ORG)
Step 2: SAM Build
sam build --cached --parallel \
--config-env $(ENV)-$(ORG) \
--build-dir .aws-sam-$(ENV)-$(ORG) \
--cache-dir .aws-sam-$(ENV)-$(ORG)/cache
Builds:
RawscribeLambda- Application code frombackend/DependencyLayer- Python packages frombackend/layers/dependencies/
Step 3: Config Upload
# Upload merged config to Lambda S3 bucket
aws s3 cp infra/.config/lambda/$(ENV)-$(ORG).json \
s3://rawscribe-lambda-$(ENV)-$(ORG)-$(ACCOUNT_NUMBER)/config.json
Step 4: SAM Deploy
sam deploy --no-confirm-changeset \
--stack-name rawscribe-$(ENV)-$(ORG) \
--template-file .aws-sam-$(ENV)-$(ORG)/template.yaml \
--s3-bucket rawscribe-sam-deployments-$(ACCOUNT_NUMBER) \
--s3-prefix rawscribe-$(ENV)-$(ORG) \
--parameter-overrides \
Environment=$(ENV) \
Organization=$(ORG) \
EnableAuth=$(ENABLE_AUTH) \
CreateBuckets=$(CREATE_BUCKETS) \
--capabilities CAPABILITY_NAMED_IAM
Step 5: CloudFormation Processing
CloudFormation creates/updates resources defined in template.yaml:
Validates template
Creates change set
Executes changes (create/update/delete resources)
Outputs resource IDs
Updates stack status
Step 6: Post-Deployment
Admin user creation (if ADMIN_USERNAME provided)
Authentication testing
API endpoint testing
Display deployment summary
SAM Template Structure¶
Template Organization¶
template.yaml defines all infrastructure:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
MemorySize: 512
Runtime: python3.9
Parameters:
Environment, Organization, EnableAuth, CreateBuckets, etc.
Conditions:
CreateAuth, CreateUserPool, UseExistingUserPool, IsProd
Resources:
# IAM Roles
LambdaExecutionRole
# Lambda Resources
DependencyLayer
RawscribeLambda
# API Gateway
ApiGateway
# Cognito (conditional)
CognitoUserPool
CognitoUserPoolClient
CognitoAdminGroup
CognitoLabManagerGroup
CognitoResearcherGroup
CognitoClinicianGroup
# S3 (conditional)
FrontendBucket
FrontendBucketPolicy
# CloudFront
CloudFrontOriginAccessControl
CloudFrontDistribution
Outputs:
ApiEndpoint, CognitoUserPoolId, CognitoClientId, etc.
Conditional Resource Creation¶
Resources are conditionally created based on parameters:
Conditions:
CreateAuth: !Equals [!Ref EnableAuth, 'true']
CreateUserPool: !And
- !Condition CreateAuth
- !Equals [!Ref CognitoUserPoolId, '']
UseExistingUserPool: !And
- !Condition CreateAuth
- !Not [!Equals [!Ref CognitoUserPoolId, '']]
Examples:
ENABLE_AUTH=true→ Creates Cognito resourcesENABLE_AUTH=false→ Skips Cognito creationCREATE_BUCKETS=true→ Creates S3 bucketsCREATE_BUCKETS=false→ References existing buckets
Build Directory Isolation¶
Each ENV/ORG combination has isolated build artifacts:
.aws-sam-stage-myorg/ # Stage environment, myorg organization
├── build.toml # SAM build metadata
├── cache/ # Build cache (speeds up rebuilds)
│ └── hash files
├── DependencyLayer/ # Python dependencies layer
│ └── python/
│ ├── fastapi/
│ ├── boto3/
│ ├── pydantic/
│ └── ... (all requirements.txt packages)
├── RawscribeLambda/ # Application code
│ ├── rawscribe/
│ │ ├── main.py
│ │ ├── routes/
│ │ └── utils/
│ └── (dependencies from layer not included here)
└── template.yaml # Processed CloudFormation template
Isolation benefits:
Different orgs can build/deploy simultaneously
No cross-contamination between builds
Each org can use different dependency versions
Parallel CI/CD pipelines possible
Build Cache¶
SAM caches dependency layer builds:
.aws-sam-stage-myorg/cache/
└── hash-of-requirements.txt/ # Cache key from requirements.txt hash
└── DependencyLayer/ # Cached layer
When cache is used:
requirements.txtunchangedUsing
--cachedflag (automatic in Makefile)Same ENV/ORG combination
When cache is invalidated:
requirements.txtmodifiedBuild directory deleted
Cache directory cleared
Deployment Commands Explained¶
rs-deploy (Full Build and Deploy)¶
Command:
make rs-deploy ENV=stage ORG=myorg
Process:
Calls
rs-buildtargetSAM builds Lambda + layer (uses cache if possible)
Calls
rs-deploy-onlytargetHandles ROLLBACK_COMPLETE state
Uploads config to S3
SAM deploys via CloudFormation
Creates admin user if credentials provided
Tests deployment
Build artifacts created:
.aws-sam-stage-myorg/
├── DependencyLayer/ # Built from backend/layers/dependencies/
├── RawscribeLambda/ # Built from backend/
└── template.yaml # Processed template
Time: 5-7 minutes (or 30 seconds if layer cached)
rs-deploy-only (Deploy Without Build)¶
Command:
make rs-deploy-only ENV=stage ORG=myorg
Process:
Uses existing
.aws-sam-stage-myorg/buildChecks for ROLLBACK_COMPLETE state
Deletes failed stack if needed
Uploads config to S3
SAM deploys using existing build
Creates admin user if credentials provided
Time: 1-2 minutes
Requirement: Must have existing build directory from previous rs-deploy
rs-deploy-function (Quick Lambda Update)¶
Command:
make rs-deploy-function ENV=stage ORG=myorg
Process:
Creates minimal zip of Python code only
No dependencies included (uses existing layer)
Directly updates Lambda via AWS API
Bypasses CloudFormation completely
Uploads via S3 if package > 69MB
Build artifacts:
backend/.build/lambda/
├── package/ # Temporary build
│ └── rawscribe/ # Code only, no dependencies
└── function-minimal.zip # ~2MB (code only)
Time: 30 seconds
Limitations:
Can’t update environment variables
Can’t update infrastructure
Can’t update dependencies
CloudFormation Stack Management¶
Stack Lifecycle¶
NO_STACK
↓ (first deployment)
CREATE_IN_PROGRESS
↓ (success)
CREATE_COMPLETE
↓ (update deployment)
UPDATE_IN_PROGRESS
↓ (success)
UPDATE_COMPLETE
↓ (failed update)
UPDATE_ROLLBACK_IN_PROGRESS
↓
ROLLBACK_COMPLETE (requires deletion before redeployment)
Automatic ROLLBACK_COMPLETE Handling¶
The Makefile automatically handles failed deployments:
# Check if stack in ROLLBACK_COMPLETE
STACK_STATUS=$(aws cloudformation describe-stacks ...)
if [ "$STACK_STATUS" = "ROLLBACK_COMPLETE" ]; then
# Delete failed stack
aws cloudformation delete-stack --stack-name $(STACK_NAME)
# Wait for deletion
aws cloudformation wait stack-delete-complete --stack-name $(STACK_NAME)
# Proceed with fresh deployment
fi
Stack Outputs¶
CloudFormation provides outputs that become configuration values:
Outputs:
ApiEndpoint:
Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/${Environment}'
CognitoUserPoolId:
Value: !If [CreateUserPool, !Ref CognitoUserPool, !Ref CognitoUserPoolId]
CognitoClientId:
Value: !If [CreateUserPool, !Ref CognitoUserPoolClient, !Ref CognitoClientId]
These outputs are:
Retrieved by
sync-configsMerged into org-specific config files
Used by frontend and backend at runtime
Dependency Layer Architecture¶
Layer Build Process¶
Source: backend/layers/dependencies/requirements.txt
Build:
# SAM builds layer using BuildMethod: python3.9
# Equivalent to:
pip install -r requirements.txt -t python/
zip -r layer.zip python/
Result: Lambda layer with all Python packages
Layer Usage¶
Lambda function references layer:
RawscribeLambda:
Type: AWS::Serverless::Function
Properties:
Layers:
- !Ref DependencyLayer
At runtime:
Layer mounted at
/opt/python/Python automatically searches
/opt/python/for importsApplication code can import all layer packages
Layer Caching Strategy¶
Cache key: Hash of requirements.txt
Cache reuse:
make rs-deploywith unchanged requirements.txt → Reuses cached layer (30 sec build)make rs-deploywith changed requirements.txt → Rebuilds layer (5 min build)
Force layer rebuild:
rm -rf .aws-sam-stage-myorg/cache/
make rs-deploy ENV=stage ORG=myorg
Environment Variables¶
Lambda Environment Variables¶
Set by CloudFormation from template.yaml:
Environment:
Variables:
ENV: !Ref Environment # stage
ORG: !Ref Organization # myorg
CONFIG_S3_BUCKET: !Sub 'rawscribe-lambda-${Environment}-${Organization}-${AWS::AccountId}'
CONFIG_S3_KEY: config.json
COGNITO_REGION: !Ref AWS::Region
COGNITO_USER_POOL_ID: !If [CreateUserPool, !Ref CognitoUserPool, ...]
COGNITO_CLIENT_ID: !If [CreateUserPool, !Ref CognitoUserPoolClient, ...]
FORMS_BUCKET: !Sub 'rawscribe-forms-${Environment}-${Organization}-${AWS::AccountId}'
ELN_BUCKET: !Sub 'rawscribe-eln-${Environment}-${Organization}-${AWS::AccountId}'
DRAFTS_BUCKET: !Sub 'rawscribe-eln-drafts-${Environment}-${Organization}-${AWS::AccountId}'
Benefits:
Infrastructure values automatically set
No hardcoded resource IDs
Updates automatically on redeployment
Different values per environment/org
Configuration Precedence¶
Lambda loads configuration in this order:
Environment variables (from CloudFormation) - Highest priority
Config file from S3 (
CONFIG_S3_BUCKET/CONFIG_S3_KEY)Bundled config (if S3 load fails)
Application defaults - Lowest priority
Resource Naming¶
All resources follow consistent naming patterns:
CloudFormation Stack¶
Pattern: rawscribe-{env}-{org}
Example: rawscribe-stage-myorg
Lambda Function¶
Pattern: rawscribe-{env}-{org}-backend
Example: rawscribe-stage-myorg-backend
Configured in template.yaml:
FunctionName: !Sub 'rawscribe-${Environment}-${Organization}-backend'
Lambda Layer¶
Pattern: rawscribe-deps-{env}-{org}
Example: rawscribe-deps-stage-myorg
Configured in template.yaml:
LayerName: !Sub 'rawscribe-deps-${Environment}-${Organization}'
API Gateway¶
Pattern: rawscribe-{env}-{org}-api
Example: rawscribe-stage-myorg-api
Configured in template.yaml:
Name: !Sub 'rawscribe-${Environment}-${Organization}-api'
StageName: !Ref Environment
Cognito User Pool¶
Pattern: rawscribe-{env}-{org}-userpool
Example: rawscribe-stage-myorg-userpool
Configured in template.yaml:
UserPoolName: !Sub 'rawscribe-${Environment}-${Organization}-userpool'
S3 Buckets¶
Pattern: rawscribe-{service}-{env}-{org}-{accountid}
Examples:
rawscribe-lambda-stage-myorg-288761742376
rawscribe-forms-stage-myorg-288761742376
rawscribe-eln-stage-myorg-288761742376
rawscribe-eln-drafts-stage-myorg-288761742376
syndi-frontend-stage-myorg-288761742376
Configured in template.yaml:
BucketName: !Sub 'rawscribe-forms-${Environment}-${Organization}-${AWS::AccountId}'
IAM Roles¶
Pattern: rawscribe-{env}-{org}-lambda-role
Example: rawscribe-stage-myorg-lambda-role
Configured in template.yaml:
RoleName: !Sub 'rawscribe-${Environment}-${Organization}-lambda-role'
Build Artifacts¶
SAM Build Directory¶
.aws-sam-{ENV}-{ORG}/
├── build.toml # Build metadata
├── cache/ # Dependency layer cache
│ └── {hash}/
│ └── DependencyLayer/
├── DependencyLayer/ # Built layer (ready for upload)
│ └── python/
│ └── {all packages}/
├── RawscribeLambda/ # Built Lambda (ready for upload)
│ └── rawscribe/
│ ├── main.py
│ ├── routes/
│ ├── utils/
│ └── .config/ # Bundled config
└── template.yaml # Processed template with substitutions
Lambda Package Contents¶
Full package (from rs-deploy):
Application code (
rawscribe/)Configuration (
.config/config.json)No dependencies (in separate layer)
Minimal package (from rs-deploy-function):
Application code only
No configuration
No dependencies
Much smaller (~2MB vs ~10MB)
Multi-Organization Isolation¶
Build Isolation¶
Each organization gets separate build directory:
.aws-sam-stage-org1/ # Organization 1 build
.aws-sam-stage-org2/ # Organization 2 build
.aws-sam-stage-org3/ # Organization 3 build
Benefits:
Parallel builds possible
No version conflicts
Independent deployment schedules
Isolated dependency versions
Runtime Isolation¶
Each organization gets separate resources:
Organization 1:
├── Lambda: rawscribe-stage-org1-backend
├── API: rawscribe-stage-org1-api
├── Cognito: rawscribe-stage-org1-userpool
└── S3: rawscribe-*-stage-org1-{accountid}
Organization 2:
├── Lambda: rawscribe-stage-org2-backend
├── API: rawscribe-stage-org2-api
├── Cognito: rawscribe-stage-org2-userpool
└── S3: rawscribe-*-stage-org2-{accountid}
Isolation guarantees:
User from org1 cannot authenticate to org2
Lambda from org1 cannot access org2’s S3 buckets
API endpoints completely separate
Zero data leakage between organizations
Deployment Parameters¶
Required Parameters¶
ENV - Environment name
Values:
dev,test,stage,prodUsage: Resource naming, configuration selection
Example:
ENV=stage
ORG - Organization identifier
Values: Any lowercase alphanumeric string
Usage: Resource naming, multi-org isolation
Example:
ORG=myorgNo default - Must be explicitly provided for security
Optional Parameters¶
ENABLE_AUTH - Enable Cognito authentication
Values:
true,falseDefault:
trueEffect: Creates/uses Cognito User Pool
CREATE_BUCKETS - Create S3 buckets
Values:
true,falseDefault:
falseEffect: Creates S3 buckets (use
truefor first deployment)
ADMIN_USERNAME - Create admin user
Values: Email address
Default: None
Effect: Creates and configures admin user during deployment
ADMIN_PASSWORD - Admin user password
Values: String meeting Cognito password policy
Default: None
Effect: Sets permanent password for admin user
Deployment Strategies¶
Blue-Green Deployment¶
Deploy to new organization, test, then switch:
# Deploy to "blue" org
ORG=myorg-blue ENV=prod make rs-deploy
# Test thoroughly
make test-jwt-aws ENV=prod ORG=myorg-blue
# If good, switch DNS/routing to blue
# Keep green as fallback
Canary Deployment¶
Deploy to subset of users first:
# Deploy to canary org
ORG=myorg-canary ENV=prod make rs-deploy
# Route 10% of traffic to canary
# Monitor metrics
# If stable, deploy to main
ORG=myorg ENV=prod make rs-deploy
Rolling Updates¶
Update organizations one at a time:
# Update org1
ORG=org1 ENV=prod make rs-deploy-function
# Test
make test-jwt-aws ENV=prod ORG=org1
# If successful, update org2
ORG=org2 ENV=prod make rs-deploy-function
# Repeat for all orgs
Troubleshooting Deployment¶
Build Failures¶
Stack Failures¶
Resource Conflicts¶
Performance Optimization¶
Speed Up Deployments¶
Use appropriate command:
Code only:
rs-deploy-function(30 sec)Config only:
rs-deploy-only(1-2 min)Full:
rs-deploy(5-7 min, or 30 sec with cache)
Keep requirements.txt stable:
Pin versions to avoid unexpected updates
Layer rebuild adds 4-5 minutes
Use build cache:
Don’t delete
.aws-sam-*unnecessarilyCache saves 4-5 minutes on layer builds
Parallel deployments:
Deploy multiple orgs simultaneously
Each uses isolated build directory
Monitor Performance¶
# Check Lambda cold start time
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Duration \
--dimensions Name=FunctionName,Value=rawscribe-stage-myorg-backend \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 300 \
--statistics Average,Maximum \
--region us-east-1