Files
claude-skills-reference/engineering-team/aws-solution-architect/scripts/serverless_stack.py
Alireza Rezvani c7dc957823 fix(skill): restructure aws-solution-architect for better organization (#61) (#114)
Complete restructure based on AI Agent Skills Benchmark feedback (original score: 66/100):

## Directory Reorganization
- Moved Python scripts to scripts/ directory
- Moved sample files to assets/ directory
- Created references/ directory with extracted content
- Removed HOW_TO_USE.md (integrated into SKILL.md)
- Removed __pycache__

## New Reference Files (3 files)
- architecture_patterns.md: 6 AWS patterns (serverless, microservices, three-tier,
  data processing, GraphQL, multi-region) with diagrams, cost breakdowns, pros/cons
- service_selection.md: Decision matrices for compute, database, storage, messaging,
  networking, security services with code examples
- best_practices.md: Serverless design, cost optimization, security hardening,
  scalability patterns, common pitfalls

## SKILL.md Rewrite
- Reduced from 345 lines to 307 lines (moved patterns to references/)
- Added trigger phrases to description ("design serverless architecture",
  "create CloudFormation templates", "optimize AWS costs")
- Structured around 6-step workflow instead of encyclopedia format
- Added Quick Start examples (MVP, Scaling, Cost Optimization, IaC)
- Removed marketing language ("Expert", "comprehensive")
- Consistent imperative voice throughout

## Structure Changes
- scripts/: architecture_designer.py, cost_optimizer.py, serverless_stack.py
- references/: architecture_patterns.md, service_selection.md, best_practices.md
- assets/: sample_input.json, expected_output.json

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 02:42:08 +01:00

664 lines
17 KiB
Python

"""
Serverless stack generator for AWS.
Creates CloudFormation/CDK templates for serverless applications.
"""
from typing import Dict, List, Any, Optional
class ServerlessStackGenerator:
"""Generate serverless application stacks."""
def __init__(self, app_name: str, requirements: Dict[str, Any]):
"""
Initialize with application requirements.
Args:
app_name: Application name (used for resource naming)
requirements: Dictionary with API, database, auth requirements
"""
self.app_name = app_name.lower().replace(' ', '-')
self.requirements = requirements
self.region = requirements.get('region', 'us-east-1')
def generate_cloudformation_template(self) -> str:
"""
Generate CloudFormation template for serverless stack.
Returns:
YAML CloudFormation template as string
"""
template = f"""AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless stack for {self.app_name}
Parameters:
Environment:
Type: String
Default: dev
AllowedValues:
- dev
- staging
- production
Description: Deployment environment
CorsAllowedOrigins:
Type: String
Default: '*'
Description: CORS allowed origins for API Gateway
Resources:
# DynamoDB Table
{self.app_name.replace('-', '')}Table:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub '${{Environment}}-{self.app_name}-data'
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
SSESpecification:
SSEEnabled: true
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Tags:
- Key: Environment
Value: !Ref Environment
- Key: Application
Value: {self.app_name}
# Lambda Execution Role
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: DynamoDBAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
Resource: !GetAtt {self.app_name.replace('-', '')}Table.Arn
# Lambda Function
ApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${{Environment}}-{self.app_name}-api'
Handler: index.handler
Runtime: nodejs18.x
CodeUri: ./src
MemorySize: 512
Timeout: 10
Role: !GetAtt LambdaExecutionRole.Arn
Environment:
Variables:
TABLE_NAME: !Ref {self.app_name.replace('-', '')}Table
ENVIRONMENT: !Ref Environment
Events:
ApiEvent:
Type: Api
Properties:
Path: /{{proxy+}}
Method: ANY
RestApiId: !Ref ApiGateway
Tags:
Environment: !Ref Environment
Application: {self.app_name}
# API Gateway
ApiGateway:
Type: AWS::Serverless::Api
Properties:
Name: !Sub '${{Environment}}-{self.app_name}-api'
StageName: !Ref Environment
Cors:
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
AllowOrigin: !Sub "'${{CorsAllowedOrigins}}'"
Auth:
DefaultAuthorizer: CognitoAuthorizer
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
ThrottleSettings:
BurstLimit: 200
RateLimit: 100
Tags:
Environment: !Ref Environment
Application: {self.app_name}
# Cognito User Pool
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Sub '${{Environment}}-{self.app_name}-users'
UsernameAttributes:
- email
AutoVerifiedAttributes:
- email
Policies:
PasswordPolicy:
MinimumLength: 8
RequireUppercase: true
RequireLowercase: true
RequireNumbers: true
RequireSymbols: false
MfaConfiguration: OPTIONAL
EnabledMfas:
- SOFTWARE_TOKEN_MFA
UserAttributeUpdateSettings:
AttributesRequireVerificationBeforeUpdate:
- email
Schema:
- Name: email
Required: true
Mutable: true
# Cognito User Pool Client
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: !Sub '${{Environment}}-{self.app_name}-client'
UserPoolId: !Ref UserPool
GenerateSecret: false
RefreshTokenValidity: 30
AccessTokenValidity: 1
IdTokenValidity: 1
TokenValidityUnits:
RefreshToken: days
AccessToken: hours
IdToken: hours
ExplicitAuthFlows:
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
# CloudWatch Log Group
ApiLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/aws/lambda/${{Environment}}-{self.app_name}-api'
RetentionInDays: 7
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub 'https://${{ApiGateway}}.execute-api.${{AWS::Region}}.amazonaws.com/${{Environment}}'
Export:
Name: !Sub '${{Environment}}-{self.app_name}-ApiUrl'
UserPoolId:
Description: Cognito User Pool ID
Value: !Ref UserPool
Export:
Name: !Sub '${{Environment}}-{self.app_name}-UserPoolId'
UserPoolClientId:
Description: Cognito User Pool Client ID
Value: !Ref UserPoolClient
Export:
Name: !Sub '${{Environment}}-{self.app_name}-UserPoolClientId'
TableName:
Description: DynamoDB Table Name
Value: !Ref {self.app_name.replace('-', '')}Table
Export:
Name: !Sub '${{Environment}}-{self.app_name}-TableName'
"""
return template
def generate_cdk_stack(self) -> str:
"""
Generate AWS CDK stack in TypeScript.
Returns:
CDK stack code as string
"""
stack = f"""import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import {{ Construct }} from 'constructs';
export class {self.app_name.replace('-', '').title()}Stack extends cdk.Stack {{
constructor(scope: Construct, id: string, props?: cdk.StackProps) {{
super(scope, id, props);
// DynamoDB Table
const table = new dynamodb.Table(this, '{self.app_name}Table', {{
tableName: `${{cdk.Stack.of(this).stackName}}-data`,
partitionKey: {{ name: 'PK', type: dynamodb.AttributeType.STRING }},
sortKey: {{ name: 'SK', type: dynamodb.AttributeType.STRING }},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
pointInTimeRecovery: true,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
removalPolicy: cdk.RemovalPolicy.RETAIN,
}});
// Cognito User Pool
const userPool = new cognito.UserPool(this, '{self.app_name}UserPool', {{
userPoolName: `${{cdk.Stack.of(this).stackName}}-users`,
selfSignUpEnabled: true,
signInAliases: {{ email: true }},
autoVerify: {{ email: true }},
passwordPolicy: {{
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: false,
}},
mfa: cognito.Mfa.OPTIONAL,
mfaSecondFactor: {{
sms: false,
otp: true,
}},
removalPolicy: cdk.RemovalPolicy.RETAIN,
}});
const userPoolClient = userPool.addClient('{self.app_name}Client', {{
authFlows: {{
userSrp: true,
}},
accessTokenValidity: cdk.Duration.hours(1),
refreshTokenValidity: cdk.Duration.days(30),
}});
// Lambda Function
const apiFunction = new lambda.Function(this, '{self.app_name}ApiFunction', {{
functionName: `${{cdk.Stack.of(this).stackName}}-api`,
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('./src'),
memorySize: 512,
timeout: cdk.Duration.seconds(10),
environment: {{
TABLE_NAME: table.tableName,
USER_POOL_ID: userPool.userPoolId,
}},
logRetention: 7, // days
}});
// Grant Lambda permissions to DynamoDB
table.grantReadWriteData(apiFunction);
// API Gateway
const api = new apigateway.RestApi(this, '{self.app_name}Api', {{
restApiName: `${{cdk.Stack.of(this).stackName}}-api`,
description: 'API for {self.app_name}',
defaultCorsPreflightOptions: {{
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: ['Content-Type', 'Authorization'],
}},
deployOptions: {{
stageName: 'prod',
throttlingRateLimit: 100,
throttlingBurstLimit: 200,
metricsEnabled: true,
loggingLevel: apigateway.MethodLoggingLevel.INFO,
}},
}});
// Cognito Authorizer
const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'ApiAuthorizer', {{
cognitoUserPools: [userPool],
}});
// API Integration
const integration = new apigateway.LambdaIntegration(apiFunction);
// Add proxy resource (/{{proxy+}})
const proxyResource = api.root.addProxy({{
defaultIntegration: integration,
anyMethod: true,
defaultMethodOptions: {{
authorizer: authorizer,
authorizationType: apigateway.AuthorizationType.COGNITO,
}},
}});
// Outputs
new cdk.CfnOutput(this, 'ApiUrl', {{
value: api.url,
description: 'API Gateway URL',
}});
new cdk.CfnOutput(this, 'UserPoolId', {{
value: userPool.userPoolId,
description: 'Cognito User Pool ID',
}});
new cdk.CfnOutput(this, 'UserPoolClientId', {{
value: userPoolClient.userPoolClientId,
description: 'Cognito User Pool Client ID',
}});
new cdk.CfnOutput(this, 'TableName', {{
value: table.tableName,
description: 'DynamoDB Table Name',
}});
}}
}}
"""
return stack
def generate_terraform_configuration(self) -> str:
"""
Generate Terraform configuration for serverless stack.
Returns:
Terraform HCL configuration as string
"""
terraform = f"""terraform {{
required_version = ">= 1.0"
required_providers {{
aws = {{
source = "hashicorp/aws"
version = "~> 5.0"
}}
}}
}}
provider "aws" {{
region = var.aws_region
}}
variable "aws_region" {{
description = "AWS region"
type = string
default = "{self.region}"
}}
variable "environment" {{
description = "Environment name"
type = string
default = "dev"
}}
variable "app_name" {{
description = "Application name"
type = string
default = "{self.app_name}"
}}
# DynamoDB Table
resource "aws_dynamodb_table" "main" {{
name = "${{var.environment}}-${{var.app_name}}-data"
billing_mode = "PAY_PER_REQUEST"
hash_key = "PK"
range_key = "SK"
attribute {{
name = "PK"
type = "S"
}}
attribute {{
name = "SK"
type = "S"
}}
server_side_encryption {{
enabled = true
}}
point_in_time_recovery {{
enabled = true
}}
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
tags = {{
Environment = var.environment
Application = var.app_name
}}
}}
# Cognito User Pool
resource "aws_cognito_user_pool" "main" {{
name = "${{var.environment}}-${{var.app_name}}-users"
username_attributes = ["email"]
auto_verified_attributes = ["email"]
password_policy {{
minimum_length = 8
require_lowercase = true
require_numbers = true
require_uppercase = true
require_symbols = false
}}
mfa_configuration = "OPTIONAL"
software_token_mfa_configuration {{
enabled = true
}}
schema {{
name = "email"
attribute_data_type = "String"
required = true
mutable = true
}}
tags = {{
Environment = var.environment
Application = var.app_name
}}
}}
resource "aws_cognito_user_pool_client" "main" {{
name = "${{var.environment}}-${{var.app_name}}-client"
user_pool_id = aws_cognito_user_pool.main.id
generate_secret = false
explicit_auth_flows = [
"ALLOW_USER_SRP_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH"
]
refresh_token_validity = 30
access_token_validity = 1
id_token_validity = 1
token_validity_units {{
refresh_token = "days"
access_token = "hours"
id_token = "hours"
}}
}}
# IAM Role for Lambda
resource "aws_iam_role" "lambda" {{
name = "${{var.environment}}-${{var.app_name}}-lambda-role"
assume_role_policy = jsonencode({{
Version = "2012-10-17"
Statement = [{{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {{
Service = "lambda.amazonaws.com"
}}
}}]
}})
tags = {{
Environment = var.environment
Application = var.app_name
}}
}}
resource "aws_iam_role_policy_attachment" "lambda_basic" {{
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}}
resource "aws_iam_role_policy" "dynamodb" {{
name = "dynamodb-access"
role = aws_iam_role.lambda.id
policy = jsonencode({{
Version = "2012-10-17"
Statement = [{{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
]
Resource = aws_dynamodb_table.main.arn
}}]
}})
}}
# Lambda Function
resource "aws_lambda_function" "api" {{
filename = "lambda.zip"
function_name = "${{var.environment}}-${{var.app_name}}-api"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "nodejs18.x"
memory_size = 512
timeout = 10
environment {{
variables = {{
TABLE_NAME = aws_dynamodb_table.main.name
USER_POOL_ID = aws_cognito_user_pool.main.id
ENVIRONMENT = var.environment
}}
}}
tags = {{
Environment = var.environment
Application = var.app_name
}}
}}
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "lambda" {{
name = "/aws/lambda/${{aws_lambda_function.api.function_name}}"
retention_in_days = 7
tags = {{
Environment = var.environment
Application = var.app_name
}}
}}
# API Gateway
resource "aws_api_gateway_rest_api" "main" {{
name = "${{var.environment}}-${{var.app_name}}-api"
description = "API for ${{var.app_name}}"
tags = {{
Environment = var.environment
Application = var.app_name
}}
}}
resource "aws_api_gateway_authorizer" "cognito" {{
name = "cognito-authorizer"
rest_api_id = aws_api_gateway_rest_api.main.id
type = "COGNITO_USER_POOLS"
provider_arns = [aws_cognito_user_pool.main.arn]
}}
resource "aws_api_gateway_resource" "proxy" {{
rest_api_id = aws_api_gateway_rest_api.main.id
parent_id = aws_api_gateway_rest_api.main.root_resource_id
path_part = "{{proxy+}}"
}}
resource "aws_api_gateway_method" "proxy" {{
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = "ANY"
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.cognito.id
}}
resource "aws_api_gateway_integration" "lambda" {{
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = aws_api_gateway_method.proxy.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api.invoke_arn
}}
resource "aws_lambda_permission" "apigw" {{
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${{aws_api_gateway_rest_api.main.execution_arn}}/*/*"
}}
resource "aws_api_gateway_deployment" "main" {{
depends_on = [
aws_api_gateway_integration.lambda
]
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = var.environment
}}
# Outputs
output "api_url" {{
description = "API Gateway URL"
value = aws_api_gateway_deployment.main.invoke_url
}}
output "user_pool_id" {{
description = "Cognito User Pool ID"
value = aws_cognito_user_pool.main.id
}}
output "user_pool_client_id" {{
description = "Cognito User Pool Client ID"
value = aws_cognito_user_pool_client.main.id
}}
output "table_name" {{
description = "DynamoDB Table Name"
value = aws_dynamodb_table.main.name
}}
"""
return terraform