برنامه نویسی

برنامه بدون سرور خود را با پسوندهای AWS Lambda تقویت کنید

سرویس AWS Lambda مملو از ویژگی های چشمگیر است که ممکن است هنوز برای شما جدید باشد. یکی از آنها امکان اجرای کدهای اضافی در کنار کد کنترلر است. این ویژگی پسوند نامیده می شود.

این مقاله به شما می آموزد که چگونه از افزونه ها استفاده کنید تا ابزار نظارتی خود را برای لامبداها بدون به خطر انداختن عملکرد آنها ایجاد کنید. این پیاده سازی از NodeJs Lambda مستقر شده با AWS CDK استفاده می کند.

⚡ ویژگی پسوند Lambda

بیایید با یک جمع بندی سریع در مورد پسوند لامبدا شروع کنیم. اگر قبلاً با آن آشنایی دارید، به قسمت ایجاد آن بروید.

🤔 پسوند لامبدا چیست؟

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

میتونه باشه:

  • درونی؛ داخلی: در همان فرآیند کد کنترل کننده اجرا می شود.
  • خارجی: در یک فرآیند جداگانه اجرا می شود.

در هر دو مورد، حافظه، زمان‌بندی و ذخیره‌سازی زودگذر محیط اجرا را با کنترل‌کننده به اشتراک می‌گذارد.

این می تواند با سرویس Lambda تعامل داشته باشد تا در مورد هر رویداد دریافت شده توسط کنترل کننده مطلع شود و می تواند رویدادهای تله متری را از سرویس Lambda دریافت کند.

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

به عنوان مثال، یک برنامه افزودنی نظارت داخلی می تواند کتابخانه http را لغو کند تا تمام تماس های http را رهگیری و ثبت کند.

زیرا در یک فرآیند جداگانه اجرا می شود پسوند خارجی می تواند قابلیت هایی را به هندلر اضافه کند بدون اینکه بر عملکرد آن تأثیر بگذارد.

به عنوان مثال، یک برنامه افزودنی نظارت خارجی می تواند پس از اینکه کنترل کننده پاسخ خود را برگرداند تا از کاهش عملکرد یک API، معیارها را به یک سرویس نظارت خارجی ارسال کند.

🧩 نحوه استفاده از پسوند لامبدا

پسوند لامبدا به صورت یک می آید لایه لامبدا.

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

برای افزودن یک پسوند داخلی، باید لایه را به لامبدا اضافه کنید، اما باید زمان اجرا را نیز پیکربندی کنید تا نقطه ورودی افزونه را در کنار هندلر اجرا کنید. این پیکربندی با استفاده از متغیر محیطی خاص زمان اجرا یا اسکریپت wrapper انجام می شود.

به عنوان مثال، برای NodeJs، می توانید تنظیم کنید NODE_OPTIONS متغیر محیطی به --require /opt/my-extension.js یا تنظیم کنید AWS_LAMBDA_EXEC_WRAPPER متغیر محیطی به مسیر یک اسکریپت wrapper.

⚙️ نحوه ایجاد پسوند

بیایید یک برنامه افزودنی نظارتی ایجاد کنیم که همه تماس‌های http انجام شده توسط لامبدا را ثبت کند و آنها را برای ذخیره و تجسم به یک سرویس خارجی ارسال کند.

ما از یک برنامه افزودنی داخلی برای رهگیری همه تماس‌های http و ارسال آنها به یک برنامه افزودنی خارجی استفاده می‌کنیم که پس از هر فراخوانی لامبدا یک جمع‌بندی نظارتی ارسال می‌کند.

طرح برنامه های افزودنی مانیتور

تمام کدهای زیر در github موجود است:

حداقل مخزن برای ایجاد و استفاده از افزونه های Lambda داخلی و خارجی با NodeJs lambda

این مثال نحوه ایجاد یک Lambda ساده با ابزار نظارتی متشکل از موارد زیر را نشان می دهد:

  • یک برنامه افزودنی داخلی که تمام تماس های http انجام شده توسط لامبدا را ثبت می کند
  • یک برنامه افزودنی خارجی که آن لاگ‌ها را جمع می‌کند و آنها را به یک ابزار نظارتی فرضی ارسال می‌کند

طرح برنامه های افزودنی مانیتور

نصب

تغییر دادن https://webhook.site/* آدرس های اینترنتی در src/urls.ts به آدرس های وب هوک خود.

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

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

تست

pnpm integration-test
وارد حالت تمام صفحه شوید

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

🔍 پسوند داخلی رهگیر

رهگیر باید در همان فرآیندی که کنترل کننده اجرا می شود تا بتواند تمام تماس های http را رهگیری کند.

📝 کد رهگیر

برای سادگی، ما از msw به عنوان یک رهگیر گره خارج از جعبه استفاده می کنیم.

// src/layers/monitorExtension/partial-interceptor.ts

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.all('*', async req => {
    const url = req.url.toString();
    const method = req.method;
    console.log(`request intercepted in interceptor: ${method} ${url}`);

    return req.passthrough();
  }),
);

server.listen({ onUnhandledRequest: 'bypass' });
وارد حالت تمام صفحه شوید

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

سپس با یک تماس http محلی، گزارش‌ها را به افزونه خارجی ارسال می‌کنیم.

💡باید از جعبه شنی url زیرا تنها مورد مجاز در محیط اجرای لامبدا است. پورت رایگان است، بنابراین من یک پورت تصادفی را انتخاب کردم.

// src/layers/monitorExtension/interceptor.ts

import { rest } from 'msw';
import { setupServer } from 'msw/node';
import fetch from 'node-fetch';

console.log('Executing interceptor extension code...');

const LOG_EXTENSION_SERVER_URL = 'http://sandbox:4243';
const server = setupServer(
  rest.all('*', async (req, res, ctx) => {
    const url = req.url.toString();

    // Bypass the calls made by this code to the local server to avoid infinite loop
    if (url.includes(LOG_EXTENSION_SERVER_URL)) {
      return req.passthrough();
    }

    const method = req.method;
    const headers = req.headers;
    const body = await req.text();

    console.log(`request intercepted in interceptor: ${method} ${url}`);
    fetch(LOG_EXTENSION_SERVER_URL, {
      method: 'POST',
      body: JSON.stringify({
        url,
        method,
        headers,
        body,
        date: new Date().toISOString(),
      }),
    }).catch(error => console.error('error sending logs in interceptor extension', error));

    return req.passthrough();
  }),
);

server.listen({ onUnhandledRequest: 'bypass' });
وارد حالت تمام صفحه شوید

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

🚀 افزونه داخلی را مستقر کنید

ابتدا باید کد برنامه افزودنی را توسط لامبدا قابل اجرا کنیم. ساده ترین راه استفاده است esbuild آن را در یک فایل cjs دسته بندی کنید.

💡 اساساً این همان کاری است که با کد کنترل کننده زیر چوب توسط CDK هنگام استفاده از ساختار NodejsFunction انجام می شود. اما در اینجا، ما باید این کار را به صورت دستی انجام دهیم.

{
  "scripts": {
    "build:interceptor": "./node_modules/.bin/esbuild  ./src/layers/monitorExtension/interceptor.ts --bundle --outfile="./dist/layers/monitorExtension/interceptor.js" --platform=node --main-fields=module,main"
  }
}
وارد حالت تمام صفحه شوید

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

سپس باید کد را به لطف لایه لامبدا به لامبدا ارسال کنیم.

با CDK می توانیم از آن استفاده کنیم LambdaLayerVersion ساختن برای ایجاد لایه ای که تمام محتوای یک دایرکتوری را به آن ارسال می کند /opt پوشه لامبدا

// lib/partial-stack.ts

export class AppStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const layer = new LayerVersion(scope, 'MonitorLayer', {
      code: Code.fromAsset('dist/layers/monitorExtension'),
    });

    const helloFunction = new NodejsFunction(this, 'Hello', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'handler',
      entry: path.join(__dirname, `/../src/functions/hello/handler.ts`),
      layers: [layer],
    });
  }
}
وارد حالت تمام صفحه شوید

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

در نهایت، ما باید لامبدا را برای اجرای کد رهگیر در کنار کد کنترل کننده پیکربندی کنیم.

برای آن ما از متغیرهای محیطی خاص زمان اجرا استفاده خواهیم کرد. برای NodeJ ها باید مقدار را تنظیم کنیم NODE_OPTIONS متغیر محیطی به --require /opt/interceptor.js.

// lib/stack.ts

export class AppStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const layer = new LayerVersion(scope, 'MonitorLayer', {
      code: Code.fromAsset('dist/layers/monitorExtension'),
    });

    const helloFunction = new NodejsFunction(this, 'Hello', {
      runtime: Runtime.NODEJS_18_X,
      handler: 'handler',
      entry: path.join(__dirname, `/../src/functions/hello/handler.ts`),
      layers: [layer],
      environment: {
        NODE_OPTIONS: '--require /opt/interceptor.js',
      },
    });
  }
}
وارد حالت تمام صفحه شوید

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

خودشه! ما پسوند داخلی خود را ایجاد کرده ایم.

👀 نتایج

اجازه می‌دهیم آن را روی یک لامبدا ساده، که تماس‌های http را برقرار می‌کند، مستقر کنیم:

// src/functions/hello/handler.ts

import fetch from 'node-fetch';

export const hello = async () => {
  await fetch('https://webhook.site/87c3df17-c965-40d9-a616-790c4002a162');

  await fetch('https://webhook.site/87c3df17-c965-40d9-a616-790c4002a162', {
    method: 'POST',
    body: JSON.stringify({
      message: 'hello world',
    }),
  });

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      message: 'OK',
    }),
  };
};

export const handler = hello;
وارد حالت تمام صفحه شوید

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

ما می توانیم آن لاگ ها را ببینیم:

لاگ های اجرای لامبدا را رهگیری کردند

کد رهگیر در کنار کد کنترل کننده اجرا شد و تماس های http را رهگیری کرد.

اما می بینیم که نمی تواند آنها را به سرور http محلی ارسال کند زیرا وجود ندارد. بیایید پسوند خارجی برای رفع آن ایجاد کنیم.

📊 پسوند خارجی نظارت

ما می‌خواهیم لاگ‌ها را جمع‌آوری کنیم و آنها را بدون تأثیر بر عملکرد لامبدا به یک سرویس خارجی ارسال کنیم. بنابراین ما از یک فرآیند خارجی، با نام یک برنامه افزودنی خارجی، برای مدیریت آن لاگ ها استفاده خواهیم کرد.

📝 کد داخلی خارجی

اول، ما می خواهیم فرآیند خارجی ما از آنچه در لامبدا اتفاق می افتد آگاه باشد. برای انجام این کار، می‌توانیم از API پسوند lambda برای موارد زیر استفاده کنیم:

  1. فرآیند را به عنوان افزونه ثبت کنید
  2. در رویدادهای مختلف لامبدا یا رویدادهای تله متری مشترک شوید

اگر با API پسوند لامبدا آشنایی ندارید، توصیه می کنم مقاله فوق العاده ساده سازی AWS Lambda APIs داخلی Wakeem’s World را بخوانید.

من یک بسته گره را با الهام از مقاله او منتشر کردم تا ارتباط با API پسوند لامبدا را انتزاعی کنم.

SDK برای پیاده سازی آسان پسوند nodejs lambda

SDK برای ساخت آسان افزونه های Lambda در NodeJ و تایپ اسکریپت.

با الهام از ساده سازی API های AWS Lambda داخلی

pnpm add lambda-extension-service
وارد حالت تمام صفحه شوید

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

یا در صورت استفاده از نخ

yarn add lambda-extension-service
وارد حالت تمام صفحه شوید

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

یا اگر از npm استفاده می کنید

npm install lambda-extension-service
وارد حالت تمام صفحه شوید

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

استفاده

import { EventTypes, ExtensionAPIService, TelemetryEventTypes } from "lambda-extension-service";
(async () => {
  const extensionApiService = new ExtensionAPIService({ extensionName: "my-extension" });
  await extensionApiService.register([EventTypes.Invoke, EventTypes.Shutdown]);
  extensionApiService.onTelemetryEvent((event) => 
      console.log("Telemetry event received: ", JSON.stringify(event))
  );
  await extensionApiService.registerTelemetry([
      TelemetryEventTypes.Function,
      TelemetryEventTypes.Platform,
      TelemetryEventTypes.Extension,
  ]);

  while (true) {
      const event = await extensionApiService.next();
      console.log

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

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

با آن، ما به راحتی می‌توانیم برنامه افزودنی خارجی خود را ثبت کنیم و در تمام رویدادهای فراخوانی مشترک شویم و زمانی که لامبدا در شرف خاموش شدن است از آن مطلع شویم.

// src/layers/monitorExtension/partial-monitor.ts

import { EventTypes, ExtensionAPIService } from 'lambda-extension-service';

console.log('Executing monitor extension code...');

const main = async () => {
  const extensionApiService = new ExtensionAPIService({
    extensionName: 'monitor',
  });
  await extensionApiService.register([EventTypes.Invoke, EventTypes.Shutdown]);

  while (true) {
    const event = await extensionApiService.next();
    console.log('Received event', event);
  }
};

main().catch(error => console.error(error));
وارد حالت تمام صفحه شوید

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

اکنون می‌توانیم یک سرور http برای دریافت گزارش‌ها از افزونه داخلی راه‌اندازی کنیم.

// src/layers/monitorExtension/logServer.ts

import { createServer } from 'http';
import { Log } from './types';

type LogServerOptions = {
  port: number;
};
export const listenForLog = (onLogReceived: (log: Log) => void, { port }: LogServerOptions = { port: 4243 }) => {
  const server = createServer(function (request, response) {
    if (request.method == 'POST') {
      let body = '';
      request.on('data', function (data) {
        body += data;
      });
      request.on('end', function () {
        try {
          onLogReceived(JSON.parse(body));
        } catch (e) {
          console.error('failed to parse logs', e);
        }
        response.writeHead(200, {});
        response.end('OK');
      });
    } else {
      console.error('unexpected request', request.method, request.url);
      response.writeHead(404, {});
      response.end();
    }
  });

  server.listen(port, 'sandbox');
  console.info(`Listening for logs at http://sandbox:${port}`);
};
وارد حالت تمام صفحه شوید

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

و سپس در هر فراخوانی جدید یا خاموش شدن، آنها را جمع آوری کرده و به یک سرویس خارجی بفرستید.

// src/layers/monitorExtension//monitor.ts

import { EventTypes, ExtensionAPIService } from 'lambda-extension-service';
import { Log } from './types';
import { LogAggregator } from './logAggregator';
import { listenForLog } from './logServer';
import { LambdaContext } from './lambdaContext';
import { forwardLogs } from './forwardLogs';

console.log('Executing monitor extension code...');

const main = async () => {
  const logAggregator = new LogAggregator();
  const lambdaContext = new LambdaContext();
  const onLogReceived = (log: Log) => {
    logAggregator.addLog(log, lambdaContext.getRequestId());
  };
  listenForLog(onLogReceived);

  const extensionApiService = new ExtensionAPIService({
    extensionName: 'monitor',
  });
  await extensionApiService.register([EventTypes.Invoke, EventTypes.Shutdown]);

  while (true) {
    const event = await extensionApiService.next();
    const lastContext = lambdaContext.getContext();
    lambdaContext.updateContext(event);

    if (lastContext !== undefined) {
      await forwardLogs({
        context: lastContext,
        logs: logAggregator.getLogs(lastContext.requestId),
      });
    }
  }
};

main().catch(error => console.error(error));
وارد حالت تمام صفحه شوید

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

🚀 پسوند خارجی را مستقر کنید

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

💡 ما از همان استفاده مجدد می کنیم esbuild دستور مانند پسوند interceptor، اما ما باید یک shebang اضافه کنیم تا فایل قابل اجرا با --banner:js="#!/usr/bin/env node" گزینه.

{
  "scripts": {
    "build:monitor": "./node_modules/.bin/esbuild  src/layers/monitorExtension/monitor/index.ts --bundle --outfile="./dist/layers/monitorExtension/monitor.js" --platform=node --main-fields=module,main --banner:js="#!/usr/bin/env node""
  }
}
وارد حالت تمام صفحه شوید

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

سپس باید کد را به لطف لایه لامبدا به لامبدا ارسال کنیم.

💡 با خروجی بیلد کد مانیتور در همان پوشه کد رهگیر (./dist/layers/interceptorExtension)، لایه هر دو فایل را بدون تغییر پیکربندی CDK ما ارسال می کند.

در نهایت باید کاری کنیم که لامبدا فرآیند مانیتور را اجرا کند.

به‌طور پیش‌فرض، سرویس لامبدا سعی می‌کند همه فایل‌های موجود را اجرا کند /opt/extensions پوشه هنگام راه اندازی محیط لامبدا.

بنابراین بیایید یک اسکریپت bash به آن اضافه کنیم ./dist/layers/monitorExtension/extensions برای شروع کد مانیتور

# dist/layers/monitorExtension/extensions/monitor

#!/bin/bash
set -euo pipefail

OWN_FILENAME="$(basename $0)"
LAMBDA_EXTENSION_NAME="$OWN_FILENAME" # (external) extension name has to match the filename
NODE_OPTIONS="" # Needed to reset NODE_OPTIONS set by Lambda runtime. Otherwise, the internal interceptor extension will be loaded in the external process too.

exec "/opt/${LAMBDA_EXTENSION_NAME}.js"
وارد حالت تمام صفحه شوید

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

این اسکریپت ارسال خواهد شد /opt/extensions/monitor و به طور خودکار توسط سرویس لامبدا اجرا می شود.

💡توجه داشته باشید که متغیرهای محیطی لامبدا بین فرآیندها مشترک هستند. بنابراین فرآیند خارجی این را خواهد داشت NODE_OPTIONS تنظیم کنید تا به کد رهگیر نیاز داشته باشد. باید ریست کنیم NODE_OPTIONS برای جلوگیری از این امر و باعث می شود که کد رهگیر دو بار بارگذاری شود و تماس های مانیتور را قطع کند.

خودشه! تمدید نظارت خود را به پایان رساندیم.

👀 نتایج

بیایید دوباره آن را روی لامبدای قبلی خود مستقر کنیم. در اینجا لاگ های نهایی آمده است:

لاگ های اجرای لامبدا نظارت شد

  1. 🟠 سرویس لامبدا در محیط اجرای لامبدا راه اندازی می شود
  2. پسوند خارجی مانیتورینگ مقداردهی اولیه می شود
  3. 🟣 پسوند داخلی رهگیر مقداردهی اولیه می شود
  4. 🟠 سرویس لامبدا راه اندازی برنامه افزودنی نظارت را تایید می کند
  5. سرویس لامبدا اولین مدیریت رویداد را آغاز می کند
  6. 🟣 پسوند داخلی رهگیر دو تماس http را رهگیری می کند
  7. 🟠 سرویس لامبدا اولین رسیدگی به رویداد را پایان می دهد
  8. سرویس لامبدا رسیدگی به رویداد دوم را آغاز می کند
  9. افزونه مانیتورینگ گزارش های اولین رویداد را به سرویس نظارت خارجی ارسال می کند
  10. 🟣 پسوند داخلی رهگیر دو تماس http را رهگیری می کند
  11. 🟠 سرویس لامبدا اولین رسیدگی به رویداد را پایان می دهد
  12. هنگام خاموش شدن لامبدا، برنامه افزودنی خارجی نظارت، گزارش های رویداد دوم را به سرویس نظارت خارجی ارسال می کند.

در سرویس نظارت خارجی جعلی، گزارش‌های جمع‌آوری شده بر اساس رویداد دریافت کردیم:

گزارش های ارسال شده

⏭️ آن را در حساب AWS خود با استفاده از مخزن اختصاصی آزمایش کنید:

حداقل مخزن برای ایجاد و استفاده از افزونه های Lambda داخلی و خارجی با NodeJs lambda

این مثال نحوه ایجاد یک Lambda ساده با ابزار نظارتی متشکل از موارد زیر را نشان می دهد:

  • یک برنامه افزودنی داخلی که تمام تماس های http انجام شده توسط لامبدا را ثبت می کند
  • یک برنامه افزودنی خارجی که آن لاگ‌ها را جمع می‌کند و آنها را به یک ابزار نظارتی فرضی ارسال می‌کند

طرح برنامه های افزودنی مانیتور

نصب

تغییر دادن https://webhook.site/* آدرس های اینترنتی در src/urls.ts به آدرس های وب هوک خود.

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

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

تست

pnpm integration-test
وارد حالت تمام صفحه شوید

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

نتیجه

اکنون بدانید که چگونه پسوندهای داخلی و خارجی برای لامبدا ایجاد کنید. این یک روش قدرتمند برای پیاده سازی و به اشتراک گذاری ابزارهای پشتیبانی برای لامبداهای شما بدون تأثیرگذاری بر عملکرد آنها و بدون تغییر مستقیم کد کنترل کننده نوشته شده توسط توسعه دهندگان دیگر است.

در صورت تمایل نظرات و سوالات خود را در نظرات زیر به اشتراک بگذارید. خوشحال میشم بهشون جواب بدم

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

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

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

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