Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Deploy to DEV

on:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
timeout-minutes: 15

permissions:
contents: read
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version-file: .nvmrc
cache: 'npm'

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5.1.1
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN_DEV }}
aws-region: ${{ vars.AWS_REGION }}
role-session-name: deploy-dev-lambda-starter

- name: Install dependencies
run: npm ci

- name: Build application
run: npm run build

- name: Run tests
run: npm run test

- name: Install infrastructure dependencies
working-directory: ./infrastructure
run: npm ci

- name: Create infrastructure .env file
working-directory: ./infrastructure
run: echo "${{ vars.CDK_ENV_DEV }}" > .env

- name: Build infrastructure
working-directory: ./infrastructure
run: npm run build

- name: Bootstrap CDK (if needed)
working-directory: ./infrastructure
run: |
# Check if bootstrap is needed
if ! aws cloudformation describe-stacks --stack-name CDKToolkit --region ${{ vars.AWS_REGION }} >/dev/null 2>&1; then
echo "Bootstrapping CDK..."
npm run bootstrap
else
echo "CDK already bootstrapped"
fi

- name: Synthesize CDK stacks
working-directory: ./infrastructure
run: npm run synth

- name: Deploy CDK stacks
working-directory: ./infrastructure
run: npm run deploy:all -- --require-approval never --progress events

# Final Step: Clean up sensitive infrastructure files
- name: Clean up sensitive files
if: always()
working-directory: ./infrastructure
run: |
echo "🧹 Cleaning up sensitive files..."
rm -f .env
rm -rf cdk.out
echo "✅ Sensitive files cleaned up"
56 changes: 47 additions & 9 deletions docs/InfrastructureGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,45 @@ const tableName = process.env.TASK_TABLE_NAME;
- `prd`: `RETAIN` (logs preserved on stack deletion)
- Other environments: `DESTROY` (logs deleted on stack deletion)

**IAM Permissions**:

- **DynamoDB**: Read access (Scan) to the Task table
- **CloudWatch Logs**: Write access to its log group

#### Create Task Function

**Resource Type**: AWS Lambda Function

**Configuration**:

- **Function Name**: `{app-name}-create-task-{env}`
- **Runtime**: Node.js 24.x
- **Handler**: `handler` (bundled with esbuild)
- **Memory**: 256 MB
- **Timeout**: 10 seconds
- **Log Format**: JSON (structured logging)
- **Bundling**: Automatic TypeScript compilation with esbuild
- **Environment Variables**:
- `TASKS_TABLE`: DynamoDB table name
- `ENABLE_LOGGING`: Logging enabled flag (from `CDK_APP_ENABLE_LOGGING`)
- `LOG_LEVEL`: Minimum log level (from `CDK_APP_LOGGING_LEVEL`)
- `LOG_FORMAT`: Log output format (from `CDK_APP_LOGGING_FORMAT`)

**CloudWatch Logs**:

- **Log Group**: `/aws/lambda/{app-name}-create-task-{env}`
- **Log Retention**:
- `prd`: 30 days
- Other environments: 7 days
- **Removal Policy**:
- `prd`: `RETAIN` (logs preserved on stack deletion)
- Other environments: `DESTROY` (logs deleted on stack deletion)

**IAM Permissions**:

- **DynamoDB**: Write access (PutItem) to the Task table
- **CloudWatch Logs**: Write access to its log group

#### Lambda Starter API

**Resource Type**: API Gateway REST API
Expand All @@ -440,14 +479,20 @@ const tableName = process.env.TASK_TABLE_NAME;
**API Resources**:

- **GET /tasks**: List all tasks
- Integration: Lambda proxy integration
- Integration: Lambda proxy integration with List Tasks Function
- Response: JSON array of tasks

- **POST /tasks**: Create a new task
- Integration: Lambda proxy integration with Create Task Function
- Request Body: JSON object with task properties (title, description, status)
- Response: JSON object with created task including ID and timestamps

**Outputs**:

- `ApiUrl`: The API Gateway endpoint URL (e.g., `https://abc123.execute-api.us-east-1.amazonaws.com/dev/`)
- `ApiId`: The API Gateway ID
- `ListTasksFunctionArn`: The Lambda function ARN
- `ListTasksFunctionArn`: The List Tasks Lambda function ARN
- `CreateTaskFunctionArn`: The Create Task Lambda function ARN

**Logging Configuration**:

Expand Down Expand Up @@ -486,13 +531,6 @@ CDK_APP_LOGGING_FORMAT=json
CDK_APP_ENABLE_LOGGING=false
```

**IAM Permissions**:

The Lambda function is granted:

- **DynamoDB**: Read access (Scan) to the Task table
- **CloudWatch Logs**: Write access to its log group

### Resource Tagging

All resources are automatically tagged:
Expand Down
42 changes: 42 additions & 0 deletions infrastructure/stacks/lambda-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ describe('LambdaStack', () => {
});
});

it('should create a create task Lambda function', () => {
template.hasResourceProperties('AWS::Lambda::Function', {
FunctionName: 'lambda-starter-create-task-dev',
Runtime: 'nodejs24.x',
Handler: 'handler',
Timeout: 10,
MemorySize: 256,
});
});

it('should configure Lambda environment variables', () => {
template.hasResourceProperties('AWS::Lambda::Function', {
Environment: {
Expand Down Expand Up @@ -90,6 +100,12 @@ describe('LambdaStack', () => {
});
});

it('should create a POST method on /tasks', () => {
template.hasResourceProperties('AWS::ApiGateway::Method', {
HttpMethod: 'POST',
});
});

it('should integrate API Gateway with Lambda', () => {
template.hasResourceProperties('AWS::ApiGateway::Method', {
Integration: {
Expand Down Expand Up @@ -136,6 +152,24 @@ describe('LambdaStack', () => {
});
});

it('should grant Lambda write access to DynamoDB', () => {
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: Match.arrayWith([
Match.objectLike({
Action: [
'dynamodb:BatchWriteItem',
'dynamodb:PutItem',
'dynamodb:UpdateItem',
'dynamodb:DeleteItem',
'dynamodb:DescribeTable',
],
}),
]),
},
});
});

it('should export API URL', () => {
template.hasOutput('ApiUrl', {
Export: {
Expand All @@ -159,6 +193,14 @@ describe('LambdaStack', () => {
},
});
});

it('should export create task function ARN', () => {
template.hasOutput('CreateTaskFunctionArn', {
Export: {
Name: 'dev-create-task-function-arn',
},
});
});
});

describe('prd environment', () => {
Expand Down
46 changes: 46 additions & 0 deletions infrastructure/stacks/lambda-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export class LambdaStack extends cdk.Stack {
*/
public readonly listTasksFunction: NodejsFunction;

/**
* The create task Lambda function.
*/
public readonly createTaskFunction: NodejsFunction;

constructor(scope: Construct, id: string, props: LambdaStackProps) {
super(scope, id, props);

Expand Down Expand Up @@ -90,6 +95,37 @@ export class LambdaStack extends cdk.Stack {
// Grant the Lambda function read access to the DynamoDB table
props.taskTable.grantReadData(this.listTasksFunction);

// Create the create task Lambda function
this.createTaskFunction = new NodejsFunction(this, 'CreateTaskFunction', {
functionName: `${props.appName}-create-task-${props.envName}`,
runtime: lambda.Runtime.NODEJS_24_X,
handler: 'handler',
entry: path.join(__dirname, '../../src/handlers/create-task.ts'),
environment: {
TASKS_TABLE: props.taskTable.tableName,
ENABLE_LOGGING: props.enableLogging.toString(),
LOG_LEVEL: props.loggingLevel,
LOG_FORMAT: props.loggingFormat,
},
timeout: cdk.Duration.seconds(10),
memorySize: 256,
bundling: {
minify: true,
sourceMap: true,
},
loggingFormat: lambda.LoggingFormat.JSON,
applicationLogLevelV2: lambda.ApplicationLogLevel.INFO,
systemLogLevelV2: lambda.SystemLogLevel.INFO,
logGroup: new logs.LogGroup(this, 'CreateTaskFunctionLogGroup', {
logGroupName: `/aws/lambda/${props.appName}-create-task-${props.envName}`,
retention: props.envName === 'prd' ? logs.RetentionDays.ONE_MONTH : logs.RetentionDays.ONE_WEEK,
removalPolicy: props.envName === 'prd' ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
}),
});

// Grant the Lambda function write access to the DynamoDB table
props.taskTable.grantWriteData(this.createTaskFunction);

// Create API Gateway REST API
this.api = new apigateway.RestApi(this, 'LambdaStarterApi', {
restApiName: `${props.appName}-api-${props.envName}`,
Expand All @@ -112,6 +148,9 @@ export class LambdaStack extends cdk.Stack {
// Add GET method to /tasks
tasksResource.addMethod('GET', new apigateway.LambdaIntegration(this.listTasksFunction));

// Add POST method to /tasks
tasksResource.addMethod('POST', new apigateway.LambdaIntegration(this.createTaskFunction));

// Output the API URL
new cdk.CfnOutput(this, 'ApiUrl', {
value: this.api.url,
Expand All @@ -132,5 +171,12 @@ export class LambdaStack extends cdk.Stack {
description: 'ARN of the list tasks Lambda function',
exportName: `${props.envName}-list-tasks-function-arn`,
});

// Output the create task function ARN
new cdk.CfnOutput(this, 'CreateTaskFunctionArn', {
value: this.createTaskFunction.functionArn,
description: 'ARN of the create task Lambda function',
exportName: `${props.envName}-create-task-function-arn`,
});
}
}
Loading