برنامه نویسی

گام به گام بدون سرور در AWS بیاموزید – EventBridge

TL; DR

در این مجموعه سعی می‌کنم اصول بی‌سرور را در AWS توضیح دهم تا بتوانید برنامه‌های بدون سرور خود را بسازید. با آخرین مقاله، نحوه ارسال ایمیل با استفاده از SES را کشف کردیم. در این مقاله، اجازه دهید به EventBridge شیرجه بزنیم، سرویسی که به شما امکان می‌دهد برنامه‌های رویداد محور بسازید.

معرفی

در طول 6 مقاله آخر این مجموعه، ما فقط برنامه های همزمان ساختیم: کاربر با استفاده از یک API درخواست ارسال کرد، درخواست پردازش شد و سپس کاربر با نتیجه پاسخ دریافت کرد. این یک الگوی بسیار رایج است، اما تنها یکی نیست. گاهی اوقات، می‌خواهید بدون اینکه کاربر منتظر نتیجه باشد، اطلاعات موجود در پس‌زمینه را بررسی کنید و پس از اتمام پردازش به کاربر اطلاع دهید. رویدادها به شما این امکان را می دهند!

ساخت یک برنامه بر اساس رویدادها با استفاده از چندین سرویس AWS قابل دستیابی است. یکی از رایج ترین آنها EventBridge است که موضوع این مقاله است. EventBridge به شما امکان می دهد قوانینی را ایجاد کنید که هنگام وقوع یک رویداد فعال شوند. سپس این قوانین می توانند یک تابع لامبدا را راه اندازی کنند، یا پیامی را به یک صف ارسال کنند، یا حتی یک نقطه پایانی HTTP را فراخوانی کنند. امکانات بی پایان هستند!

بیایید یک برنامه رزرو پرواز بسازیم!

امروز قصد داریم یک اپلیکیشن رزرو پرواز ساده بسازیم. ویژگی های زیر خواهد بود:

  • کاربر می تواند با استفاده از یک API درخواست رزرو پرواز کند
  • در صورت وجود صندلی، پرواز را برای کاربر رزرو می کنیم
  • در همان زمان، برای تایید رزرو به کاربر ایمیل ارسال می کنیم
  • صندلی‌های موجود هر روز به‌طور خودکار به‌روزرسانی می‌شوند

معماری به شکل زیر خواهد بود:

معماری

4 عملکرد لامبدا وجود خواهد داشت، برای درخواست رزرو، ثبت رزرو، ارسال ایمیل و به‌روزرسانی صندلی‌ها. این bookFlight lambda رویدادی را با EventBridge ارسال می‌کند که باعث راه‌اندازی آن می‌شود registerBooking و sendBookingReceipt لامبدا این syncFlights لامبدا هر روز برای به‌روزرسانی صندلی‌های موجود، با استفاده از قانون دیگر EventBridge فعال می‌شود. همچنین یک جدول DynamoDB برای ذخیره رزروها و یک SES Identity برای ارسال ایمیل ها وجود خواهد داشت.

در این معماری، استفاده از رویدادها به ما اجازه می دهد تا بخش های مختلف برنامه را جدا کنیم. این به ما این امکان را می دهد که به راحتی اجرای هر قسمت را بدون تأثیر بر روی قسمت های دیگر تغییر دهیم. علاوه بر این، توانایی فعال کردن syncFlights lambda را هر روز بدون تلاش باز می کند.

به جز قوانین EventBridge، ما قبلاً تمام خدمات مورد استفاده در این معماری را پوشش داده ایم. اگر می خواهید در مورد آنها بیشتر بدانید، می توانید مقالات قبلی این مجموعه را مطالعه کنید!

تامین زیرساخت ها

برای کدنویسی این برنامه، مانند همیشه از AWS CDK برای TypeScript استفاده خواهم کرد. من راه اندازی پروژه را در اولین مقاله از این مجموعه پوشش می دهم، اگر نیاز به تجدید نظر دارید!

قوانین EventBridge را ایجاد کنید

در تعریف پشته CDK، می‌توانیم با ایجاد یک EventBus و قوانینی که لامبداهای ما را فعال می‌کنند، شروع کنیم.

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import path from 'path';

export class LearnServerlessStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // Create an eventBus
    const eventBus = new cdk.aws_events.EventBus(this, 'eventBus');
    // Create a rule to trigger the registerBooking and sendBookingReceipt lambdas
    const bookFlightRule = new cdk.aws_events.Rule(this, 'bookFlightRule', {
      eventBus,
      eventPattern: {
        source: ['bookFlight'],
        detailType: ['flightBooked'],
      },
    });
    // Create a rate rule to trigger the syncFlights lambda every day
    const syncFlightsRule = new cdk.aws_events.Rule(this, 'syncFlightsRule', {
      schedule: cdk.aws_events.Schedule.rate(cdk.Duration.days(1)),
    });
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این قطعه کد:

  • ما یک EventBus ایجاد می کنیم که نقطه ورود رویدادها در EventBridge است.
  • سپس، یک قاعده ایجاد می کنیم که باعث می شود registerBooking و sendBookingReceipt لامبدا این قانون به گونه‌ای پیکربندی شده است که هنگام رویداد با منبع، راه‌اندازی شود bookFlight و نوع جزئیات flightBooked به EventBus ارسال می شود.
  • در نهایت، ما یک قانون نرخ ایجاد می کنیم که باعث می شود syncFlights لامبدا هر روز با استفاده از ویژگی نرخ EventBridge.

منابع دیگر را ایجاد کنید: جدول DynamoDB، SES Identity و API Gateway

سپس، بیایید سایر منابع لازم را ایجاد کنیم.

import { bookingReceiptHtmlTemplate } from './bookingReceiptHtmlTemplate';
// ...previous code

// Create a DynamoDB table to store the bookings
const flightTable = new cdk.aws_dynamodb.Table(this, 'flightTable', {
  partitionKey: {
    name: 'PK',
    type: cdk.aws_dynamodb.AttributeType.STRING,
  },
  sortKey: {
    name: 'SK',
    type: cdk.aws_dynamodb.AttributeType.STRING,
  },
  billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST,
});

// Create an API Gateway to expose the bookFlight lambda
const api = new cdk.aws_apigateway.RestApi(this, 'api', {});

// Create a SES template to send nice emails
const bookingReceiptTemplate = new cdk.aws_ses.CfnTemplate(this, 'bookingReceiptTemplate', {
  template: {
    htmlPart: bookingReceiptHtmlTemplate,
    subjectPart: 'Your flight to {{destination}} was booked!',
    templateName: 'bookingReceiptTemplate',
  },
});

// This part is common to the previous article. No need to follow it if you already have a SES Identity
const DOMAIN_NAME = 'pchol.fr';

const hostedZone = new cdk.aws_route53.HostedZone(this, 'hostedZone', {
  zoneName: DOMAIN_NAME,
});

const identity = new cdk.aws_ses.EmailIdentity(this, 'sesIdentity', {
  identity: cdk.aws_ses.Identity.publicHostedZone(hostedZone),
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این قطعه کد:

  • ما یک جدول DynamoDB برای ذخیره رزروها ایجاد می کنیم. کلید پارتیشن مقصد پرواز و کلید مرتب سازی تاریخ پرواز خواهد بود.
  • ما یک دروازه API ایجاد می کنیم تا لامبدا bookFlight را در معرض نمایش بگذاریم.
  • ما یک قالب SES ایجاد می کنیم. این قالب بر اساس یک قالب HTML است.
  • ما یک SES Identity ایجاد می کنیم. این قسمت با مقاله قبلی مشترک است. اگر قبلاً یک SES Identity دارید، می‌توانید از آن صرفنظر کنید.

من یک الگوی ساده HTML تعریف کردم که با استفاده از CSS و مکان‌نماها، نحوه ظاهر ایمیل‌ها را مشخص می‌کند. می توانید مال من را اینجا پیدا کنید:

export const bookingReceiptHtmlTemplate = `<html>
  <head>
    <style>
      * {
        font-family: sans-serif;
        text-align: center;
        padding: 0;
        margin: 0;
      }
      .title {
        color: #fff;
        background: #17bb90;
        padding: 1em;
      }
      .container {
        border: 2px solid #17bb90;
        border-radius: 1em;
        margin: 1em auto;
        max-width: 500px;
        overflow: hidden;
      }
      .message {
        padding: 1em;
        line-height: 1.5em;
        color: #033c49;
      }
      .footer {
        font-size: .8em;
        color: #888;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="title">
        <h1>Your flight was booked!</h1>
      </div>
      <div class="message">
        <p>Your flight was booked on {{flightDate}}, for {{numberOfSeats}} person(s), to {{destination}}!</p>
      </div>
    </div>
    <p class="footer">This is an automated message, please do not try to answer</p>
  </body>
</html>`;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توابع لامبدا را ایجاد کنید و همه چیز را به هم وصل کنید

در نهایت، ما می‌توانیم توابع لامبدا را ایجاد کنیم، و آنها را به API Gateway و EventBridge متصل کنیم، و همچنین مجوزهای لازم و متغیرهای محیطی را به آنها اعطا کنیم.

// Create the bookFlight lambda
const bookFlight = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'bookFlight', {
  entry: path.join(__dirname, 'bookFlight', 'handler.ts'),
  handler: 'handler',
  environment: {
    TABLE_NAME: flightTable.tableName,
    EVENT_BUS_NAME: eventBus.eventBusName,
  },
});
bookFlight.addToRolePolicy(
  new cdk.aws_iam.PolicyStatement({
    actions: ['events:PutEvents'],
    resources: [eventBus.eventBusArn],
  }),
);
flightTable.grantReadData(bookFlight);
myFirstApi.root.addResource('book-flight').addMethod('POST', new cdk.aws_apigateway.LambdaIntegration(bookFlight));

// Create the registerBooking lambda
const registerBooking = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'registerBooking', {
  entry: path.join(__dirname, 'registerBooking', 'handler.ts'),
  handler: 'handler',
  environment: {
    TABLE_NAME: flightTable.tableName,
  },
});
flightTable.grantReadWriteData(registerBooking);
bookFlightRule.addTarget(new cdk.aws_events_targets.LambdaFunction(registerBooking));

// Create the sendBookingReceipt lambda
const sendBookingReceipt = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'sendBookingReceipt', {
  entry: path.join(__dirname, 'sendBookingReceipt', 'handler.ts'),
  handler: 'handler',
  environment: {
    SENDER_EMAIL: `contact@${identity.emailIdentityName}`,
    TEMPLATE_NAME: bookingReceiptTemplate.ref,
  },
});
sendBookingReceipt.addToRolePolicy(
  new cdk.aws_iam.PolicyStatement({
    actions: ['ses:SendTemplatedEmail'],
    resources: [`*`],
  }),
);
bookFlightRule.addTarget(new cdk.aws_events_targets.LambdaFunction(sendBookingReceipt));

// Create the syncFlights lambda
const syncFlights = new cdk.aws_lambda_nodejs.NodejsFunction(this, 'syncFlights', {
  entry: path.join(__dirname, 'syncFlights', 'handler.ts'),
  handler: 'handler',
  environment: {
    TABLE_NAME: flightTable.tableName,
  },
});
flightTable.grantWriteData(syncFlights);
syncFlightsRule.addTarget(new cdk.aws_events_targets.LambdaFunction(syncFlights));
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اینجا ما 4 تابع لامبدا ایجاد می کنیم:

  • bookFlight به نام جدول و نام اتوبوس رویداد دسترسی دارد. توسط یک مسیر POST راه اندازی می شود، و ما به آن اجازه می دهیم رویدادها را در اتوبوس رویداد منتشر کند و جدول را بخواند.
  • registerBooking به نام جدول دسترسی دارد. توسط اتوبوس رویداد با استفاده از راه اندازی می شود bookFlightRule.addTarget روش، و ما به آن اجازه خواندن و نوشتن جدول را می دهیم.
  • sendBookingReceipt به ایمیل فرستنده و نام قالب دسترسی دارد. توسط اتوبوس رویداد با استفاده از راه اندازی می شود rule.addTarget روش، و ما به آن اجازه ارسال ایمیل با استفاده از SES را می دهیم.
  • syncFlights به نام جدول دسترسی دارد. توسط اتوبوس رویداد با استفاده از راه اندازی می شود syncFlightsRule.addTarget روش، و اجازه نوشتن جدول را به آن می دهیم.

و ما با زیرساخت ها تمام شده ایم! آخرین مرحله، مرحله خنده دار، نوشتن کد برای هر تابع لامبدا است.

کد هر تابع لامبدا را بنویسید

کتاب پرواز

import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';

const ddbClient = new DynamoDBClient({});
const eventBridgeClient = new EventBridgeClient({});

export const handler = async ({ body }: { body: string }): Promise<{ statusCode: number; body: string }> => {
  const tableName = process.env.TABLE_NAME;
  const eventBusName = process.env.EVENT_BUS_NAME;

  if (tableName === undefined || eventBusName === undefined) {
    throw new Error('Missing environment variables');
  }

  const { destination, flightDate, numberOfSeats, bookerEmail } = JSON.parse(body) as {
    destination?: string;
    flightDate?: string;
    numberOfSeats?: number;
    bookerEmail?: string;
  };

  if (
    destination === undefined ||
    flightDate === undefined ||
    numberOfSeats === undefined ||
    bookerEmail === undefined
  ) {
    return {
      statusCode: 400,
      body: JSON.stringify({
        message: 'Missing required parameters',
      }),
    };
  }

  const { Item } = await ddbClient.send(
    new GetItemCommand({
      TableName: tableName,
      Key: {
        PK: { S: `DESTINATION#${destination}` },
        SK: { S: flightDate },
      },
    }),
  );

  const availableSeats = Item?.availableSeats?.N;

  if (availableSeats === undefined) {
    return {
      statusCode: 404,
      body: JSON.stringify({
        message: 'Flight not found',
      }),
    };
  }

  if (+availableSeats < numberOfSeats) {
    return {
      statusCode: 400,
      body: JSON.stringify({
        message: 'Not enough seats for this flight',
      }),
    };
  }

  await eventBridgeClient.send(
    new PutEventsCommand({
      Entries: [
        {
          Source: 'bookFlight',
          DetailType: 'flightBooked',
          EventBusName: eventBusName,
          Detail: JSON.stringify({
            destination,
            flightDate,
            numberOfSeats,
            bookerEmail,
          }),
        },
      ],
    }),
  );

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Processing flight booking',
    }),
  };
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

3 مرحله اصلی در این تابع لامبدا وجود دارد:

  • ابتدا محتوای بدنه دریافتی از API Gateway را تجزیه می کنم. من این رویکرد را در اولین مقاله خود به تفصیل شرح می دهم.
  • سپس، جزئیات پرواز را از جدول پروازها دریافت کنید. من از GetItemCommand، Refresher در این مقاله استفاده می کنم. اگر صندلی‌های دیگری وجود نداشته باشد، یک خطا برمی‌گردانیم.
  • در نهایت، با استفاده از PutEventsCommand یک رویداد را در اتوبوس رویداد منتشر می کنم. من یک منبع و یک نوع جزئیات را مشخص می کنم که با قوانینی که قبلا ایجاد کردیم مطابقت دارد. من همچنین نام اتوبوس رویداد و جزئیات رویداد را مشخص می کنم. جزئیات رویداد در دسترس شنوندگان توابع لامبدا خواهد بود.

این لامبدا یک کد وضعیت 200 را برمی‌گرداند اگر همه چیز خوب پیش برود و به کاربر می‌گوید که درخواست مورد توجه قرار گرفته است، حتی اگر هنوز به طور کامل به آن رسیدگی نشده باشد. کاربر در پایان فرآیند یک ایمیل دریافت خواهد کرد.

ثبت رزرو

import { DynamoDBClient, UpdateItemCommand } from '@aws-sdk/client-dynamodb';

const ddbClient = new DynamoDBClient({});

export const handler = async (event: {
  detail: {
    destination: string;
    flightDate: string;
    numberOfSeats: number;
  };
}): Promise<void> => {
  const { destination, flightDate, numberOfSeats } = event.detail;

  await ddbClient.send(
    new UpdateItemCommand({
      TableName: process.env.TABLE_NAME,
      Key: {
        PK: { S: `DESTINATION#${destination}` },
        SK: { S: flightDate },
      },
      UpdateExpression: 'SET availableSeats = availableSeats - :numberOfSeats',
      ExpressionAttributeValues: {
        ':numberOfSeats': { N: `${numberOfSeats}` },
      },
    }),
  );
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این تابع لامبدا توسط اتوبوس رویداد فعال می شود و تعداد صندلی های موجود در جدول پروازها را به روز می کند. در این مقاله از UpdateItemCommand استفاده می کند.

به تایپ کنترل کننده توجه کنید: با رویداد معمولی API Gateway که قبلاً با آن کار می کردیم متفاوت است. به یاد داشته باشید که جزئیات رویدادی که در اتوبوس رویداد ارسال شده است در دسترس است event.detail ویژگی.

این لامبدا چیزی را برنمی‌گرداند: به صورت ناهمزمان راه‌اندازی شد و هیچ‌کس منتظر پاسخ آن نیست!

ارسال رسید رزرو

import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';

const sesClient = new SESv2Client({});

export const handler = async (event: {
  detail: {
    destination: string;
    flightDate: string;
    numberOfSeats: number;
    bookerEmail: string;
  };
}): Promise<void> => {
  const { destination, flightDate, numberOfSeats, bookerEmail } = event.detail;

  const senderEmail = process.env.SENDER_EMAIL;
  const templateName = process.env.TEMPLATE_NAME;

  if (senderEmail === undefined || templateName === undefined) {
    throw new Error('Missing environment variables');
  }

  await sesClient.send(
    new SendEmailCommand({
      FromEmailAddress: senderEmail,
      Content: {
        Template: {
          TemplateName: templateName,
          TemplateData: JSON.stringify({ destination, flightDate, numberOfSeats }),
        },
      },
      Destination: {
        ToAddresses: [bookerEmail],
      },
    }),
  );
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این دومین لامبدا است که توسط قانون bookFlight ایجاد می شود. همچنین به ویژگی event.detail دسترسی دارد و از آن برای ارسال ایمیل به کاربر استفاده می کند. از SendEmailCommand استفاده می‌کند که در این مقاله نحوه دستیابی به آن را به‌روزرسانی می‌کند.

همان معامله، چیزی را بر نمی گرداند زیرا به صورت ناهمزمان راه اندازی می شود.

همگام سازی پروازها

import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';

const DESTINATIONS = ['CDG', 'LHR', 'FRA', 'IST', 'AMS', 'FCO', 'LAX'];

const client = new DynamoDBClient({});

export const handler = async (): Promise<void> => {
  const tableName = process.env.TABLE_NAME;

  if (tableName === undefined) {
    throw new Error('Table name not set');
  }

  const flightDate = new Date().toISOString().slice(0, 10);

  await Promise.all(
    DESTINATIONS.map(async destination =>
      client.send(
        new PutItemCommand({
          TableName: tableName,
          Item: {
            PK: { S: `DESTINATION#${destination}` },
            SK: { S: flightDate },
            availableSeats: { N: '2' },
          },
        }),
      ),
    ),
  );
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این لامبدا بسیار ساده است، توسط یک قانون cron فعال می شود و برای هر مقصد یک آیتم جدید در جدول پروازها ایجاد می کند. در این مقاله از PutItemCommand استفاده می کند. من فقط برای سادگی، خرمای مسخره شده را در جدول قرار دادم. هر بار 2 صندلی موجود است.

از آنجایی که لامبدا توسط یک برنامه راه‌اندازی می‌شود، ناهمزمان نیز هست و چیزی را بر نمی‌گرداند.

آزمایش برنامه ما!

کار ما با کد توابع لامبدا تمام شده است. زمان استقرار برنامه و آزمایش آن است!

npm run cdk deploy
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر نمی‌خواهید 1 روز منتظر بمانید تا syncFlights lambda فعال شود، می‌توانید با کلیک بر روی دکمه “تست” در صفحه عملکرد لامبدا، آن را به صورت دستی از کنسول AWS فعال کنید. لامبدا به هیچ محموله ای نیاز ندارد، بنابراین به راحتی می توان آن را به صورت دستی فعال کرد.

سپس، تنها یک فراخوان API برای اجرا وجود دارد: یک درخواست POST در /book-flight برای درخواست رزرو!

پستچی درخواست موفقیت کرد

ما یک پاسخ 200 دریافت می کنیم که به ما می گوید این درخواست در نظر گرفته شده است. چند ثانیه بعد، یک ایمیل با رسید رزرو دریافت می کنیم. اگر ایمیل را دریافت نکردید، با استفاده از cloudwatch و آخرین مقاله SES من عیب یابی کنید.

ایمیل دریافت شد

اگر همه چیز درست کار کند، باید فقط 1 صندلی در مقصد LHR باقی بماند، بنابراین اگر من درخواست 2 صندلی داشته باشم، باید 400 پاسخ دریافت کنم.

درخواست پستچی شکست خورد

این دقیقاً همان چیزی است که اتفاق می افتد! در این حالت، هیچ ایمیلی دریافت نمی‌شود زیرا گردش کار زودتر متوقف شده است.

تکالیف 🤓

اگر تمام مقالات قبلی من را بخوانید، می توانید S3، Cognito و Step-Functions را به برنامه اضافه کنید. با این دانش، باید بتوانید ویژگی های زیر را پیاده سازی کنید:

  • احراز هویت کاربر را به برنامه اضافه کنید
  • خرید بلیط در S3
  • یک گردش کار پرداخت مسخره شده را با توابع مرحله اجرا کنید

در صورت تمایل می‌توانید خودتان را به APIهای دنیای واقعی وصل کنید تا اطلاعات پروازها را دریافت کنید!

اینها فقط نمونه هستند! شما می توانید هر کاری که می خواهید با برنامه انجام دهید، و من خوشحال می شوم ببینم چه چیزی به ذهن شما می رسد! اگر می خواهید کار خود را به اشتراک بگذارید، دریغ نکنید با من تماس بگیرید توییتر!

مقالات در اجرای خود پیچیده‌تر و پیچیده‌تر می‌شوند: ما به موارد استفاده در دنیای واقعی نزدیک‌تر می‌شویم. امیدوارم تا اینجا از سریال لذت برده باشید و چیزهای زیادی یاد بگیرید!

نتیجه

این آموزش تنها یک مثال عملی کوچک از آنچه می توانید با رویدادها در AWS انجام دهید بود. موارد استفاده زیاد دیگری با راه حل های تمیزتر و کارآمدتر وجود دارد. امیدوارم به شما کمک کرده باشد که اصول برنامه های کاربردی رویداد محور و نحوه استفاده از آنها را در AWS درک کنید.

قصد دارم این سری مقالات را به صورت دو ماه یکبار ادامه دهم. من قبلاً ایجاد توابع لامبدا ساده و APIهای REST و همچنین تعامل با پایگاه‌های داده DynamoDB و سطل‌های S3 را پوشش داده‌ام. شما می توانید این پیشرفت را در مخزن من دنبال کنید! من موضوعات جدیدی مانند استقرار جلویی، ایمنی نوع، الگوهای پیشرفته تر و موارد دیگر را پوشش خواهم داد… اگر پیشنهادی دارید، دریغ نکنید با من تماس بگیرید!

اگر واکنش نشان دهید و این مقاله را با دوستان و همکاران خود به اشتراک بگذارید، واقعاً ممنون می شوم. این به من کمک زیادی می کند تا مخاطبانم را افزایش دهم. همچنین فراموش نکنید که مشترک شوید تا با انتشار مقاله بعدی به روز شوید!

من می خواهم در تماس بمانید اینجا من است حساب توییتر. من اغلب مطالب جالبی درباره AWS و بدون سرور پست می‌کنم یا دوباره پست می‌کنم، لطفاً مرا دنبال کنید!

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

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

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

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