ساخت یک برنامه بدون سرور با کیت توسعه ابری AWS (CDK): API Gateway، Lambda، Layers و DynamoDB

Summarize this content to 400 words in Persian Lang
روز 007 – 100DaysAWSIaCDevopsChallenge
امروز در سری چالش 100 روز کد، زیرساخت دیگری را با استفاده از کیت توسعه ابری (CDK) حاوی سرویسهای AWS مانند دروازه API، توابع لامبدا، لایهها و DynamoDB ایجاد میکنم. این پروژه شامل ایجاد زیرساختی است که API ها را با استفاده از API Gateway در معرض دید قرار می دهد. این APIها در یک تابع Lambda مستقر خواهند شد و ما از DynamoDB برای ماندگاری داده ها استفاده خواهیم کرد. برای رسیدن به این هدف، مراحل زیر را دنبال می کنیم:
پروژه CDK را راه اندازی کنید: پروژه CDK را راه اندازی کنید و آن را برای زبان برنامه نویسی دلخواه خود پیکربندی کنید.
API Gateway را تعریف کنید: یک دروازه API برای رسیدگی به درخواست های HTTP و مسیریابی آنها به تابع Lambda ایجاد کنید.
ایجاد تابع Lambda: برای مدیریت منطق تجاری برای APIها، تابع Lambda را بنویسید و استقرار دهید.
لایه های لامبدا را اضافه کنید: برای به اشتراک گذاشتن کدهای مشترک بین چندین تابع Lambda، لایه های Lambda را تعریف و اضافه کنید.
DynamoDB را پیکربندی کنید: یک جدول DynamoDB برای ذخیره و بازیابی داده های مورد نیاز API ها تنظیم کنید.
خدمات را یکپارچه کنید: API Gateway را به تابع Lambda متصل کنید و تابع Lambda را برای تعامل با DynamoDB پیکربندی کنید.
زیرساخت را مستقر کنید: پشته CDK را در AWS مستقر کنید و عملکرد انتها به انتها API ها را آزمایش کنید.
پیش نیازها
کیت توسعه ابری AWS (CDK)
AWS SDK (نسخه جاوا اسکریپت)
API Gateway، Lambda، IAM و DynamoDB
تایپ اسکریپت
esbuild ↗
نمودار زیرساخت
پروژه CDK را راه اندازی کنید
ابتدا باید حداقل Nodejs 18.x↗ یا جدیدتر را نصب کنیم و یک پروفایل aws را پیکربندی کنیم. علاوه بر این، ما باید داکر را روی دستگاه میزبان خود نصب کنیم NodejsFunction ساختار CDK به آن نیاز دارد که توابع نوشته شده در TypeScript را با استفاده از جاوا اسکریپت بسازد esbuild.
npm install -g aws-cdk
mdkir day_007 && cd day_007
cdk init app –language typescript
cdk bootstrap –profile cdk-user
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
برای جزئیات بیشتر در مورد راه اندازی CDK، به Docs↗ بروید
جدول DynamoDB را پیکربندی کنید
DynamoDB یک سرویس پایگاه داده NoSQL مدیریت شده توسط خدمات وب آمازون (AWS) است. ما از آن برای تداوم دادههای خود استفاده خواهیم کرد، زیرا در مقایسه با RDS یا Aurora، کار با آن بسیار ساده و آسان است. جدول مورد نیاز ما دارای یک کلید ترکیبی (کلید اولیه و کلید مرتب سازی) خواهد بود. از آنجایی که DynamoDB یک پایگاه داده NoSQL است، لازم نیست ابتدا ویژگی های دیگر را فراتر از ویژگی های کلیدی مشخص کنید. ویژگی های جدول اضافی را می توان در طول زمان در صورت نیاز اضافه کرد.
همانطور که در بالا ذکر شد، جدول ما دارای یک کلید ترکیبی است که به دو کلید تقسیم می شود: hash key معروف به کلید پارتیشن (کلید اصلی) و range key به عنوان کلید مرتب سازی (بخش دوم کلید اصلی) شناخته می شود.
کلید هش
کلید برد
سایر صفات
شناسه
TodoName
CreatedAt, UpdatedAt, Owner
CDK
اجازه دهید جدول را با استفاده از CDK ایجاد کنید
import { aws_dynamodb as dynamodb } from “aws-cdk-lib”
/*
const props = {
…
tableName: “TodoApp”
}
*/
const table = new dynamodb.CfnTable(this, ‘DynamoDBTableResource’, {
tableName: props.tableName,
keySchema: [{
keyType: ‘HASH’,
attributeName: ‘ID’
}, {
keyType: ‘RANGE’,
attributeName: ‘TodoName’
}],
attributeDefinitions: [{
attributeName: ‘ID’,
attributeType: dynamodb.AttributeType.STRING
}, {
attributeName: ‘TodoName’,
attributeType: dynamodb.AttributeType.STRING
}],
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
tableClass: dynamodb.TableClass.STANDARD,
tags: [{
key: ‘Name’,
value: `DynamoDB-Table-${props.tableName}`
}]
})
// day_007/lib/cdk-stack.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
همانطور که می بینید، ما نیازی به تعیین ویژگی های _CreatedAt، _UpdatedAt و Owner در attributeDefinitions ویژگی.
ایجاد تابع Lambda
در داخل تابع Lambda، کد به ایجاد، بهروزرسانی و فهرست کردن موارد TodoList در DynamoDB میپردازد.
کد عملکرد
در زیر ساختار کد آمده است:
apps
├── domain
│ ├── abstract.domain.ts
│ ├── task-status.enum.ts
│ ├── task.domain.ts
│ └── todo-list.domain.ts
├── index.ts
├── layer
│ └── nodejs
│ ├── package-lock.json
│ └── package.json
├── lib
│ └── utils.ts
├── package-lock.json
├── package.json
├── src
│ └── infra
│ ├── dto
│ │ ├── lambda.response.ts
│ │ └── page.request.ts
│ └── storage
│ └── dynamodb
│ ├── abstract.repository.ts
│ └── todo-list.repository.ts
└── tsconfig.json
فایل هایی که در حال حاضر به اینجا نیاز داریم هستند index.ts، todo-list.repository.ts، و تمام فایل های داخل دامنه فهرست راهنما. من لینک کد کامل GitHub را در پایان این آموزش ارائه خواهم کرد.
apps/package.json
{
“main”: “index.ts”,
“type”: “commonjs”,
“dependencies”: {
“aws-lambda”: “^1.0.7”,
“@aws-sdk/client-dynamodb”: “^3.620.0”,
“@aws-sdk/lib-dynamodb”: “^3.620.0”,
“@aws-lambda/http”: “^1.0.1”,
“uuid”: “^10.0.0”,
“moment”: “^2.30.1”
},
“devDependencies”: {
“@types/node”: “^22.0.0”,
“@types/uuid”: “^10.0.0”,
“@types/aws-lambda”: “^8.10.142”,
“@types/moment”: “^2.13.0″
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
apps/domain/*.ts
// apps/domain/abstract.domain.ts
export interface IDomain {
id?: string;
createdAt?: string;
updatedAt?: string;
}
// apps/domain/todo-list.domain.ts
import { IDomain } from ‘./abstract.domain’
export interface TodoList extends IDomain {
name: string;
owner?: {
fullName: string,
email: string
},
tasks?: any[]
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
apps/src/infra/storage/dynamodb/todo-list.repository.ts
import { AbstractRepository } from ‘./abstract.repository’
import {
DeleteCommand,
DynamoDBDocumentClient,
GetCommand,
GetCommandOutput,
PutCommand,
PutCommandOutput,
QueryCommandOutput,
ScanCommand,
UpdateCommand
} from ‘@aws-sdk/lib-dynamodb’
import { v4 } from ‘uuid’
import moment from ‘moment’
import { TodoList } from ‘../../../../domain/todo-list.domain’
export class TodoListRepository implements AbstractRepositoryTodoList> {
constructor(private client: DynamoDBDocumentClient, private tableName: string) {
}
async create({ name, owner }: TodoList): PromiseTodoList> {
const item = {
ID: v4(),
CreatedAt: Math.floor((new Date().getTime()) / 1000),
UpdatedAt: Math.floor((new Date().getTime()) / 1000),
TodoName: name,
Owner: {
Fullname: owner?.fullName,
Email: owner?.email
}
}
let response: PutCommandOutput
try {
response = await this.client.send(new PutCommand({
TableName: this.tableName,
Item: item,
ExpressionAttributeNames: {
‘#nameAttr’: ‘TodoName’
},
ExpressionAttributeValues: {
‘:nameValue’: { ‘S’: name }
},
ConditionExpression: ‘#nameAttr :nameValue’
}))
} catch (e) {
console.error(e)
throw new Error(‘DataStorageException: ‘ + e.message)
}
console.log(‘PutCommandResponse’, response)
if (response.$metadata.httpStatusCode !== 200) {
throw new Error(‘DataStorage: Something wrong, please try again’)
}
return {
id: item.ID,
createdAt: this.convertTimestampToDate(item.CreatedAt),
updatedAt: this.convertTimestampToDate(item.UpdatedAt),
owner,
name: item.TodoName
}
}
async list(query: any): PromiseTodoList[]> {
let response: QueryCommandOutput
try {
response = await this.client.send(new ScanCommand({
TableName: this.tableName,
Limit: 25,
Select: ‘ALL_ATTRIBUTES’
}))
} catch (e) {
console.error(e)
throw new Error(‘DataStorageException: ‘ + e.message)
}
if (response.$metadata.httpStatusCode === 200 && response.Count! > 0) {
return […response.Items || []].map((value: any) => {
return {
id: value.ID,
name: value.TodoName,
createdAt: this.convertTimestampToDate(value.CreatedAt),
updatedAt: this.convertTimestampToDate(value.UpdatedAt),
owner: {
fullName: value.Owner.Fullname,
email: value.Owner.Email
},
tasks: value.Tasks!
}
})
}
return []
}
async update(id: { pk: string, sk?: string }, toUpdate: TodoList): PromiseTodoList> {
let response: PutCommandOutput
const updateDate = Math.floor((new Date().getTime()) / 1000)
try {
response = await this.client.send(new UpdateCommand({
TableName: this.tableName,
Key: {
ID: id.pk,
TodoName: id.sk
},
UpdateExpression: ‘SET #name=:newOrOldName, #owner.#email = :newOrOldEmail, #owner.#fullname = :newOrOldFullname’,
ExpressionAttributeNames: {
‘#name’: ‘TodoName’,
‘#owner’: ‘Owner’,
‘#fullname’: ‘FullName’,
‘#Email’: ‘Email’
},
ExpressionAttributeValues: {
‘:newOrOldName’: { ‘S’: toUpdate.name },
‘:newOrOldEmail’: { ‘S’: toUpdate.owner?.email },
‘:newOrOldFullname’: { ‘S’: toUpdate.owner?.fullName }
}
}))
} catch (e) {
console.error(e)
throw new Error(‘DataStorageException: ‘ + e.message)
}
console.debug(‘PutCommandOutput4Update’, response)
if (response.$metadata.httpStatusCode !== 200) {
throw new Error(‘DataStorageException: something wrong during update !!’)
}
return {
…toUpdate,
id: id.pk,
updatedAt: this.convertTimestampToDate(updateDate)
}
}
async get(id: { pk: string, sk?: string }): PromiseTodoList | null> {
let response: GetCommandOutput
try {
response = await this.client.send(new GetCommand({
TableName: this.tableName,
Key: {
ID: id.pk,
TodoName: id.sk
}
}))
} catch (e) {
console.error(e)
throw new Error(‘DataStorageException: ‘ + e.message)
}
console.debug(‘GetCommandOutput’, response)
if (response.$metadata.httpStatusCode !== 200) {
throw new Error(‘DataStorageException: something wrong during update !!’)
}
const value: any = response.Item
return !value ? null : {
id: value.ID,
name: value.TodoName,
createdAt: this.convertTimestampToDate(value.CreatedAt),
updatedAt: this.convertTimestampToDate(value.UpdatedAt),
owner: {
fullName: value.Owner.Fullname,
email: value.Owner.Email
},
tasks: value.Tasks!
}
}
private convertTimestampToDate = (time: number): string => moment.unix(time).format(‘YYYY-MM-DD HH:mm:ss’)
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
apps/index.ts
import { APIGatewayProxyEvent } from ‘aws-lambda’
import { isNull } from ‘utils’
import { LambdaResponse } from ‘./src/infra/dto/lambda.response’
import { TodoListRepository } from ‘./src/infra/storage/dynamodb/todo-list.repository’
import { DynamoDBDocumentClient } from ‘@aws-sdk/lib-dynamodb’
import { DynamoDBClient } from ‘@aws-sdk/client-dynamodb’
import { AbstractRepository } from ‘./src/infra/storage/dynamodb/abstract.repository’
import { TodoList } from ‘./domain/todo-list.domain’
const tableName = string>process.env.TABLE_NAME
const region = process.env.REGION ?? ‘us-east-1’
const dynamodbClient = DynamoDBDocumentClient.from(new DynamoDBClient({
region: region
}))
export const handler = async (event: APIGatewayProxyEvent, context: any) => {
const method = event.httpMethod
const path = event.path
console.log(‘incoming request’, { …event })
if (isNull(method) && isNull(path)) {
return {
statusCode: 500,
isBase64Encoded: false,
body: JSON.stringify({
message: ‘Something wrong !!’
})
}
}
const repo: AbstractRepositoryTodoList> = new TodoListRepository(dynamodbClient, tableName)
const requestBody = (() => {
if (event.isBase64Encoded) {
return /* global atob */ atob(event.body!)
}
return typeof event.body === ‘string’ ? JSON.parse(event.body) : event.body
})()
let response: LambdaResponse
if (method === ‘POST’ && path.endsWith(‘/create-todolist’)) {
// const existing = await repo.get?.()
const result = await repo.create({
name: requestBody.name,
owner: {
fullName: ‘Kevin L.’,
email: ‘kevin.kemta@test.com’
}
})
response = {
isBase64Encoded: false,
statusCode: 200,
body: JSON.stringify(result, null, -2)
}
} else if (path === ‘/todo-app-api/todolists’) {
const todos = await repo.list(”)
response = {
isBase64Encoded: false,
statusCode: 200,
body: JSON.stringify(todos, null, -2)
}
} else if (new RegExp(‘^/todo-app-api/todolists/[a-z0-9-]{36}/update-todolist$’, ‘gi’).test(path)) {
const old = await repo.get?.({
pk: event.pathParameters?.todoListId!,
sk: requestBody.name
})
if (!old) {
response = {
isBase64Encoded: false,
statusCode: 404,
body: JSON.stringify({
message: `The TodoList ${event.pathParameters?.todoListId!} doesn’t exists`
}, null, -2)
}
} else {
const updated = await repo.update?.({ pk: old.id!, sk: old.name }, {
name: requestBody.name ?? old.name,
owner: {
fullName: requestBody.owner?.fullName ?? old.owner?.fullName,
email: requestBody.owner?.email ?? old.owner?.email
}
})
response = {
isBase64Encoded: false,
statusCode: 200,
body: JSON.stringify(updated, null, -2)
}
}
} else {
return {
isBase64Encoded: false,
statusCode: 403,
body: JSON.stringify({
message: `The API ${method} ${path} is no yet implemented in lambda side`
}, null, -2)
}
}
return {
…response,
headers: {
‘Content-Type’: ‘application/json’,
‘Access-Control-Allow-Origin’: ‘*’
}
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
لایه Lambda را اضافه کنید
تمام وابستگی ها (node_modules) در لایه Lambda اضافه می شوند. دلیلی که من برای اضافه کردن وابستگی به لایه انتخاب کردم این است که لایه:
وابستگی ها را متمرکز می کند
با ساده کردن کد لامبدا، اندازه کد تابع را کاهش می دهد
امکان مدیریت ساده به روز رسانی را فراهم می کند
اگر به ساختار کد بازگردیم، می توانید ببینید که نام فایل وجود دارد apps/lib/utils.ts
import moment from ‘moment’
export const DATE_FORMAT = ‘YYYY-MM-DD’
export const TIME_FORMAT = ‘HH:mm:ss’
export const DATETIME_FORMAT = `${DATE_FORMAT} ${TIME_FORMAT}`
export const isNull = (value: any) => value === null
export const nowDate = () => moment().format(DATETIME_FORMAT)
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ما باید این ابزار را به عنوان کتابخانه برای Lambda خود بسازیم.
cd apps/layer/nodejs
npm install # install deps based apps/layer/nodejs/package.json
# and then build the utils library using esbuild
esbuild –bundle –platform=node –sourcemap ../../lib/utils.ts –outdir=./node_modules “–external:moment”
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اکنون منبع cdk لایه Lambda را پیکربندی کنید
import { aws_lambda as lambda, RemovalPolicy, Stack, StackProps } from ‘aws-cdk-lib’
import { Construct } from ‘constructs’
interface LayerStackProps extends StackProps {
layerName: string
}
export class LayerStack extends Stack {
public readonly layerArn: string;
constructor(scope: Construct, id: string, props?: LayerStackProps) {
super(scope, id, props)
const layer = new lambda.LayerVersion(this, ‘LayerResource’, {
code: lambda.Code.fromAsset(‘./apps/layer’),
compatibleArchitectures: [lambda.Architecture.X86_64],
compatibleRuntimes: [lambda.Runtime.NODEJS_20_X, lambda.Runtime.NODEJS_18_X],
layerVersionName: props?.layerName,
removalPolicy: RemovalPolicy.DESTROY
});
this.layerArn = layer.layerVersionArn;
this.exportValue(layer.layerVersionArn, {
name: ‘layerArn’,
description: ‘The Lambda Layer Version ARN Value which will be used by others stack as need’
});
}
}
// lib/layer-stack.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
تابع Lambda را پیکربندی کنید
import * as cdk from ‘aws-cdk-lib’
import {aws_lambda as lambda, Duration} from ‘aws-cdk-lib’
import { NodejsFunction, SourceMapMode } from ‘aws-cdk-lib/aws-lambda-nodejs’
import { dependencies } from ‘../apps/package.json’
….
private createLambdaFunction(props: CustomStackProps, dynamodbTableArn: string) {
const lambdaRole = this.createLambdaRole();
const layerArn = cdk.Fn.importValue(‘layerArn’)
return new NodejsFunction(this, ‘LambdaResource’, {
entry: ‘./apps/index.ts’,
handler: ‘index.handler’,
timeout: Duration.seconds(10),
functionName: ‘Todo-App-NodejsFunction’,
environment: {
TABLE_NAME: props.tableName
},
runtime: lambda.Runtime.NODEJS_20_X,
memorySize: 128,
role: lambdaRole,
bundling: {
externalModules: [
…Object.keys(dependencies),
‘utils’
],
sourceMap: true,
sourceMapMode: SourceMapMode.BOTH
},
layers: [
lambda.LayerVersion.fromLayerVersionArn(this, ‘UtilsAndNodeModulesResource’, layerArn)
]
})
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
مهم دانستن:
entry: در اینجا به فایلی که حاوی تابع handler است اشاره می کند ./apps/index.ts. آن فایل به یک فایل باندل به نام index.js تبدیل میشود که بعد از مرحله ساخت در /cdk.out/asset./index.ts
bundling: نقشه پیکربندی esbuild. در مورد ما، برای تولید فایل نقشه منبع و در نظر گرفتن به esbuild نیاز داریم utils و dependencies (فهرست) به عنوان موبول های خارجی. به یاد داشته باشید که این موبولهای خارجی توسط لایه ما پشتیبانی میشوند، بنابراین نیازی نیست که آنها را در کد تابع قرار دهیم.
layers: لایه ایجاد شده قبلی را اضافه می کند.
دروازه API
روش
مسیر
ظرفیت ترابری
GET
/todo-app-api/todolists
POST
/todo-app-api/todolists/create-todolist
{name: ‘string’}
PUT
/todo-app-api/todolists/{todoListId}/update-todolist
{name: ‘string’, owner: {fullName: “string”, email: “string”}}
نقاط پایانی بالا را ایجاد خواهیم کرد.مبادا با ایجاد Rest Api Resource شروع کنید
const restApi = new api.CfnRestApi(this, ‘ApiGatewayResource’, {
name: ‘ApiGateway’,
apiKeySourceType: api.ApiKeySourceType.HEADER,
disableExecuteApiEndpoint: false,
endpointConfiguration: {
types: [api.EndpointType.REGIONAL],
tags: [{ key: ‘Name’, value: ‘Api Gateway’ }]
}
})
// lib/cdk-stack.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ما باید اجازه دهیم API Gateway برای فراخوانی تابع لامبدا.
new lambda.CfnPermission(this, ‘LambdaPermissionResource’, {
sourceAccount: props.env?.account,
action: ‘lambda:InvokeFunction’,
principal: ‘apigateway.amazonaws.com’,
sourceArn: `arn:${Aws.PARTITION}:execute-api:${props.env?.region}:${props.env?.account}:${restApi.attrRestApiId}/*/*/*`,
functionName: lambdaFunction.functionArn
})
// lib/cdk-stack.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
همچنین، میتوان مجوز فوق را مستقیماً به تابع Lambda که قبلاً ایجاد شده بود، پیوست کرد. برای انجام این کار، فقط باید تماس بگیریم addPermission(..) روش ارائه شده توسط NodejsFunction کلاس
lambdaFunction.addPermission(“LambdaPermission”, {
action: ‘lambda:InvokeFunction’,
principal: new iam.ServicePrincipal(“apigateway.amazonaws.com”),
sourceAccount: props.env?.account,
sourceArn: `arn:${Aws.PARTITION}:execute-api:${props.env?.region}:${props.env?.account}:${restApi.attrRestApiId}/*/*/*`
})
// lib/cdk-stack.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
روش لیست TodoList
const rootResource = new api.CfnResource(this,
‘RestApiRootResource’, {
restApiId: restApi.attrRestApiId,
parentId: restApi.attrRootResourceId,
pathPart: ‘todo-app-api’
})
const getTodoListsResource = new api.CfnResource(this,
‘RestApiGetTodoListsResource’, {
restApiId: restApi.attrRestApiId,
parentId: rootResource.attrResourceId,
pathPart: ‘todolists’
})
const getTodoListsMethod = new ApiMethod(this,
‘ApiTodoListsMethodResource’, {
methodType: MediaType.GET,
authType: api.AuthorizationType.NONE,
restApiId: restApi.attrRestApiId,
resourceId: getTodoListsResource.attrResourceId,
operationName: ‘GetAllTodoLists’,
integration: {
connection: api.ConnectionType.INTERNET,
type: api.IntegrationType.AWS_PROXY,
httpMethod: MediaType.POST,
uri: `arn:aws:apigateway:${props.env?.region}:lambda:path/2015-03-31/functions/${lambdaFunction.functionArn}/invocations`
}
}).resource
// lib/cdk-stack.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ایجاد متد TodoList
const createTodoListResource = new api.CfnResource(this,
‘RestApiCreateTodoListResource’, {
restApiId: restApi.attrRestApiId,
parentId: getTodoListsResource.attrResourceId,
pathPart: ‘create-todolist’
})
const createTodoListMethod = new ApiMethod(this, ‘ApiCreateTodoListResource’, {
methodType: MediaType.POST,
authType: api.AuthorizationType.NONE,
restApiId: restApi.attrRestApiId,
resourceId: createTodoListResource.attrResourceId,
operationName: ‘CreateTodoList’,
integration: {
connection: api.ConnectionType.INTERNET,
type: api.IntegrationType.AWS_PROXY,
httpMethod: MediaType.POST,
uri: `arn:aws:apigateway:${props.env?.region}:lambda:path/2015-03-31/functions/${lambdaFunction.functionArn}/invocations`
}
}).resource
// lib/cdk-stack.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
می توانید متوجه شوید که منبع والد createTodoListMethod getTodoListResource است، زیرا مسیر createTodoListMethod با مسیر getTodoListResource شروع می شود.
روش TodoList را به روز کنید
const pathVariableSegmentResource = new api.CfnResource(this,
‘RestApiUpdateTodoListPathVariableResource’, {
restApiId: restApi.attrRestApiId,
parentId: getTodoListsResource.attrResourceId,
pathPart: ‘{todoListId}’
})
const updateTodoListLastSegmentResource = new api.CfnResource(
this, ‘RestApiUpdateTodoListResource’, {
restApiId: restApi.attrRestApiId,
parentId: pathVariableSegmentResource.attrResourceId,
pathPart: ‘update-todolist’
})
const updateTodoListMethod = new ApiMethod(this, ‘ApiUpdateTodoListResource’, {
methodType: MediaType.PUT,
authType: api.AuthorizationType.NONE,
restApiId: restApi.attrRestApiId,
resourceId: updateTodoListResource.attrResourceId,
operationName: ‘UpdateTodoList’,
integration: {
connection: api.ConnectionType.INTERNET,
type: api.IntegrationType.AWS_PROXY,
httpMethod: MediaType.POST,
uri: `arn:aws:apigateway:${props.env?.region}:lambda:path/2015-03-31/functions/${lambdaFunction.functionArn}/invocations`
},
requestParams: {
paths: [‘todoListId’]
}
}).resource
// lib/cdk-stack.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
و ApiMethod منبع سفارشی:
import { Construct } from ‘constructs’
import { aws_apigateway as api, StackProps } from ‘aws-cdk-lib’
export enum MediaType {
GET = ‘GET’,
POST = ‘POST’,
PUT = ‘PUT’,
DELETE = ‘DELETE’,
OPTIONS = ‘OPTIONS’
}
interface CustomMethodProps extends StackProps {
restApiId: string;
resourceId: string;
methodType: MediaType;
integration: {
uri: string;
connection: api.ConnectionType;
httpMethod: MediaType
type: api.IntegrationType
};
authType?: api.AuthorizationType,
operationName?: string
requestParams?: {
queries?: string[]
paths?: string[]
headers?: string[]
}
}
export class ApiMethod extends Construct {
public readonly resource: api.CfnMethod
constructor(scope: Construct, id: string, props: CustomMethodProps) {
super(scope, id)
const params = {} as Recordstring, boolean>
const integrationReqParams = {} as Recordstring, string>
const integrationResParams = {} as Recordstring, string>
if (props.requestParams?.paths) {
props.requestParams.paths.forEach(value => {
const p = `method.request.path.${value}`
params[p] = true
integrationReqParams[`integration.request.path.${value}`] = p
})
}
if (props.requestParams?.queries) {
props.requestParams.queries.forEach(value => {
params[`method.request.queryString.${value}`] = true
integrationReqParams[`integration.request.queryString.${value}`] = `method.request.queryString.${value}`
})
}
if (props.requestParams?.headers) {
props.requestParams.headers.forEach(value => {
params[`method.request.header.${value}`] = true
integrationReqParams[`integration.request.header.${value}`] = `method.request.header.${value}`
})
}
this.resource = new api.CfnMethod(this, ‘ApiTodoListsMethodResource’, {
apiKeyRequired: false,
restApiId: props.restApiId,
resourceId: props.resourceId,
httpMethod: props.methodType,
operationName: props.operationName || new Crypto().randomUUID(),
authorizationType: props.authType || api.AuthorizationType.NONE,
integration: {
connectionType: props.integration.connection,
integrationHttpMethod: props.integration.httpMethod,
type: props.integration.type,
uri: props.integration.uri,
integrationResponses: [{
statusCode: ‘200’,
responseParameters: {
‘method.response.header.Access-Control-Allow-Headers’: ‘\’Content-Type,X-Amz-Date,Authorization,X-Api-key,X-Amz-Security-Token\”,
‘method.response.header.Access-Control-Allow-Methods’: ‘\’GET,OPTIONS,POST,PUT\”,
‘method.response.header.Access-Control-Allow-Origin’: ‘\’*\”
}
}, {
statusCode: ‘500’,
responseParameters: integrationResParams
}],
requestParameters: integrationReqParams
},
methodResponses: [{
statusCode: ‘200’,
responseParameters: {
‘method.response.header.Access-Control-Allow-Headers’: true,
‘method.response.header.Access-Control-Allow-Methods’: true,
‘method.response.header.Access-Control-Allow-Origin’: true
}
}],
requestParameters: params
})
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
🥳✨واااااااااااااااااااااا!!!به پایان مقاله رسیدیم.خیلی ممنونم 🙂
می توانید کد منبع کامل را در GitHub Repo بیابید
روز 007 – 100DaysAWSIaCDevopsChallenge
امروز در سری چالش 100 روز کد، زیرساخت دیگری را با استفاده از کیت توسعه ابری (CDK) حاوی سرویسهای AWS مانند دروازه API، توابع لامبدا، لایهها و DynamoDB ایجاد میکنم. این پروژه شامل ایجاد زیرساختی است که API ها را با استفاده از API Gateway در معرض دید قرار می دهد. این APIها در یک تابع Lambda مستقر خواهند شد و ما از DynamoDB برای ماندگاری داده ها استفاده خواهیم کرد. برای رسیدن به این هدف، مراحل زیر را دنبال می کنیم:
- پروژه CDK را راه اندازی کنید: پروژه CDK را راه اندازی کنید و آن را برای زبان برنامه نویسی دلخواه خود پیکربندی کنید.
- API Gateway را تعریف کنید: یک دروازه API برای رسیدگی به درخواست های HTTP و مسیریابی آنها به تابع Lambda ایجاد کنید.
- ایجاد تابع Lambda: برای مدیریت منطق تجاری برای APIها، تابع Lambda را بنویسید و استقرار دهید.
- لایه های لامبدا را اضافه کنید: برای به اشتراک گذاشتن کدهای مشترک بین چندین تابع Lambda، لایه های Lambda را تعریف و اضافه کنید.
- DynamoDB را پیکربندی کنید: یک جدول DynamoDB برای ذخیره و بازیابی داده های مورد نیاز API ها تنظیم کنید.
- خدمات را یکپارچه کنید: API Gateway را به تابع Lambda متصل کنید و تابع Lambda را برای تعامل با DynamoDB پیکربندی کنید.
- زیرساخت را مستقر کنید: پشته CDK را در AWS مستقر کنید و عملکرد انتها به انتها API ها را آزمایش کنید.
پیش نیازها
- کیت توسعه ابری AWS (CDK)
- AWS SDK (نسخه جاوا اسکریپت)
- API Gateway، Lambda، IAM و DynamoDB
- تایپ اسکریپت
- esbuild ↗
نمودار زیرساخت
پروژه CDK را راه اندازی کنید
ابتدا باید حداقل Nodejs 18.x↗ یا جدیدتر را نصب کنیم و یک پروفایل aws را پیکربندی کنیم. علاوه بر این، ما باید داکر را روی دستگاه میزبان خود نصب کنیم NodejsFunction
ساختار CDK به آن نیاز دارد که توابع نوشته شده در TypeScript را با استفاده از جاوا اسکریپت بسازد esbuild
.
npm install -g aws-cdk
mdkir day_007 && cd day_007
cdk init app --language typescript
cdk bootstrap --profile cdk-user
برای جزئیات بیشتر در مورد راه اندازی CDK، به Docs↗ بروید
جدول DynamoDB را پیکربندی کنید
DynamoDB یک سرویس پایگاه داده NoSQL مدیریت شده توسط خدمات وب آمازون (AWS) است. ما از آن برای تداوم دادههای خود استفاده خواهیم کرد، زیرا در مقایسه با RDS یا Aurora، کار با آن بسیار ساده و آسان است. جدول مورد نیاز ما دارای یک کلید ترکیبی (کلید اولیه و کلید مرتب سازی) خواهد بود. از آنجایی که DynamoDB یک پایگاه داده NoSQL است، لازم نیست ابتدا ویژگی های دیگر را فراتر از ویژگی های کلیدی مشخص کنید. ویژگی های جدول اضافی را می توان در طول زمان در صورت نیاز اضافه کرد.
همانطور که در بالا ذکر شد، جدول ما دارای یک کلید ترکیبی است که به دو کلید تقسیم می شود: hash key
معروف به کلید پارتیشن (کلید اصلی) و range key
به عنوان کلید مرتب سازی (بخش دوم کلید اصلی) شناخته می شود.
کلید هش | کلید برد | سایر صفات |
---|---|---|
شناسه | TodoName | CreatedAt, UpdatedAt, Owner |
CDK
اجازه دهید جدول را با استفاده از CDK ایجاد کنید
import { aws_dynamodb as dynamodb } from "aws-cdk-lib"
/*
const props = {
...
tableName: "TodoApp"
}
*/
const table = new dynamodb.CfnTable(this, 'DynamoDBTableResource', {
tableName: props.tableName,
keySchema: [{
keyType: 'HASH',
attributeName: 'ID'
}, {
keyType: 'RANGE',
attributeName: 'TodoName'
}],
attributeDefinitions: [{
attributeName: 'ID',
attributeType: dynamodb.AttributeType.STRING
}, {
attributeName: 'TodoName',
attributeType: dynamodb.AttributeType.STRING
}],
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
tableClass: dynamodb.TableClass.STANDARD,
tags: [{
key: 'Name',
value: `DynamoDB-Table-${props.tableName}`
}]
})
// day_007/lib/cdk-stack.ts
همانطور که می بینید، ما نیازی به تعیین ویژگی های _CreatedAt، _UpdatedAt و Owner در attributeDefinitions
ویژگی.
ایجاد تابع Lambda
در داخل تابع Lambda، کد به ایجاد، بهروزرسانی و فهرست کردن موارد TodoList در DynamoDB میپردازد.
کد عملکرد
در زیر ساختار کد آمده است:
apps ├── domain │ ├── abstract.domain.ts │ ├── task-status.enum.ts │ ├── task.domain.ts │ └── todo-list.domain.ts ├── index.ts ├── layer │ └── nodejs │ ├── package-lock.json │ └── package.json ├── lib │ └── utils.ts ├── package-lock.json ├── package.json ├── src │ └── infra │ ├── dto │ │ ├── lambda.response.ts │ │ └── page.request.ts │ └── storage │ └── dynamodb │ ├── abstract.repository.ts │ └── todo-list.repository.ts └── tsconfig.json
فایل هایی که در حال حاضر به اینجا نیاز داریم هستند index.ts، todo-list.repository.ts، و تمام فایل های داخل دامنه فهرست راهنما. من لینک کد کامل GitHub را در پایان این آموزش ارائه خواهم کرد.
apps/package.json
{
"main": "index.ts",
"type": "commonjs",
"dependencies": {
"aws-lambda": "^1.0.7",
"@aws-sdk/client-dynamodb": "^3.620.0",
"@aws-sdk/lib-dynamodb": "^3.620.0",
"@aws-lambda/http": "^1.0.1",
"uuid": "^10.0.0",
"moment": "^2.30.1"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/uuid": "^10.0.0",
"@types/aws-lambda": "^8.10.142",
"@types/moment": "^2.13.0"
}
}
apps/domain/*.ts
// apps/domain/abstract.domain.ts
export interface IDomain {
id?: string;
createdAt?: string;
updatedAt?: string;
}
// apps/domain/todo-list.domain.ts
import { IDomain } from './abstract.domain'
export interface TodoList extends IDomain {
name: string;
owner?: {
fullName: string,
email: string
},
tasks?: any[]
}
apps/src/infra/storage/dynamodb/todo-list.repository.ts
import { AbstractRepository } from './abstract.repository'
import {
DeleteCommand,
DynamoDBDocumentClient,
GetCommand,
GetCommandOutput,
PutCommand,
PutCommandOutput,
QueryCommandOutput,
ScanCommand,
UpdateCommand
} from '@aws-sdk/lib-dynamodb'
import { v4 } from 'uuid'
import moment from 'moment'
import { TodoList } from '../../../../domain/todo-list.domain'
export class TodoListRepository implements AbstractRepositoryTodoList> {
constructor(private client: DynamoDBDocumentClient, private tableName: string) {
}
async create({ name, owner }: TodoList): PromiseTodoList> {
const item = {
ID: v4(),
CreatedAt: Math.floor((new Date().getTime()) / 1000),
UpdatedAt: Math.floor((new Date().getTime()) / 1000),
TodoName: name,
Owner: {
Fullname: owner?.fullName,
Email: owner?.email
}
}
let response: PutCommandOutput
try {
response = await this.client.send(new PutCommand({
TableName: this.tableName,
Item: item,
ExpressionAttributeNames: {
'#nameAttr': 'TodoName'
},
ExpressionAttributeValues: {
':nameValue': { 'S': name }
},
ConditionExpression: '#nameAttr :nameValue'
}))
} catch (e) {
console.error(e)
throw new Error('DataStorageException: ' + e.message)
}
console.log('PutCommandResponse', response)
if (response.$metadata.httpStatusCode !== 200) {
throw new Error('DataStorage: Something wrong, please try again')
}
return {
id: item.ID,
createdAt: this.convertTimestampToDate(item.CreatedAt),
updatedAt: this.convertTimestampToDate(item.UpdatedAt),
owner,
name: item.TodoName
}
}
async list(query: any): PromiseTodoList[]> {
let response: QueryCommandOutput
try {
response = await this.client.send(new ScanCommand({
TableName: this.tableName,
Limit: 25,
Select: 'ALL_ATTRIBUTES'
}))
} catch (e) {
console.error(e)
throw new Error('DataStorageException: ' + e.message)
}
if (response.$metadata.httpStatusCode === 200 && response.Count! > 0) {
return [...response.Items || []].map((value: any) => {
return {
id: value.ID,
name: value.TodoName,
createdAt: this.convertTimestampToDate(value.CreatedAt),
updatedAt: this.convertTimestampToDate(value.UpdatedAt),
owner: {
fullName: value.Owner.Fullname,
email: value.Owner.Email
},
tasks: value.Tasks!
}
})
}
return []
}
async update(id: { pk: string, sk?: string }, toUpdate: TodoList): PromiseTodoList> {
let response: PutCommandOutput
const updateDate = Math.floor((new Date().getTime()) / 1000)
try {
response = await this.client.send(new UpdateCommand({
TableName: this.tableName,
Key: {
ID: id.pk,
TodoName: id.sk
},
UpdateExpression: 'SET #name=:newOrOldName, #owner.#email = :newOrOldEmail, #owner.#fullname = :newOrOldFullname',
ExpressionAttributeNames: {
'#name': 'TodoName',
'#owner': 'Owner',
'#fullname': 'FullName',
'#Email': 'Email'
},
ExpressionAttributeValues: {
':newOrOldName': { 'S': toUpdate.name },
':newOrOldEmail': { 'S': toUpdate.owner?.email },
':newOrOldFullname': { 'S': toUpdate.owner?.fullName }
}
}))
} catch (e) {
console.error(e)
throw new Error('DataStorageException: ' + e.message)
}
console.debug('PutCommandOutput4Update', response)
if (response.$metadata.httpStatusCode !== 200) {
throw new Error('DataStorageException: something wrong during update !!')
}
return {
...toUpdate,
id: id.pk,
updatedAt: this.convertTimestampToDate(updateDate)
}
}
async get(id: { pk: string, sk?: string }): PromiseTodoList | null> {
let response: GetCommandOutput
try {
response = await this.client.send(new GetCommand({
TableName: this.tableName,
Key: {
ID: id.pk,
TodoName: id.sk
}
}))
} catch (e) {
console.error(e)
throw new Error('DataStorageException: ' + e.message)
}
console.debug('GetCommandOutput', response)
if (response.$metadata.httpStatusCode !== 200) {
throw new Error('DataStorageException: something wrong during update !!')
}
const value: any = response.Item
return !value ? null : {
id: value.ID,
name: value.TodoName,
createdAt: this.convertTimestampToDate(value.CreatedAt),
updatedAt: this.convertTimestampToDate(value.UpdatedAt),
owner: {
fullName: value.Owner.Fullname,
email: value.Owner.Email
},
tasks: value.Tasks!
}
}
private convertTimestampToDate = (time: number): string => moment.unix(time).format('YYYY-MM-DD HH:mm:ss')
}
apps/index.ts
import { APIGatewayProxyEvent } from 'aws-lambda'
import { isNull } from 'utils'
import { LambdaResponse } from './src/infra/dto/lambda.response'
import { TodoListRepository } from './src/infra/storage/dynamodb/todo-list.repository'
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { AbstractRepository } from './src/infra/storage/dynamodb/abstract.repository'
import { TodoList } from './domain/todo-list.domain'
const tableName = string>process.env.TABLE_NAME
const region = process.env.REGION ?? 'us-east-1'
const dynamodbClient = DynamoDBDocumentClient.from(new DynamoDBClient({
region: region
}))
export const handler = async (event: APIGatewayProxyEvent, context: any) => {
const method = event.httpMethod
const path = event.path
console.log('incoming request', { ...event })
if (isNull(method) && isNull(path)) {
return {
statusCode: 500,
isBase64Encoded: false,
body: JSON.stringify({
message: 'Something wrong !!'
})
}
}
const repo: AbstractRepositoryTodoList> = new TodoListRepository(dynamodbClient, tableName)
const requestBody = (() => {
if (event.isBase64Encoded) {
return /* global atob */ atob(event.body!)
}
return typeof event.body === 'string' ? JSON.parse(event.body) : event.body
})()
let response: LambdaResponse
if (method === 'POST' && path.endsWith('/create-todolist')) {
// const existing = await repo.get?.()
const result = await repo.create({
name: requestBody.name,
owner: {
fullName: 'Kevin L.',
email: 'kevin.kemta@test.com'
}
})
response = {
isBase64Encoded: false,
statusCode: 200,
body: JSON.stringify(result, null, -2)
}
} else if (path === '/todo-app-api/todolists') {
const todos = await repo.list('')
response = {
isBase64Encoded: false,
statusCode: 200,
body: JSON.stringify(todos, null, -2)
}
} else if (new RegExp('^/todo-app-api/todolists/[a-z0-9-]{36}/update-todolist$', 'gi').test(path)) {
const old = await repo.get?.({
pk: event.pathParameters?.todoListId!,
sk: requestBody.name
})
if (!old) {
response = {
isBase64Encoded: false,
statusCode: 404,
body: JSON.stringify({
message: `The TodoList ${event.pathParameters?.todoListId!} doesn't exists`
}, null, -2)
}
} else {
const updated = await repo.update?.({ pk: old.id!, sk: old.name }, {
name: requestBody.name ?? old.name,
owner: {
fullName: requestBody.owner?.fullName ?? old.owner?.fullName,
email: requestBody.owner?.email ?? old.owner?.email
}
})
response = {
isBase64Encoded: false,
statusCode: 200,
body: JSON.stringify(updated, null, -2)
}
}
} else {
return {
isBase64Encoded: false,
statusCode: 403,
body: JSON.stringify({
message: `The API ${method} ${path} is no yet implemented in lambda side`
}, null, -2)
}
}
return {
...response,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
}
}
لایه Lambda را اضافه کنید
تمام وابستگی ها (node_modules) در لایه Lambda اضافه می شوند. دلیلی که من برای اضافه کردن وابستگی به لایه انتخاب کردم این است که لایه:
- وابستگی ها را متمرکز می کند
- با ساده کردن کد لامبدا، اندازه کد تابع را کاهش می دهد
- امکان مدیریت ساده به روز رسانی را فراهم می کند
اگر به ساختار کد بازگردیم، می توانید ببینید که نام فایل وجود دارد apps/lib/utils.ts
import moment from 'moment'
export const DATE_FORMAT = 'YYYY-MM-DD'
export const TIME_FORMAT = 'HH:mm:ss'
export const DATETIME_FORMAT = `${DATE_FORMAT} ${TIME_FORMAT}`
export const isNull = (value: any) => value === null
export const nowDate = () => moment().format(DATETIME_FORMAT)
ما باید این ابزار را به عنوان کتابخانه برای Lambda خود بسازیم.
cd apps/layer/nodejs
npm install # install deps based apps/layer/nodejs/package.json
# and then build the utils library using esbuild
esbuild --bundle --platform=node --sourcemap ../../lib/utils.ts --outdir=./node_modules "--external:moment"
اکنون منبع cdk لایه Lambda را پیکربندی کنید
import { aws_lambda as lambda, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs'
interface LayerStackProps extends StackProps {
layerName: string
}
export class LayerStack extends Stack {
public readonly layerArn: string;
constructor(scope: Construct, id: string, props?: LayerStackProps) {
super(scope, id, props)
const layer = new lambda.LayerVersion(this, 'LayerResource', {
code: lambda.Code.fromAsset('./apps/layer'),
compatibleArchitectures: [lambda.Architecture.X86_64],
compatibleRuntimes: [lambda.Runtime.NODEJS_20_X, lambda.Runtime.NODEJS_18_X],
layerVersionName: props?.layerName,
removalPolicy: RemovalPolicy.DESTROY
});
this.layerArn = layer.layerVersionArn;
this.exportValue(layer.layerVersionArn, {
name: 'layerArn',
description: 'The Lambda Layer Version ARN Value which will be used by others stack as need'
});
}
}
// lib/layer-stack.ts
تابع Lambda را پیکربندی کنید
import * as cdk from 'aws-cdk-lib'
import {aws_lambda as lambda, Duration} from 'aws-cdk-lib'
import { NodejsFunction, SourceMapMode } from 'aws-cdk-lib/aws-lambda-nodejs'
import { dependencies } from '../apps/package.json'
....
private createLambdaFunction(props: CustomStackProps, dynamodbTableArn: string) {
const lambdaRole = this.createLambdaRole();
const layerArn = cdk.Fn.importValue('layerArn')
return new NodejsFunction(this, 'LambdaResource', {
entry: './apps/index.ts',
handler: 'index.handler',
timeout: Duration.seconds(10),
functionName: 'Todo-App-NodejsFunction',
environment: {
TABLE_NAME: props.tableName
},
runtime: lambda.Runtime.NODEJS_20_X,
memorySize: 128,
role: lambdaRole,
bundling: {
externalModules: [
...Object.keys(dependencies),
'utils'
],
sourceMap: true,
sourceMapMode: SourceMapMode.BOTH
},
layers: [
lambda.LayerVersion.fromLayerVersionArn(this, 'UtilsAndNodeModulesResource', layerArn)
]
})
}
مهم دانستن:
-
entry
: در اینجا به فایلی که حاوی تابع handler است اشاره می کند ./apps/index.ts. آن فایل به یک فایل باندل به نام index.js تبدیل میشود که بعد از مرحله ساخت در/cdk.out/asset. /index.ts -
bundling
: نقشه پیکربندی esbuild. در مورد ما، برای تولید فایل نقشه منبع و در نظر گرفتن به esbuild نیاز داریمutils
وdependencies
(فهرست) به عنوان موبول های خارجی. به یاد داشته باشید که این موبولهای خارجی توسط لایه ما پشتیبانی میشوند، بنابراین نیازی نیست که آنها را در کد تابع قرار دهیم. -
layers
: لایه ایجاد شده قبلی را اضافه می کند.
دروازه API
روش | مسیر | ظرفیت ترابری |
---|---|---|
GET |
/todo-app-api/todolists | |
POST |
/todo-app-api/todolists/create-todolist | {name: 'string'} |
PUT |
/todo-app-api/todolists/{todoListId}/update-todolist | {name: 'string', owner: {fullName: "string", email: "string"}} |
نقاط پایانی بالا را ایجاد خواهیم کرد.
مبادا با ایجاد Rest Api Resource شروع کنید
const restApi = new api.CfnRestApi(this, 'ApiGatewayResource', {
name: 'ApiGateway',
apiKeySourceType: api.ApiKeySourceType.HEADER,
disableExecuteApiEndpoint: false,
endpointConfiguration: {
types: [api.EndpointType.REGIONAL],
tags: [{ key: 'Name', value: 'Api Gateway' }]
}
})
// lib/cdk-stack.ts
ما باید اجازه دهیم API Gateway
برای فراخوانی تابع لامبدا.
new lambda.CfnPermission(this, 'LambdaPermissionResource', {
sourceAccount: props.env?.account,
action: 'lambda:InvokeFunction',
principal: 'apigateway.amazonaws.com',
sourceArn: `arn:${Aws.PARTITION}:execute-api:${props.env?.region}:${props.env?.account}:${restApi.attrRestApiId}/*/*/*`,
functionName: lambdaFunction.functionArn
})
// lib/cdk-stack.ts
همچنین، میتوان مجوز فوق را مستقیماً به تابع Lambda که قبلاً ایجاد شده بود، پیوست کرد. برای انجام این کار، فقط باید تماس بگیریم addPermission(..)
روش ارائه شده توسط NodejsFunction
کلاس
lambdaFunction.addPermission("LambdaPermission", {
action: 'lambda:InvokeFunction',
principal: new iam.ServicePrincipal("apigateway.amazonaws.com"),
sourceAccount: props.env?.account,
sourceArn: `arn:${Aws.PARTITION}:execute-api:${props.env?.region}:${props.env?.account}:${restApi.attrRestApiId}/*/*/*`
})
// lib/cdk-stack.ts
روش لیست TodoList
const rootResource = new api.CfnResource(this,
'RestApiRootResource', {
restApiId: restApi.attrRestApiId,
parentId: restApi.attrRootResourceId,
pathPart: 'todo-app-api'
})
const getTodoListsResource = new api.CfnResource(this,
'RestApiGetTodoListsResource', {
restApiId: restApi.attrRestApiId,
parentId: rootResource.attrResourceId,
pathPart: 'todolists'
})
const getTodoListsMethod = new ApiMethod(this,
'ApiTodoListsMethodResource', {
methodType: MediaType.GET,
authType: api.AuthorizationType.NONE,
restApiId: restApi.attrRestApiId,
resourceId: getTodoListsResource.attrResourceId,
operationName: 'GetAllTodoLists',
integration: {
connection: api.ConnectionType.INTERNET,
type: api.IntegrationType.AWS_PROXY,
httpMethod: MediaType.POST,
uri: `arn:aws:apigateway:${props.env?.region}:lambda:path/2015-03-31/functions/${lambdaFunction.functionArn}/invocations`
}
}).resource
// lib/cdk-stack.ts
ایجاد متد TodoList
const createTodoListResource = new api.CfnResource(this,
'RestApiCreateTodoListResource', {
restApiId: restApi.attrRestApiId,
parentId: getTodoListsResource.attrResourceId,
pathPart: 'create-todolist'
})
const createTodoListMethod = new ApiMethod(this, 'ApiCreateTodoListResource', {
methodType: MediaType.POST,
authType: api.AuthorizationType.NONE,
restApiId: restApi.attrRestApiId,
resourceId: createTodoListResource.attrResourceId,
operationName: 'CreateTodoList',
integration: {
connection: api.ConnectionType.INTERNET,
type: api.IntegrationType.AWS_PROXY,
httpMethod: MediaType.POST,
uri: `arn:aws:apigateway:${props.env?.region}:lambda:path/2015-03-31/functions/${lambdaFunction.functionArn}/invocations`
}
}).resource
// lib/cdk-stack.ts
می توانید متوجه شوید که منبع والد createTodoListMethod getTodoListResource است، زیرا مسیر createTodoListMethod با مسیر getTodoListResource شروع می شود.
روش TodoList را به روز کنید
const pathVariableSegmentResource = new api.CfnResource(this,
'RestApiUpdateTodoListPathVariableResource', {
restApiId: restApi.attrRestApiId,
parentId: getTodoListsResource.attrResourceId,
pathPart: '{todoListId}'
})
const updateTodoListLastSegmentResource = new api.CfnResource(
this, 'RestApiUpdateTodoListResource', {
restApiId: restApi.attrRestApiId,
parentId: pathVariableSegmentResource.attrResourceId,
pathPart: 'update-todolist'
})
const updateTodoListMethod = new ApiMethod(this, 'ApiUpdateTodoListResource', {
methodType: MediaType.PUT,
authType: api.AuthorizationType.NONE,
restApiId: restApi.attrRestApiId,
resourceId: updateTodoListResource.attrResourceId,
operationName: 'UpdateTodoList',
integration: {
connection: api.ConnectionType.INTERNET,
type: api.IntegrationType.AWS_PROXY,
httpMethod: MediaType.POST,
uri: `arn:aws:apigateway:${props.env?.region}:lambda:path/2015-03-31/functions/${lambdaFunction.functionArn}/invocations`
},
requestParams: {
paths: ['todoListId']
}
}).resource
// lib/cdk-stack.ts
و ApiMethod
منبع سفارشی:
import { Construct } from 'constructs'
import { aws_apigateway as api, StackProps } from 'aws-cdk-lib'
export enum MediaType {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
DELETE = 'DELETE',
OPTIONS = 'OPTIONS'
}
interface CustomMethodProps extends StackProps {
restApiId: string;
resourceId: string;
methodType: MediaType;
integration: {
uri: string;
connection: api.ConnectionType;
httpMethod: MediaType
type: api.IntegrationType
};
authType?: api.AuthorizationType,
operationName?: string
requestParams?: {
queries?: string[]
paths?: string[]
headers?: string[]
}
}
export class ApiMethod extends Construct {
public readonly resource: api.CfnMethod
constructor(scope: Construct, id: string, props: CustomMethodProps) {
super(scope, id)
const params = {} as Recordstring, boolean>
const integrationReqParams = {} as Recordstring, string>
const integrationResParams = {} as Recordstring, string>
if (props.requestParams?.paths) {
props.requestParams.paths.forEach(value => {
const p = `method.request.path.${value}`
params[p] = true
integrationReqParams[`integration.request.path.${value}`] = p
})
}
if (props.requestParams?.queries) {
props.requestParams.queries.forEach(value => {
params[`method.request.queryString.${value}`] = true
integrationReqParams[`integration.request.queryString.${value}`] = `method.request.queryString.${value}`
})
}
if (props.requestParams?.headers) {
props.requestParams.headers.forEach(value => {
params[`method.request.header.${value}`] = true
integrationReqParams[`integration.request.header.${value}`] = `method.request.header.${value}`
})
}
this.resource = new api.CfnMethod(this, 'ApiTodoListsMethodResource', {
apiKeyRequired: false,
restApiId: props.restApiId,
resourceId: props.resourceId,
httpMethod: props.methodType,
operationName: props.operationName || new Crypto().randomUUID(),
authorizationType: props.authType || api.AuthorizationType.NONE,
integration: {
connectionType: props.integration.connection,
integrationHttpMethod: props.integration.httpMethod,
type: props.integration.type,
uri: props.integration.uri,
integrationResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': '\'Content-Type,X-Amz-Date,Authorization,X-Api-key,X-Amz-Security-Token\'',
'method.response.header.Access-Control-Allow-Methods': '\'GET,OPTIONS,POST,PUT\'',
'method.response.header.Access-Control-Allow-Origin': '\'*\''
}
}, {
statusCode: '500',
responseParameters: integrationResParams
}],
requestParameters: integrationReqParams
},
methodResponses: [{
statusCode: '200',
responseParameters: {
'method.response.header.Access-Control-Allow-Headers': true,
'method.response.header.Access-Control-Allow-Methods': true,
'method.response.header.Access-Control-Allow-Origin': true
}
}],
requestParameters: params
})
}
}
🥳✨واااااااااااااااااااااا!!!
به پایان مقاله رسیدیم.
خیلی ممنونم 🙂
می توانید کد منبع کامل را در GitHub Repo بیابید