omgkit 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,51 +1,709 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aws
|
|
3
|
-
description: AWS services
|
|
3
|
+
description: AWS cloud services with Lambda, S3, DynamoDB, ECS, and infrastructure as code patterns
|
|
4
|
+
category: devops
|
|
5
|
+
triggers:
|
|
6
|
+
- aws
|
|
7
|
+
- amazon web services
|
|
8
|
+
- lambda
|
|
9
|
+
- s3
|
|
10
|
+
- dynamodb
|
|
11
|
+
- ecs
|
|
12
|
+
- cloudformation
|
|
13
|
+
- cdk
|
|
4
14
|
---
|
|
5
15
|
|
|
6
|
-
# AWS
|
|
16
|
+
# AWS
|
|
7
17
|
|
|
8
|
-
|
|
18
|
+
Enterprise-grade **AWS cloud development** following industry best practices. This skill covers Lambda functions, S3 storage, DynamoDB, ECS containers, API Gateway, infrastructure as code with CDK, and production-ready patterns used by top engineering teams.
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
|
|
22
|
+
Build scalable cloud applications on AWS:
|
|
23
|
+
|
|
24
|
+
- Deploy serverless functions with Lambda
|
|
25
|
+
- Store and retrieve data with S3 and DynamoDB
|
|
26
|
+
- Run containerized applications with ECS
|
|
27
|
+
- Build APIs with API Gateway
|
|
28
|
+
- Implement infrastructure as code with CDK
|
|
29
|
+
- Monitor applications with CloudWatch
|
|
30
|
+
- Secure resources with IAM
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
### 1. Lambda Functions
|
|
9
35
|
|
|
10
|
-
### Lambda
|
|
11
36
|
```typescript
|
|
12
|
-
|
|
13
|
-
|
|
37
|
+
// src/handlers/user.handler.ts
|
|
38
|
+
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
|
39
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
40
|
+
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
|
|
41
|
+
|
|
42
|
+
const client = new DynamoDBClient({});
|
|
43
|
+
const docClient = DynamoDBDocumentClient.from(client);
|
|
44
|
+
|
|
45
|
+
const TABLE_NAME = process.env.USERS_TABLE!;
|
|
14
46
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
interface User {
|
|
48
|
+
id: string;
|
|
49
|
+
email: string;
|
|
50
|
+
name: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Response helpers
|
|
55
|
+
const response = (statusCode: number, body: unknown): APIGatewayProxyResult => ({
|
|
56
|
+
statusCode,
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'Access-Control-Allow-Origin': '*',
|
|
60
|
+
'Access-Control-Allow-Credentials': true,
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(body),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const success = (data: unknown) => response(200, { data });
|
|
66
|
+
const created = (data: unknown) => response(201, { data });
|
|
67
|
+
const notFound = (message: string) => response(404, { error: { code: 'NOT_FOUND', message } });
|
|
68
|
+
const badRequest = (message: string) => response(400, { error: { code: 'BAD_REQUEST', message } });
|
|
69
|
+
const serverError = (error: Error) => response(500, { error: { code: 'SERVER_ERROR', message: error.message } });
|
|
70
|
+
|
|
71
|
+
// GET /users/{id}
|
|
72
|
+
export const getUser = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
|
73
|
+
try {
|
|
74
|
+
const userId = event.pathParameters?.id;
|
|
75
|
+
if (!userId) return badRequest('User ID is required');
|
|
76
|
+
|
|
77
|
+
const result = await docClient.send(new GetCommand({
|
|
78
|
+
TableName: TABLE_NAME,
|
|
79
|
+
Key: { id: userId },
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
if (!result.Item) {
|
|
83
|
+
return notFound(`User ${userId} not found`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return success(result.Item);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Error getting user:', error);
|
|
89
|
+
return serverError(error as Error);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// POST /users
|
|
94
|
+
export const createUser = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
|
95
|
+
try {
|
|
96
|
+
if (!event.body) return badRequest('Request body is required');
|
|
97
|
+
|
|
98
|
+
const body = JSON.parse(event.body);
|
|
99
|
+
const { email, name } = body;
|
|
100
|
+
|
|
101
|
+
if (!email || !name) {
|
|
102
|
+
return badRequest('Email and name are required');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const user: User = {
|
|
106
|
+
id: crypto.randomUUID(),
|
|
107
|
+
email: email.toLowerCase(),
|
|
108
|
+
name,
|
|
109
|
+
createdAt: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
await docClient.send(new PutCommand({
|
|
113
|
+
TableName: TABLE_NAME,
|
|
114
|
+
Item: user,
|
|
115
|
+
ConditionExpression: 'attribute_not_exists(id)',
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
return created(user);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('Error creating user:', error);
|
|
121
|
+
return serverError(error as Error);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// GET /users
|
|
126
|
+
export const listUsers = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
|
127
|
+
try {
|
|
128
|
+
const limit = parseInt(event.queryStringParameters?.limit || '20');
|
|
129
|
+
const lastKey = event.queryStringParameters?.cursor;
|
|
130
|
+
|
|
131
|
+
const params: any = {
|
|
132
|
+
TableName: TABLE_NAME,
|
|
133
|
+
Limit: limit,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (lastKey) {
|
|
137
|
+
params.ExclusiveStartKey = JSON.parse(Buffer.from(lastKey, 'base64').toString());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const result = await docClient.send(new QueryCommand(params));
|
|
141
|
+
|
|
142
|
+
const cursor = result.LastEvaluatedKey
|
|
143
|
+
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64')
|
|
144
|
+
: null;
|
|
145
|
+
|
|
146
|
+
return success({
|
|
147
|
+
items: result.Items,
|
|
148
|
+
cursor,
|
|
149
|
+
hasMore: !!result.LastEvaluatedKey,
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Error listing users:', error);
|
|
153
|
+
return serverError(error as Error);
|
|
154
|
+
}
|
|
19
155
|
};
|
|
20
156
|
```
|
|
21
157
|
|
|
22
|
-
### S3
|
|
158
|
+
### 2. S3 Operations
|
|
159
|
+
|
|
23
160
|
```typescript
|
|
24
|
-
|
|
161
|
+
// src/services/s3.service.ts
|
|
162
|
+
import {
|
|
163
|
+
S3Client,
|
|
164
|
+
PutObjectCommand,
|
|
165
|
+
GetObjectCommand,
|
|
166
|
+
DeleteObjectCommand,
|
|
167
|
+
ListObjectsV2Command,
|
|
168
|
+
CopyObjectCommand,
|
|
169
|
+
} from '@aws-sdk/client-s3';
|
|
170
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
171
|
+
import { Readable } from 'stream';
|
|
25
172
|
|
|
26
|
-
const
|
|
173
|
+
const s3Client = new S3Client({ region: process.env.AWS_REGION });
|
|
174
|
+
const BUCKET = process.env.S3_BUCKET!;
|
|
27
175
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
176
|
+
export class S3Service {
|
|
177
|
+
// Upload file
|
|
178
|
+
async upload(key: string, body: Buffer | Readable, contentType: string): Promise<string> {
|
|
179
|
+
await s3Client.send(new PutObjectCommand({
|
|
180
|
+
Bucket: BUCKET,
|
|
181
|
+
Key: key,
|
|
182
|
+
Body: body,
|
|
183
|
+
ContentType: contentType,
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
return `s3://${BUCKET}/${key}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Upload with metadata
|
|
190
|
+
async uploadWithMetadata(
|
|
191
|
+
key: string,
|
|
192
|
+
body: Buffer,
|
|
193
|
+
options: {
|
|
194
|
+
contentType: string;
|
|
195
|
+
metadata?: Record<string, string>;
|
|
196
|
+
tags?: Record<string, string>;
|
|
197
|
+
}
|
|
198
|
+
): Promise<string> {
|
|
199
|
+
await s3Client.send(new PutObjectCommand({
|
|
200
|
+
Bucket: BUCKET,
|
|
201
|
+
Key: key,
|
|
202
|
+
Body: body,
|
|
203
|
+
ContentType: options.contentType,
|
|
204
|
+
Metadata: options.metadata,
|
|
205
|
+
Tagging: options.tags
|
|
206
|
+
? Object.entries(options.tags).map(([k, v]) => `${k}=${v}`).join('&')
|
|
207
|
+
: undefined,
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
return `https://${BUCKET}.s3.amazonaws.com/${key}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Get file
|
|
214
|
+
async get(key: string): Promise<Buffer> {
|
|
215
|
+
const response = await s3Client.send(new GetObjectCommand({
|
|
216
|
+
Bucket: BUCKET,
|
|
217
|
+
Key: key,
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
const chunks: Buffer[] = [];
|
|
221
|
+
for await (const chunk of response.Body as Readable) {
|
|
222
|
+
chunks.push(chunk);
|
|
223
|
+
}
|
|
224
|
+
return Buffer.concat(chunks);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Generate presigned upload URL
|
|
228
|
+
async getUploadUrl(key: string, contentType: string, expiresIn = 3600): Promise<string> {
|
|
229
|
+
const command = new PutObjectCommand({
|
|
230
|
+
Bucket: BUCKET,
|
|
231
|
+
Key: key,
|
|
232
|
+
ContentType: contentType,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return getSignedUrl(s3Client, command, { expiresIn });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Generate presigned download URL
|
|
239
|
+
async getDownloadUrl(key: string, expiresIn = 3600): Promise<string> {
|
|
240
|
+
const command = new GetObjectCommand({
|
|
241
|
+
Bucket: BUCKET,
|
|
242
|
+
Key: key,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return getSignedUrl(s3Client, command, { expiresIn });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// List objects with prefix
|
|
249
|
+
async list(prefix: string, maxKeys = 1000): Promise<string[]> {
|
|
250
|
+
const response = await s3Client.send(new ListObjectsV2Command({
|
|
251
|
+
Bucket: BUCKET,
|
|
252
|
+
Prefix: prefix,
|
|
253
|
+
MaxKeys: maxKeys,
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
return response.Contents?.map(obj => obj.Key!) || [];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Delete file
|
|
260
|
+
async delete(key: string): Promise<void> {
|
|
261
|
+
await s3Client.send(new DeleteObjectCommand({
|
|
262
|
+
Bucket: BUCKET,
|
|
263
|
+
Key: key,
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Copy file
|
|
268
|
+
async copy(sourceKey: string, destKey: string): Promise<void> {
|
|
269
|
+
await s3Client.send(new CopyObjectCommand({
|
|
270
|
+
Bucket: BUCKET,
|
|
271
|
+
CopySource: `${BUCKET}/${sourceKey}`,
|
|
272
|
+
Key: destKey,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
33
276
|
```
|
|
34
277
|
|
|
35
|
-
### DynamoDB
|
|
278
|
+
### 3. DynamoDB Patterns
|
|
279
|
+
|
|
36
280
|
```typescript
|
|
37
|
-
|
|
281
|
+
// src/services/dynamodb.service.ts
|
|
282
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
283
|
+
import {
|
|
284
|
+
DynamoDBDocumentClient,
|
|
285
|
+
GetCommand,
|
|
286
|
+
PutCommand,
|
|
287
|
+
UpdateCommand,
|
|
288
|
+
DeleteCommand,
|
|
289
|
+
QueryCommand,
|
|
290
|
+
BatchWriteCommand,
|
|
291
|
+
TransactWriteCommand,
|
|
292
|
+
} from '@aws-sdk/lib-dynamodb';
|
|
38
293
|
|
|
39
294
|
const client = new DynamoDBClient({});
|
|
295
|
+
const docClient = DynamoDBDocumentClient.from(client, {
|
|
296
|
+
marshallOptions: { removeUndefinedValues: true },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
export class DynamoDBService<T extends { pk: string; sk: string }> {
|
|
300
|
+
constructor(private tableName: string) {}
|
|
301
|
+
|
|
302
|
+
async get(pk: string, sk: string): Promise<T | null> {
|
|
303
|
+
const result = await docClient.send(new GetCommand({
|
|
304
|
+
TableName: this.tableName,
|
|
305
|
+
Key: { pk, sk },
|
|
306
|
+
}));
|
|
307
|
+
return (result.Item as T) || null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async put(item: T): Promise<T> {
|
|
311
|
+
await docClient.send(new PutCommand({
|
|
312
|
+
TableName: this.tableName,
|
|
313
|
+
Item: item,
|
|
314
|
+
}));
|
|
315
|
+
return item;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async update(pk: string, sk: string, updates: Partial<T>): Promise<T> {
|
|
319
|
+
const updateExpressions: string[] = [];
|
|
320
|
+
const expressionNames: Record<string, string> = {};
|
|
321
|
+
const expressionValues: Record<string, unknown> = {};
|
|
322
|
+
|
|
323
|
+
Object.entries(updates).forEach(([key, value], index) => {
|
|
324
|
+
if (key !== 'pk' && key !== 'sk') {
|
|
325
|
+
updateExpressions.push(`#attr${index} = :val${index}`);
|
|
326
|
+
expressionNames[`#attr${index}`] = key;
|
|
327
|
+
expressionValues[`:val${index}`] = value;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const result = await docClient.send(new UpdateCommand({
|
|
332
|
+
TableName: this.tableName,
|
|
333
|
+
Key: { pk, sk },
|
|
334
|
+
UpdateExpression: `SET ${updateExpressions.join(', ')}`,
|
|
335
|
+
ExpressionAttributeNames: expressionNames,
|
|
336
|
+
ExpressionAttributeValues: expressionValues,
|
|
337
|
+
ReturnValues: 'ALL_NEW',
|
|
338
|
+
}));
|
|
339
|
+
|
|
340
|
+
return result.Attributes as T;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async delete(pk: string, sk: string): Promise<void> {
|
|
344
|
+
await docClient.send(new DeleteCommand({
|
|
345
|
+
TableName: this.tableName,
|
|
346
|
+
Key: { pk, sk },
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async query(
|
|
351
|
+
pk: string,
|
|
352
|
+
options?: {
|
|
353
|
+
skPrefix?: string;
|
|
354
|
+
limit?: number;
|
|
355
|
+
scanForward?: boolean;
|
|
356
|
+
}
|
|
357
|
+
): Promise<T[]> {
|
|
358
|
+
const params: any = {
|
|
359
|
+
TableName: this.tableName,
|
|
360
|
+
KeyConditionExpression: 'pk = :pk',
|
|
361
|
+
ExpressionAttributeValues: { ':pk': pk },
|
|
362
|
+
Limit: options?.limit,
|
|
363
|
+
ScanIndexForward: options?.scanForward ?? true,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (options?.skPrefix) {
|
|
367
|
+
params.KeyConditionExpression += ' AND begins_with(sk, :skPrefix)';
|
|
368
|
+
params.ExpressionAttributeValues[':skPrefix'] = options.skPrefix;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const result = await docClient.send(new QueryCommand(params));
|
|
372
|
+
return (result.Items as T[]) || [];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async batchWrite(items: T[]): Promise<void> {
|
|
376
|
+
const chunks = this.chunkArray(items, 25);
|
|
377
|
+
|
|
378
|
+
for (const chunk of chunks) {
|
|
379
|
+
await docClient.send(new BatchWriteCommand({
|
|
380
|
+
RequestItems: {
|
|
381
|
+
[this.tableName]: chunk.map(item => ({
|
|
382
|
+
PutRequest: { Item: item },
|
|
383
|
+
})),
|
|
384
|
+
},
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async transactWrite(operations: Array<{ type: 'put' | 'delete'; item: T }>): Promise<void> {
|
|
390
|
+
await docClient.send(new TransactWriteCommand({
|
|
391
|
+
TransactItems: operations.map(op => {
|
|
392
|
+
if (op.type === 'put') {
|
|
393
|
+
return { Put: { TableName: this.tableName, Item: op.item } };
|
|
394
|
+
}
|
|
395
|
+
return { Delete: { TableName: this.tableName, Key: { pk: op.item.pk, sk: op.item.sk } } };
|
|
396
|
+
}),
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
40
399
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
400
|
+
private chunkArray<U>(array: U[], size: number): U[][] {
|
|
401
|
+
const chunks: U[][] = [];
|
|
402
|
+
for (let i = 0; i < array.length; i += size) {
|
|
403
|
+
chunks.push(array.slice(i, i + size));
|
|
404
|
+
}
|
|
405
|
+
return chunks;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### 4. CDK Infrastructure
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// lib/app-stack.ts
|
|
414
|
+
import * as cdk from 'aws-cdk-lib';
|
|
415
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
416
|
+
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
417
|
+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
418
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
419
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
420
|
+
import { Construct } from 'constructs';
|
|
421
|
+
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
422
|
+
|
|
423
|
+
export class AppStack extends cdk.Stack {
|
|
424
|
+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
|
425
|
+
super(scope, id, props);
|
|
426
|
+
|
|
427
|
+
// DynamoDB Table
|
|
428
|
+
const usersTable = new dynamodb.Table(this, 'UsersTable', {
|
|
429
|
+
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
|
|
430
|
+
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
|
|
431
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
432
|
+
pointInTimeRecovery: true,
|
|
433
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
usersTable.addGlobalSecondaryIndex({
|
|
437
|
+
indexName: 'GSI1',
|
|
438
|
+
partitionKey: { name: 'gsi1pk', type: dynamodb.AttributeType.STRING },
|
|
439
|
+
sortKey: { name: 'gsi1sk', type: dynamodb.AttributeType.STRING },
|
|
440
|
+
projectionType: dynamodb.ProjectionType.ALL,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// S3 Bucket
|
|
444
|
+
const bucket = new s3.Bucket(this, 'AssetsBucket', {
|
|
445
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
446
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
447
|
+
versioned: true,
|
|
448
|
+
lifecycleRules: [
|
|
449
|
+
{
|
|
450
|
+
expiration: cdk.Duration.days(365),
|
|
451
|
+
noncurrentVersionExpiration: cdk.Duration.days(30),
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Lambda Functions
|
|
457
|
+
const getUserFn = new NodejsFunction(this, 'GetUserFunction', {
|
|
458
|
+
entry: 'src/handlers/user.handler.ts',
|
|
459
|
+
handler: 'getUser',
|
|
460
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
461
|
+
timeout: cdk.Duration.seconds(30),
|
|
462
|
+
memorySize: 256,
|
|
463
|
+
environment: {
|
|
464
|
+
USERS_TABLE: usersTable.tableName,
|
|
465
|
+
S3_BUCKET: bucket.bucketName,
|
|
466
|
+
},
|
|
467
|
+
tracing: lambda.Tracing.ACTIVE,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const createUserFn = new NodejsFunction(this, 'CreateUserFunction', {
|
|
471
|
+
entry: 'src/handlers/user.handler.ts',
|
|
472
|
+
handler: 'createUser',
|
|
473
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
474
|
+
timeout: cdk.Duration.seconds(30),
|
|
475
|
+
memorySize: 256,
|
|
476
|
+
environment: {
|
|
477
|
+
USERS_TABLE: usersTable.tableName,
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Grant permissions
|
|
482
|
+
usersTable.grantReadData(getUserFn);
|
|
483
|
+
usersTable.grantReadWriteData(createUserFn);
|
|
484
|
+
bucket.grantRead(getUserFn);
|
|
485
|
+
|
|
486
|
+
// API Gateway
|
|
487
|
+
const api = new apigateway.RestApi(this, 'UsersApi', {
|
|
488
|
+
restApiName: 'Users Service',
|
|
489
|
+
deployOptions: {
|
|
490
|
+
stageName: 'prod',
|
|
491
|
+
throttlingRateLimit: 1000,
|
|
492
|
+
throttlingBurstLimit: 500,
|
|
493
|
+
},
|
|
494
|
+
defaultCorsPreflightOptions: {
|
|
495
|
+
allowOrigins: apigateway.Cors.ALL_ORIGINS,
|
|
496
|
+
allowMethods: apigateway.Cors.ALL_METHODS,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const users = api.root.addResource('users');
|
|
501
|
+
users.addMethod('GET', new apigateway.LambdaIntegration(getUserFn));
|
|
502
|
+
users.addMethod('POST', new apigateway.LambdaIntegration(createUserFn));
|
|
503
|
+
|
|
504
|
+
const user = users.addResource('{id}');
|
|
505
|
+
user.addMethod('GET', new apigateway.LambdaIntegration(getUserFn));
|
|
506
|
+
|
|
507
|
+
// Outputs
|
|
508
|
+
new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
|
|
509
|
+
new cdk.CfnOutput(this, 'BucketName', { value: bucket.bucketName });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### 5. ECS Container Deployment
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
// lib/ecs-stack.ts
|
|
518
|
+
import * as cdk from 'aws-cdk-lib';
|
|
519
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
520
|
+
import * as ecs from 'aws-cdk-lib/aws-ecs';
|
|
521
|
+
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
|
|
522
|
+
import * as ecr from 'aws-cdk-lib/aws-ecr';
|
|
523
|
+
import { Construct } from 'constructs';
|
|
524
|
+
|
|
525
|
+
export class EcsStack extends cdk.Stack {
|
|
526
|
+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
|
527
|
+
super(scope, id, props);
|
|
528
|
+
|
|
529
|
+
// VPC
|
|
530
|
+
const vpc = new ec2.Vpc(this, 'AppVpc', {
|
|
531
|
+
maxAzs: 2,
|
|
532
|
+
natGateways: 1,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// ECS Cluster
|
|
536
|
+
const cluster = new ecs.Cluster(this, 'AppCluster', {
|
|
537
|
+
vpc,
|
|
538
|
+
containerInsights: true,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Fargate Service with ALB
|
|
542
|
+
const service = new ecsPatterns.ApplicationLoadBalancedFargateService(
|
|
543
|
+
this,
|
|
544
|
+
'AppService',
|
|
545
|
+
{
|
|
546
|
+
cluster,
|
|
547
|
+
taskImageOptions: {
|
|
548
|
+
image: ecs.ContainerImage.fromRegistry('nginx:latest'),
|
|
549
|
+
containerPort: 80,
|
|
550
|
+
environment: {
|
|
551
|
+
NODE_ENV: 'production',
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
desiredCount: 2,
|
|
555
|
+
cpu: 256,
|
|
556
|
+
memoryLimitMiB: 512,
|
|
557
|
+
publicLoadBalancer: true,
|
|
558
|
+
healthCheckGracePeriod: cdk.Duration.seconds(60),
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// Auto Scaling
|
|
563
|
+
const scaling = service.service.autoScaleTaskCount({
|
|
564
|
+
minCapacity: 2,
|
|
565
|
+
maxCapacity: 10,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
scaling.scaleOnCpuUtilization('CpuScaling', {
|
|
569
|
+
targetUtilizationPercent: 70,
|
|
570
|
+
scaleInCooldown: cdk.Duration.seconds(60),
|
|
571
|
+
scaleOutCooldown: cdk.Duration.seconds(60),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
scaling.scaleOnMemoryUtilization('MemoryScaling', {
|
|
575
|
+
targetUtilizationPercent: 80,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### 6. SQS and SNS Integration
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
// src/services/messaging.service.ts
|
|
585
|
+
import { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';
|
|
586
|
+
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
|
|
587
|
+
|
|
588
|
+
const sqsClient = new SQSClient({});
|
|
589
|
+
const snsClient = new SNSClient({});
|
|
590
|
+
|
|
591
|
+
export class MessagingService {
|
|
592
|
+
// Send message to SQS
|
|
593
|
+
async sendToQueue(queueUrl: string, message: unknown, delaySeconds = 0): Promise<string> {
|
|
594
|
+
const result = await sqsClient.send(new SendMessageCommand({
|
|
595
|
+
QueueUrl: queueUrl,
|
|
596
|
+
MessageBody: JSON.stringify(message),
|
|
597
|
+
DelaySeconds: delaySeconds,
|
|
598
|
+
}));
|
|
599
|
+
return result.MessageId!;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Receive messages from SQS
|
|
603
|
+
async receiveFromQueue(queueUrl: string, maxMessages = 10): Promise<Array<{ id: string; body: unknown; receiptHandle: string }>> {
|
|
604
|
+
const result = await sqsClient.send(new ReceiveMessageCommand({
|
|
605
|
+
QueueUrl: queueUrl,
|
|
606
|
+
MaxNumberOfMessages: maxMessages,
|
|
607
|
+
WaitTimeSeconds: 20,
|
|
608
|
+
}));
|
|
609
|
+
|
|
610
|
+
return (result.Messages || []).map(msg => ({
|
|
611
|
+
id: msg.MessageId!,
|
|
612
|
+
body: JSON.parse(msg.Body!),
|
|
613
|
+
receiptHandle: msg.ReceiptHandle!,
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Delete message from SQS
|
|
618
|
+
async deleteFromQueue(queueUrl: string, receiptHandle: string): Promise<void> {
|
|
619
|
+
await sqsClient.send(new DeleteMessageCommand({
|
|
620
|
+
QueueUrl: queueUrl,
|
|
621
|
+
ReceiptHandle: receiptHandle,
|
|
622
|
+
}));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Publish to SNS
|
|
626
|
+
async publishToTopic(topicArn: string, message: unknown, subject?: string): Promise<string> {
|
|
627
|
+
const result = await snsClient.send(new PublishCommand({
|
|
628
|
+
TopicArn: topicArn,
|
|
629
|
+
Message: JSON.stringify(message),
|
|
630
|
+
Subject: subject,
|
|
631
|
+
}));
|
|
632
|
+
return result.MessageId!;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Use Cases
|
|
638
|
+
|
|
639
|
+
### File Upload with Presigned URL
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
// Lambda handler for file upload
|
|
643
|
+
export const getUploadUrl = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
|
|
644
|
+
const { filename, contentType } = JSON.parse(event.body || '{}');
|
|
645
|
+
const userId = event.requestContext.authorizer?.claims?.sub;
|
|
646
|
+
|
|
647
|
+
const key = `uploads/${userId}/${Date.now()}-${filename}`;
|
|
648
|
+
const s3Service = new S3Service();
|
|
649
|
+
const uploadUrl = await s3Service.getUploadUrl(key, contentType);
|
|
650
|
+
|
|
651
|
+
return success({ uploadUrl, key });
|
|
652
|
+
};
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Event-Driven Processing
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
// SQS Event Handler
|
|
659
|
+
import { SQSHandler, SQSEvent } from 'aws-lambda';
|
|
660
|
+
|
|
661
|
+
export const processQueue: SQSHandler = async (event: SQSEvent) => {
|
|
662
|
+
for (const record of event.Records) {
|
|
663
|
+
const message = JSON.parse(record.body);
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
await processMessage(message);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error('Failed to process message:', error);
|
|
669
|
+
throw error; // Message returns to queue
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
};
|
|
45
673
|
```
|
|
46
674
|
|
|
47
675
|
## Best Practices
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
676
|
+
|
|
677
|
+
### Do's
|
|
678
|
+
|
|
679
|
+
- Use IAM roles instead of access keys
|
|
680
|
+
- Enable encryption at rest and in transit
|
|
681
|
+
- Use VPC for sensitive workloads
|
|
682
|
+
- Implement proper error handling and retries
|
|
683
|
+
- Use CloudWatch for monitoring and alerting
|
|
684
|
+
- Tag all resources for cost tracking
|
|
685
|
+
- Use infrastructure as code (CDK/CloudFormation)
|
|
686
|
+
- Implement least privilege access
|
|
687
|
+
- Use environment variables for configuration
|
|
688
|
+
- Enable X-Ray tracing for debugging
|
|
689
|
+
|
|
690
|
+
### Don'ts
|
|
691
|
+
|
|
692
|
+
- Don't hardcode credentials in code
|
|
693
|
+
- Don't use root account for deployments
|
|
694
|
+
- Don't expose S3 buckets publicly
|
|
695
|
+
- Don't skip encryption for sensitive data
|
|
696
|
+
- Don't ignore CloudWatch alarms
|
|
697
|
+
- Don't over-provision resources
|
|
698
|
+
- Don't skip VPC for production workloads
|
|
699
|
+
- Don't use synchronous invocations for long tasks
|
|
700
|
+
- Don't ignore cost optimization
|
|
701
|
+
- Don't skip backup and disaster recovery
|
|
702
|
+
|
|
703
|
+
## References
|
|
704
|
+
|
|
705
|
+
- [AWS Documentation](https://docs.aws.amazon.com/)
|
|
706
|
+
- [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/)
|
|
707
|
+
- [AWS SDK for JavaScript](https://docs.aws.amazon.com/sdk-for-javascript/)
|
|
708
|
+
- [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/)
|
|
709
|
+
- [AWS Best Practices](https://aws.amazon.com/architecture/best-practices/)
|