برنامه نویسی

ساخت یک برنامه بدون سرور با کیت توسعه ابری 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 بیابید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا