Skip to content

Commit d63a699

Browse files
authored
Create endpoint (#4)
* Implement create task functionality with validation and tests * Add debug logging for ScanCommand and PutCommand in task service * Update Infrastructure Guide with Create Task function details and IAM permissions * Add GitHub Actions workflow for deploying to DEV environment * Refactor concurrency group in deploy workflow for consistency
1 parent 3245c18 commit d63a699

File tree

10 files changed

+1191
-12
lines changed

10 files changed

+1191
-12
lines changed

.github/workflows/deploy-dev.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Deploy to DEV
2+
3+
on:
4+
workflow_dispatch:
5+
6+
concurrency:
7+
group: ${{ github.workflow }}
8+
cancel-in-progress: false
9+
10+
jobs:
11+
deploy:
12+
name: Deploy
13+
runs-on: ubuntu-latest
14+
timeout-minutes: 15
15+
16+
permissions:
17+
contents: read
18+
id-token: write
19+
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v5
23+
24+
- name: Setup Node.js
25+
uses: actions/setup-node@v5
26+
with:
27+
node-version-file: .nvmrc
28+
cache: 'npm'
29+
30+
- name: Configure AWS credentials
31+
uses: aws-actions/configure-aws-credentials@v5.1.1
32+
with:
33+
role-to-assume: ${{ vars.AWS_ROLE_ARN_DEV }}
34+
aws-region: ${{ vars.AWS_REGION }}
35+
role-session-name: deploy-dev-lambda-starter
36+
37+
- name: Install dependencies
38+
run: npm ci
39+
40+
- name: Build application
41+
run: npm run build
42+
43+
- name: Run tests
44+
run: npm run test
45+
46+
- name: Install infrastructure dependencies
47+
working-directory: ./infrastructure
48+
run: npm ci
49+
50+
- name: Create infrastructure .env file
51+
working-directory: ./infrastructure
52+
run: echo "${{ vars.CDK_ENV_DEV }}" > .env
53+
54+
- name: Build infrastructure
55+
working-directory: ./infrastructure
56+
run: npm run build
57+
58+
- name: Bootstrap CDK (if needed)
59+
working-directory: ./infrastructure
60+
run: |
61+
# Check if bootstrap is needed
62+
if ! aws cloudformation describe-stacks --stack-name CDKToolkit --region ${{ vars.AWS_REGION }} >/dev/null 2>&1; then
63+
echo "Bootstrapping CDK..."
64+
npm run bootstrap
65+
else
66+
echo "CDK already bootstrapped"
67+
fi
68+
69+
- name: Synthesize CDK stacks
70+
working-directory: ./infrastructure
71+
run: npm run synth
72+
73+
- name: Deploy CDK stacks
74+
working-directory: ./infrastructure
75+
run: npm run deploy:all -- --require-approval never --progress events
76+
77+
# Final Step: Clean up sensitive infrastructure files
78+
- name: Clean up sensitive files
79+
if: always()
80+
working-directory: ./infrastructure
81+
run: |
82+
echo "🧹 Cleaning up sensitive files..."
83+
rm -f .env
84+
rm -rf cdk.out
85+
echo "✅ Sensitive files cleaned up"

docs/InfrastructureGuide.md

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,45 @@ const tableName = process.env.TASK_TABLE_NAME;
420420
- `prd`: `RETAIN` (logs preserved on stack deletion)
421421
- Other environments: `DESTROY` (logs deleted on stack deletion)
422422

423+
**IAM Permissions**:
424+
425+
- **DynamoDB**: Read access (Scan) to the Task table
426+
- **CloudWatch Logs**: Write access to its log group
427+
428+
#### Create Task Function
429+
430+
**Resource Type**: AWS Lambda Function
431+
432+
**Configuration**:
433+
434+
- **Function Name**: `{app-name}-create-task-{env}`
435+
- **Runtime**: Node.js 24.x
436+
- **Handler**: `handler` (bundled with esbuild)
437+
- **Memory**: 256 MB
438+
- **Timeout**: 10 seconds
439+
- **Log Format**: JSON (structured logging)
440+
- **Bundling**: Automatic TypeScript compilation with esbuild
441+
- **Environment Variables**:
442+
- `TASKS_TABLE`: DynamoDB table name
443+
- `ENABLE_LOGGING`: Logging enabled flag (from `CDK_APP_ENABLE_LOGGING`)
444+
- `LOG_LEVEL`: Minimum log level (from `CDK_APP_LOGGING_LEVEL`)
445+
- `LOG_FORMAT`: Log output format (from `CDK_APP_LOGGING_FORMAT`)
446+
447+
**CloudWatch Logs**:
448+
449+
- **Log Group**: `/aws/lambda/{app-name}-create-task-{env}`
450+
- **Log Retention**:
451+
- `prd`: 30 days
452+
- Other environments: 7 days
453+
- **Removal Policy**:
454+
- `prd`: `RETAIN` (logs preserved on stack deletion)
455+
- Other environments: `DESTROY` (logs deleted on stack deletion)
456+
457+
**IAM Permissions**:
458+
459+
- **DynamoDB**: Write access (PutItem) to the Task table
460+
- **CloudWatch Logs**: Write access to its log group
461+
423462
#### Lambda Starter API
424463

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

442481
- **GET /tasks**: List all tasks
443-
- Integration: Lambda proxy integration
482+
- Integration: Lambda proxy integration with List Tasks Function
444483
- Response: JSON array of tasks
445484

485+
- **POST /tasks**: Create a new task
486+
- Integration: Lambda proxy integration with Create Task Function
487+
- Request Body: JSON object with task properties (title, description, status)
488+
- Response: JSON object with created task including ID and timestamps
489+
446490
**Outputs**:
447491

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

452497
**Logging Configuration**:
453498

@@ -486,13 +531,6 @@ CDK_APP_LOGGING_FORMAT=json
486531
CDK_APP_ENABLE_LOGGING=false
487532
```
488533

489-
**IAM Permissions**:
490-
491-
The Lambda function is granted:
492-
493-
- **DynamoDB**: Read access (Scan) to the Task table
494-
- **CloudWatch Logs**: Write access to its log group
495-
496534
### Resource Tagging
497535

498536
All resources are automatically tagged:

infrastructure/stacks/lambda-stack.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ describe('LambdaStack', () => {
5757
});
5858
});
5959

60+
it('should create a create task Lambda function', () => {
61+
template.hasResourceProperties('AWS::Lambda::Function', {
62+
FunctionName: 'lambda-starter-create-task-dev',
63+
Runtime: 'nodejs24.x',
64+
Handler: 'handler',
65+
Timeout: 10,
66+
MemorySize: 256,
67+
});
68+
});
69+
6070
it('should configure Lambda environment variables', () => {
6171
template.hasResourceProperties('AWS::Lambda::Function', {
6272
Environment: {
@@ -90,6 +100,12 @@ describe('LambdaStack', () => {
90100
});
91101
});
92102

103+
it('should create a POST method on /tasks', () => {
104+
template.hasResourceProperties('AWS::ApiGateway::Method', {
105+
HttpMethod: 'POST',
106+
});
107+
});
108+
93109
it('should integrate API Gateway with Lambda', () => {
94110
template.hasResourceProperties('AWS::ApiGateway::Method', {
95111
Integration: {
@@ -136,6 +152,24 @@ describe('LambdaStack', () => {
136152
});
137153
});
138154

155+
it('should grant Lambda write access to DynamoDB', () => {
156+
template.hasResourceProperties('AWS::IAM::Policy', {
157+
PolicyDocument: {
158+
Statement: Match.arrayWith([
159+
Match.objectLike({
160+
Action: [
161+
'dynamodb:BatchWriteItem',
162+
'dynamodb:PutItem',
163+
'dynamodb:UpdateItem',
164+
'dynamodb:DeleteItem',
165+
'dynamodb:DescribeTable',
166+
],
167+
}),
168+
]),
169+
},
170+
});
171+
});
172+
139173
it('should export API URL', () => {
140174
template.hasOutput('ApiUrl', {
141175
Export: {
@@ -159,6 +193,14 @@ describe('LambdaStack', () => {
159193
},
160194
});
161195
});
196+
197+
it('should export create task function ARN', () => {
198+
template.hasOutput('CreateTaskFunctionArn', {
199+
Export: {
200+
Name: 'dev-create-task-function-arn',
201+
},
202+
});
203+
});
162204
});
163205

164206
describe('prd environment', () => {

infrastructure/stacks/lambda-stack.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export class LambdaStack extends cdk.Stack {
5656
*/
5757
public readonly listTasksFunction: NodejsFunction;
5858

59+
/**
60+
* The create task Lambda function.
61+
*/
62+
public readonly createTaskFunction: NodejsFunction;
63+
5964
constructor(scope: Construct, id: string, props: LambdaStackProps) {
6065
super(scope, id, props);
6166

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

98+
// Create the create task Lambda function
99+
this.createTaskFunction = new NodejsFunction(this, 'CreateTaskFunction', {
100+
functionName: `${props.appName}-create-task-${props.envName}`,
101+
runtime: lambda.Runtime.NODEJS_24_X,
102+
handler: 'handler',
103+
entry: path.join(__dirname, '../../src/handlers/create-task.ts'),
104+
environment: {
105+
TASKS_TABLE: props.taskTable.tableName,
106+
ENABLE_LOGGING: props.enableLogging.toString(),
107+
LOG_LEVEL: props.loggingLevel,
108+
LOG_FORMAT: props.loggingFormat,
109+
},
110+
timeout: cdk.Duration.seconds(10),
111+
memorySize: 256,
112+
bundling: {
113+
minify: true,
114+
sourceMap: true,
115+
},
116+
loggingFormat: lambda.LoggingFormat.JSON,
117+
applicationLogLevelV2: lambda.ApplicationLogLevel.INFO,
118+
systemLogLevelV2: lambda.SystemLogLevel.INFO,
119+
logGroup: new logs.LogGroup(this, 'CreateTaskFunctionLogGroup', {
120+
logGroupName: `/aws/lambda/${props.appName}-create-task-${props.envName}`,
121+
retention: props.envName === 'prd' ? logs.RetentionDays.ONE_MONTH : logs.RetentionDays.ONE_WEEK,
122+
removalPolicy: props.envName === 'prd' ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
123+
}),
124+
});
125+
126+
// Grant the Lambda function write access to the DynamoDB table
127+
props.taskTable.grantWriteData(this.createTaskFunction);
128+
93129
// Create API Gateway REST API
94130
this.api = new apigateway.RestApi(this, 'LambdaStarterApi', {
95131
restApiName: `${props.appName}-api-${props.envName}`,
@@ -112,6 +148,9 @@ export class LambdaStack extends cdk.Stack {
112148
// Add GET method to /tasks
113149
tasksResource.addMethod('GET', new apigateway.LambdaIntegration(this.listTasksFunction));
114150

151+
// Add POST method to /tasks
152+
tasksResource.addMethod('POST', new apigateway.LambdaIntegration(this.createTaskFunction));
153+
115154
// Output the API URL
116155
new cdk.CfnOutput(this, 'ApiUrl', {
117156
value: this.api.url,
@@ -132,5 +171,12 @@ export class LambdaStack extends cdk.Stack {
132171
description: 'ARN of the list tasks Lambda function',
133172
exportName: `${props.envName}-list-tasks-function-arn`,
134173
});
174+
175+
// Output the create task function ARN
176+
new cdk.CfnOutput(this, 'CreateTaskFunctionArn', {
177+
value: this.createTaskFunction.functionArn,
178+
description: 'ARN of the create task Lambda function',
179+
exportName: `${props.envName}-create-task-function-arn`,
180+
});
135181
}
136182
}

0 commit comments

Comments
 (0)