Как стать автором
Обновить
0

Разработчик в стране Serverless: создаем REST API (Часть 4)

Время на прочтение 16 мин
Количество просмотров 3.5K

В предыдущих частях я сделал кубики, из которых состоит serverless приложение:

Реализовано 4 функции, развернута БД. Функции интегрированы с БД, запускаются локально в режиме отладки. Вся инфраструктура поднимается буквально с помощью нескольких SAM команд.

Но пока это мало похоже на какое-то целостное приложение, т.к. у приложения нет API. Мои лямбда функции не умеют обрабатывать http запросы. Так что  в этой главе займусь вопросами, связанными с построениями настоящего API. Также сделаю интеграцию с github api.

Создание API в SAM шаблоне

В моей задаче необходимо создать REST API. Для обработки http запросов я буду использовать сервис ApiGateway от AWS. Он позволяет создавать API, интегрируется с лямбда функциями для обработки входящих запросов. На каждый входящий http запрос ApiGateway создает специальное событие, которое в дальнейшем может быть обработано функцией. 

В моих функциях уже есть необходимая логика, которая позволяет работать с БД. Но этот код работает для входящего события совершенно другого формата. Надо адаптировать свой код под формат события от ApiGateway. Похожие действия придется провести и с ответом, мой формат ответа должен поддерживаться ApiGateway. Пример входящего события можно найти в папке events в корне проекта. SAM заботливо создал его, когда я создавал приложение. 

На самом деле SAM для меня уже сделал отдельный API. В шаблоне для лямбда функции присутствует свойство Events:

# ./template.yaml
Resources:
  GetProjectByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/handlers/get-project-by-id
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Эта настройка описывает источники событий, которые данная функция будет обрабатывать, тип “Api” определяет сервис ApiGateway в качестве источника, в шаблоне указан путь, запросы на который будут обработаны функцией.

Как видно из примера, я нигде явно не указывал ресурс ApiGateway. SAM создаст его сам. Такой вид создания API называется implicit api. SAM при сборке анализирует все функции в шаблоне, собирает все функции, у которых есть заполненный раздел Events ресурсами с типом Api, собирает все пути, за которые эти функции отвечают. И создает ресурс типа AWS::Serverless::Api с именем ServerlessRestApi. В этом API уже есть все пути, на эти пути назначены указанные лямбды. Implicit api выглядит удобным для простых, несложных приложений. 

Если же нужна более гибкая настройка, то более удобным вариантом будет явное определение ресурса. Минимальный сконфигурированный ApiGateway с именем, совпадающим с именем стека, выглядит вот так:

# ./template.yaml
BugTrackerApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: prod
      OpenApiVersion: "3.0.3"
  GetProjectByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      ...
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref BugTrackerApi

Здесь следует сделать остановку и ознакомиться с некоторыми особенностями конфигурации API Gateway.

С OpenApiVersion связан один баг в SAM. Баг заключается в том, что при создании ApiGateway помимо основного стейджа (по умолчанию prod), sam создает еще загадочный стейдж с именем “Stage”. Если в ресурсе указать поле OpenApiVersion, то этого не произойдет. Это работает только с вновь созданными ресурсами, испорченные ресурсы этот флажок лечить не умеет.

Теперь разберусь с параметром StageName. Но для того, чтобы понять, что это такое и как правильно им пользоваться, сначала обратим внимание на немного другой вопрос, а именно — вопрос организации проектных окружений.

Multi Stage vs Multi Stack

Ранее я немного касался вопроса, связанного с CF и окружениями. Вернусь к этому вопросу и постараюсь раскрыть его глубже. 

Обычно необходимо иметь 3-4 стандартных фиксированных окружений — dev, qa, stg, prod (часто это окружение поднимается в отдельном аккаунте). Очень полезной является возможность разворачивать feature environment для членов команды. Есть два подхода к созданию таких окружений:

  • все окружения в одном SAM/CF стеке, 

  • для каждого окружения отдельные стеки.

Сначала рассмотрю один стек для всех окружений. Как происходит работа с лямбдами в таком случае? 

Один стек

Само собой, никто не копирует лямбда функции в нужном количестве в шаблоне под каждое окружение. Ресурс с лямбдой функцией один. Но как-то надо разный код одной и той же лямбды иметь на разных окружениях. В этом случае можно использовать дополнительные возможности функций: версионирование и алиасы. Развернутые в облаке лямбда функции можно публиковать, после публикации функции присваивается версия. Версия — обычное число, AWS сам ее увеличивает. Для текущего состояния лямбды есть $LATEST версия. Помимо версий понадобятся алиасы — текстовое имя лямбда функции, привязанной к определенной версии. 

Предположим, у меня есть развернутая лямбда функция MyFunc. Можно создать алиас DEV_MY_FUNC, соответствующий $LATEST, алиас QA_MY_FUNC — функции версии 5, и алиас STG_MY_FUNC — функции версии 4. Теперь в моем облаке одновременно могут работать три разных версии одной лямбда функции, несмотря на то, что в моем SAM шаблоне только один ресурс с лямбда функцией.

В ApiGateway для реализаций нескольких окружений существуют стейджи. Стейджи по сути означают версию нашего апи. Таких версий может быть несколько. Например, для моих dev/qa/stg окружений у моего апи будут стейджи dev, qa, stg соответственно. Каждый стейдж порождает отдельные урлы:

У меня есть функция MyFunc. Хочу, чтобы эта функция обрабатывала запросы по пути /hello.

MyFunc:
    Type: AWS::Serverless::Function
    Properties:
      ...
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref MyApiGateway

Если связать ApiGateway и функцию, то она будет обрабатывать запросы:

  • https://GATEWAY_HOST/dev/hello

  • https://GATEWAY_HOST/qa/hello

  • https://GATEWAY_HOST/stg/hello

При такой настройке все мои API окружения связаны с одной и той же лямбда функцией. А я хочу, чтобы dev стейдж работал со свежим кодом, а qa со старым стабильным. Как раз тут в игру и вступают алиасы.

Для каждого стейджа есть возможность создавать переменные. Я создам переменную MY_FUNC_ALIAS. Она будет содержать имя алиаса моей функции, который соответствует окружению. 

Для dev стейджа  это будет DEV_MY_FUNC, для qa —  QA_MY_FUNC, для stg —  STG_MY_FUNC.

Ну и остался последний шаг. Нужно сказать ApiGateway использовать не $LATEST версию моей функции, а ту версию, которая задана переменной MY_FUNC_ALIAS.

Теперь осталось в настройках ApiGateway указать интеграцию не с конкретной версией лямбда функции, а с лямбда функцией, которая определяется этой переменной MyFunc:${stageVariables.MY_FUNC_ALIAS}.

Если это настраивать через UI, то выглядит это так:

Тогда автоматически каждый стейдж будет использовать версию лямбда функции, специфичную для него . 

Чтобы весь этот механизм исправно работал, необходимо публиковать новые версии функций, перевешивать алиасы после каждой реализованной фичи, код которой уже в основной ветке. Эти действия можно делать программно при помощи aws cli.

А что, если при таком подходе я захочу, чтобы каждый член команды мог с легкостью развернуть feature environment в облаке для разработки? Каждое новое окружение —  новый стейдж. Все стейджи и их переменные должны быть где-то прописаны в SAM шаблоне (не руками же это делать?!), чтобы AWS создал ApiGateway со всеми конфигурациями. Количество стейджей, алиасов и версий вырастет в разы.

Вот такая вот нетривиальная схема получается для подхода с одним стеком для всех окружений. 

Особенности этого подхода:

  • придется использовать более сложную конфигурацию ресурсов, используя CF. Работа со стейджами может быть реализована только при помощи CF ресурсов. Я специально не привожу примеры;

  • сложный CI/CD: добавляются действия по обновлению версий и алиасов после каждой сделанной фичи;

  • все окружения в “одном файле”, вероятность ошибки поломать “чужое” окружение возрастает. Не протестированный код может попасть на верхнее окружение;

  • создание feature environment превращается в боль, потому что постоянно требуется модифицировать шаблон (добавлять новые стейджи, удалять старые неиспользуемые).

Отдельный стек на каждое окружение

Альтернативой этому подходу является создание отдельного стека на каждое окружение, используя один и тот же шаблон. В этом случае окружения получаются полностью изолированными. Не нужны никакие версии, алиасы, стейджи. Лямбда в стеке для dev окружения отличается от лямбды для qa. Это разные ресурсы, разные объекты в облаке с разными ARN. Изменение одного окружения никак не повлияет на другое окружение. Этот подход будет работать всегда. Причем с любыми ресурсами. Во второй части я рассмотрел вариант, когда можно вынести глобальные общие ресурсы в отдельный стек. Этот подход отлично сочетается с разделением окружений на уровне стеков.

Этот вариант проще поддерживать и реализовывать, поэтому я останавливаюсь на нем. Это значит, что мой ApiGateway ресурс в шаблоне должен содержать только один стейдж. Пусть он будет prod.

Ок, со Stage разобрался. А что за история с OpenApiVersion? А это интересная история одного бага в SAM. Он до сих пор актуален. Баг заключается в том, что при создании ApiGateway помимо основного стейджа (по умолчанию prod), sam создает еще загадочный стейдж с именем “Stage”. Если в ресурсе указать поле OpenApiVersion, то этого не произойдет. Это работает только с вновь созданными ресурсами, испорченные ресурсы этот флажок лечить не умеет.

Интеграция лямбда функции с API

Теперь созданный ApiGateway ресурс можно подключить к лямбде в sam конфиге. Заодно сделаю более правильные REST урлы:

# ./template.yaml
GetProjectByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
			...
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /projects/{projectId}
            Method: get
            RestApiId: !Ref BugTrackerApi

Я добавил в путь переменную projectId, которая автоматически будет попадать в объект события — http запроса. Параметр RestApiId содержит идентификатор ресурса API. Аналогично подправлю и другие лямбда функции, чтобы они работали с моим ApiGateway.

ApiGateway настроен. Осталось поправить код лямбда функций. В коде обработчика моей лямбда функции в качестве первого аргумента фигурирует параметр event. Формат этого параметра отличается в зависимости от источника данных. Например, если я сделаю обычный запрос GET /projects/1, то параметр event будет иметь следующие поля (приведу только самые нужные и интересные данные):

{
  "pathParameters": { 
    "projectId": "1" 
  },
  "httpMethod": "GET",
...
}

Пример POST /projects/1 запроса:

{
  "pathParameters": { 
    "projectId": "1" 
  },
  "httpMethod": "POST",
  "body": "{\"message\": \"hello world\"}",
...
}

Теперь понятно, откуда брать идентификатор и query параметры, если они мне понадобятся:

// ./src/handlers/get-project-by-id/app.js
exports.lambdaHandler = async (event, context) => {
    const projectId = event.pathParameters.projectId;
    try {
        const dbPassword = ...;
        const dbConnection = ...;
        const [rows, fields] = await dbConnection.execute("SELECT * FROM `projects` where `id` = ?", [projectId]);

Что насчет ответа? В лямбде можно вернуть ответ абсолютно в любом формате, но для ApiGateway годится не все. Сервис поддерживает ответ от лямбда функций в следующем формате:

{
    "isBase64Encoded":false,
    "statusCode": 200,
    "headers": { "Content-Type": "application/json", ... },
    "multiValueHeaders": { "headerName": ["headerValue", "headerValue2", ...], ... },
    "body": "{\"status\":\"ok\"}"
}

Самые важные поля — это statusCode и body. Первое ApiGateway транслирует в http code. А содержимое body становится ответом, которое увидит клиент. Таким образом, если хочется вернуть красивый json в ответе, то надо в body передать строку, представляющую этот json. isBase64Encoded используется, когда нужно вернуть бинарные закодированные в base64 данные. Обновленная функция с поддержкой “правильного ответа” получается такой:

// ./src/handlers/get-project-by-id/app.js

const [rows, fields] = await dbConnection.execute(..., [projectId]);
var response = {
  statusCode: 404,
  body: {
    msg: "entity not found"
  }
};
if(rows.length == 1) {
  return {
    statusCode: 200,
    body: JSON.stringify(rows[0])
  };
} else {
  throw "Entity not found";
}

Лямбда функция может возвращать ошибки. Если делать обработчик в асинхронном стиле (c async и promise, как у меня), то любое необработанное исключение будет приводить к ошибке. Ошибки в ответе от лямбды выглядят вот так:

{
  "errorType": "TypeError",
  "errorMessage": "Cannot read property 'projectId' of undefined",
  "trace": [
    "TypeError: Cannot read property 'projectId' of undefined",
    "    at Runtime.exports.lambdaHandler [as handler] (/var/task/get_project_by_id.js:7:44)",
    "    at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)"
  ]
}

Поле errorMessage содержит текст ошибки. Поле errorType — тип ошибки. А поле trace содержит стектрейс ошибки, если он есть.

Как видно, этот формат не совместим в ApiGateway. Любые такие ошибки в лямбде ApiGateway рассматривает как внутренние ошибки. Вообще, если лямбда вернет что-то несовместимое по формату, то я увижу в ответе 502 ошибку. Чтобы таких неприятных ситуаций не случалось, в функции все ошибки необходимо отлавливать и оборачивать в соответствующую структуру. Это позволит установить необходимые заголовки, проставить правильный http code.

// ./src/handlers/get-project-by-id/app.js
try {
  	//db query here
    var response = ...;
		return response;
} catch (err) {
    //db exception
  	console.log("Cannot retrieve project by id", err);
		return {
  			statusCode: 500,
  			body: JSON.stringify({
    				msg: err.message
  			})
		};
}

Лямбда функцию я могу запускать локально, что позволяет ускорить разработку, уменьшает количество невынужденных ошибок. Но я также могу локально запустить  лямбда функцию в комплекте с http сервером, который будет играть роль ApiGateway. По традиции, опишу 2 варианта запуска: используя консольную команду и плагин AWS Toolkit для VSCode.

Запуск в консоли

Первый способ — запуск кода локально средствами sam cli. В этом режиме не подключается отладчик, запускается отдельный веб сервер, который будет работать, пока его явно не остановить. Этот сервер поддерживает hot reloading, все изменения в коде сразу применяются к запущенному приложению без его перезапуска. Сохраняется все состояние сервера. Команда запуска выглядит так:

serverless-bugtracker> sam local start api ./env.json

Нельзя забывать про файл с переменными окружения. Этот файл я создал для локального запуска лямбда функции в предыдущей части. Такая же логика работает и для запуска API. Поэтому этот файл можно использовать в обоих случаях.

Этот режим больше для локальной разработки, отладка тут возможна через боль. Для этого нужен сторонний отладчик, нужно запускать лямбду в специальном отладочном режиме... По ощущениям для отладки удобнее второй способ, который собственно для этого и создавался.

Запуск в VSCode

Второй способ — запуск приложения в режиме отладки через VSCode. Настройка отладки API отличается от аналогичной настройки лямбда функции:

// ./vscode/launch.json
{
  "type": "aws-sam",
  "request": "direct-invoke",
  "name": "API serverless-bugtracker:GetProjectByIdFunction",
  "invokeTarget": {
    "target": "api",
    "templatePath": "${workspaceFolder}/template.yaml",
    "logicalId": "GetProjectByIdFunction",
  },
  "api": {
    "path": "/projects/1",
    "httpMethod": "get",
    "payload": {
      "json": {}
    }
  },
  "sam": {
    "localArguments": ["--env-vars","D:\\serverless-bugtracker\\env.json"]
  }
},

Как видно из кода, при запуске api я в явном виде указываю url для запроса и данные, которые хочу послать. Я по-прежнему могу запускать лямбду функцию отдельно от всего в режиме отладки, только придется обновить раздел payload, потому что формат события теперь отличается от того, что я посылал ранее.

Если для лямбда функции можно было сразу определить настройки окружения прямо в конфигурации, то для API эти настройки приходится передавать через файл. В секции json (как и в случае лямбда функции) передаются данные для POST/PUT запросов. В логах можно увидеть, как запускается лямбда функция, ответы ApiGateway. Под капотом плагин вызывает ту же самую команду, что я описал в первом варианте sam local start-api. Правда, эта команда запускается из недр VSCode, именно поэтому требуется указать полный путь до файла с переменными. В этом режиме VSCode сразу подключает свой отладчик. После обработки запроса веб сервер и отладчик выключаются. Если хочется проверить код с разными параметрами, то придется каждый раз запускать приложение в режиме отладки.

Пришло время разворачивать код в облаке. В AWS Console можно найти урл, по которому доступен ApiGateway.

Интеграция с GitHub

Чтобы закончить необходимый функционал, мне осталось сделать интеграцию с GitHub в функции получения проектов. Потребуется сгенерировать специальный токен, чтобы пользоваться github api. Как и в случае с паролями от RDS, это значение необходимо положить в SSM. Работа с github-token аналогична работе с паролем к БД:

// ./src/handlers/get-project-by-id/app.js
const { Octokit } = require("@octokit/rest");

const githubTokenPromise = getParameterFromSsm("/dev/serverless-bugtracker/github-token");
const githubPromise = githubTokenPromise.then(token => {
    return new Octokit({
        auth: token, request:  {agent: new https.Agent({ keepAlive: true })}
    });
});
exports.lambdaHandler = async (event, context) => {
    //connect to db, etc...
    const [rows, fields] = ...;
    const project = rows[0];
    const { data: prInfo} = await listPullRequests(project['github_owner'], project['github_repo']);
    //parse pull requests and prepare response...
}

async function listPullRequests(projectOwner, projectRepo) {
    const githubClient = await githubPromise;
    return githubClient.rest.pulls.list(...);
}

Теперь на каждом запросе проекта я возвращаю список его PR из гитхаба. Аналогично первой функции, я настрою оставшиеся функции на работу с моим ApiGateway: добавлю URL, настрою VPC подсети.

Можно обратить внимание на то, что в коде функции зашиты имена SSM параметров с паролем и github токеном. Эти параметры глобальны для всех окружений. Но если посмотреть в template.yaml, то там можно обнаружить параметр, который по смыслу может принимать различные значения для разных окружений. Это параметр с именем базы данных. Очевидно, что для dev и qa окружений это имя должно отличаться.

Для начала мне потребуется дополнительный параметр - имя окружения:

Parameters:
  Env:
    Description: Environment name
    Type: String
    Default: dev

Дальше можно пойти двумя путями.

Первый - сделать строку-шаблон, в которую можно подставить значение Env. Этот способ подходит для простых случаев, когда значения могут быть легко вычислены на основе других параметров:

Globals:
  Function:
    Environment:
      Variables:
        DB_NAME: !Sub '{{resolve:ssm:/${Env}/serverless-bugtracker/db-name}}'

Второй - сделать специальное перечисление всех возможных значений в зависимости от выбранного окружения. Этот вариант подходит для случаев, когда значения не вычислимы от параметров. Например, я хочу иметь лямбда функции с разным объемом оперативной памяти в зависимости от окружения:

Mappings:
  LambdaSizes:
    dev:
      "memorySize": "128"
    qa:
      "memorySize": "128"
    stg:
      "memorySize": "512"


Globals:
  Function:
    MemorySize: !FindInMap [LambdaSizes, !Ref Env, memorySize]

Блок Mappings описывает простые структуры ключ-значение. В моем случае ключ - окружение, значение представляет собой объект, у которого меня интересует поле memorySize. Допустимо иметь несколько свойств в этом объекте. Функция FindInMap осуществляет чтение значения по ключу и свойству.

Настройка приватности

Получилось полноценное REST API. 

Но созданное API является публичным. Если выполнить запрос из браузера, то я смогу увидеть результат операции. В разных приложениях могут быть разные нефункциональные требования, которые касаются открытости приложения, контроля доступа (начиная от использования Bearer-токенов и интеграцией с AWS WAF и заканчивая использованием сервиса Cognito). Но я вдаваться во все эти механизмы не планирую, т.к. меня интересует базовый способ ограничения доступа к моему ApiGateway на сетевом уровне, то есть возможность работы и доступности ApiGateway только из моей VPC.

При создании нового API сервис ApiGateway предоставляет возможность указать в настройках параметр Endpoint Type, который можно задать одним из следующих типов:

  • edge-optimized - предназначен для организации глобального доступа к API. Для этого используются точки присутствия CloudFront (CDN от AWS) - то есть, запросы к этому типу ApiGateway будут направлены к географически ближайшей точке CloudFront;

  • regional - предполагает, что доступ будут получать из региона, где создано API;

  • private - вариант, который предполагает доступ к сервису только из VPC.

Меня из указанных вариантов, по очевидным причинам, заинтересовал тип Private. Для организации Private-варианта доступа в ApiGateway используются VPC Endpoints и Resource Policies. VPC Endpoints представляют из себя сетевой интерфейс, который позволяет создать приватную точку входа внутри VPC до определенных сервисов AWS, не поддерживающих прямое развертывание в VPC. Для наглядности и простоты понимания, что из себя представляет VPC Endpoints, проще всего ознакомиться с диаграммой ниже — пример организации доступа к ApiGateway по-умолчанию (красная линия) и через VPC Endpoint (зеленая линия). По умолчанию трафик к ApiGateway идет через публичную сеть Интернет, но в случае использования VPC Endpoint мой трафик не покинет пределов сети AWS:

Для создания VPC Endpoint я добавил необходимый ресурс в CF шаблон моей VPC. Данный ресурс содержит ряд интересных параметров, наиболее примечательные из которых:

  • PrivateDnsEnabled —  данная опция прозрачно настраивает DNS-имя для нашего приватного ApiGateway, то есть это позволит мне использовать указанный ApiGateway endpoint URL через созданный VPC Endpoint;

  • ServiceName  — собственно, позволяет мне указать, для какого именно сервиса AWS мне нужен VPC Endpoint —  получить полный список можно с помощью AWS CLI.

Ниже я привел код ресурса VPC Endpoint без всех зависимых ресурсов:

# ./global-resources/vpc.yaml
ApiGwVpcEndpoint:
  Type: AWS::EC2::VPCEndpoint
  Properties:
    PolicyDocument:
      Version: 2012-10-17
      Statement:
        - Effect: Allow
          Principal: '*'
          Action: '*'
          Resource: '*'
    SubnetIds:
      - !Ref PrivateSubnet00
      - !Ref PrivateSubnet01
    VpcEndpointType: Interface
    PrivateDnsEnabled: true
    SecurityGroupIds:
      - !Ref ApiGwSecurityGroup
    ServiceName: !Sub 'com.amazonaws.${AWS::Region}.execute-api'
    VpcId: !Ref VPC

В вышеупомянутых Resource Policies на основе различных критериев задаются правила доступа к ApiGateway. Ниже привожу ресурс ApiGateway после внесенных изменений для работы в Private-варианте:

# ./teamplate.yaml
BugTrackerApi:
    Type: AWS::Serverless::Api
    Properties:
    ...
      EndpointConfiguration:
        Type: PRIVATE
        VPCEndpointIds:
          - !ImportValue
            'Fn::Sub': '${GlobalResourceStack}-VPCeApiGW'
      Auth:
        ResourcePolicy:
          IntrinsicVpceWhitelist:
            - !ImportValue
              'Fn::Sub': '${GlobalResourceStack}-VPCeApiGW'

В добавленных настройках можно увидеть конфигурацию приватного Endpoint, где задается тип Endpoint и ID используемого VPC Endpoint, чуть ниже в разделе Auth настраивается Resource Policy, которую я использовал для ApiGateway в своем проекте.

Для настройки Resource Policy в описании SAM-ресурса для ApiGateway уже есть готовые шаблоны политик под разные критерии организации правил доступа, в моем случае я взял за основу шаблон IntrinsicVpceWhitelist. Данный шаблон задает политику доступа, которая разрешает взаимодействовать с ApiGateway только с определенного VPC Endpoint. То есть, для настройки Resource Policies можно использовать как один из готовых шаблонов (в виде allow/block-листов для IP Range, AWS Account ID, VPC или VPC-endpoint), так и полностью самостоятельно указать нужные правила в виде похожем на IAM-политику(CustomStatements). В случае самостоятельной настройки Resource Policy на первый взгляд может показаться, что это те же самые IAM-политики, но здесь есть ряд отличий. Основное отличие — это объект, на который применяется политика: в случае IAM это либо пользователь, группа или роль; а в случае Resource Policy объектом является сам сервис. Как я уже указал выше  — я воспользовался одним из готовых пресетов для настройки Resource Policy (разрешенный список VPC-endpoint —  IntrinsicVpceWhitelist). 

Таким образом, подведу итог по организации безопасного доступа к ApiGateway:  я создал Interface VPC Endpoint, настроил приватный Endpoint Type и задал необходимые правила для доступа в Resource Policy. В качестве результата я получил приватный доступ к ApiGateway. При этом нужно понимать, что Endpoint Type можно динамически менять в случае необходимости. 

Еще один важный момент, на который необходимо обратить внимание: после любого изменения конфигурации Resource Policy нужно обязательно сделать Deploy API, т.к. изменения в Resource Policy не применяются автоматически. Это можно сделать в UI с помощью соответствующего пункта меню Deploy API или с помощью команды:

aws apigateway create-deployment --rest-api-id xxxxxxxx --stage-name prod --description 'Deployment to the prod stage'

Параметр rest-api-id —  это идентификатор созданного ApiGateway ресурса. Взять его можно как в консоли AWS сервиса, так и с помощью следующей CLI команды:

aws apigateway get-rest-apis --query 'items[*].[name,id]'

Данная команда выведет информацию (имя и искомый идентификатор) по существующим ApiGateway. Пример вывода команды можно наблюдать ниже:

[
    [
        "myapi1",
        "bay55rt458"
    ],
    [
        "serverless-bugtracker-ch3",
        "q0cr55puj4"
    ]
]

Кроме того, после настройки приватного доступа может возникнуть резонный вопрос: а как мне самому получать доступ до ApiGateway, если я его сделаю приватным и доступ к нему возможен только из VPC? В третьей главе я уже развернул клиентский VPN от AWS, который позволил мне получить доступ до RDS и собственно через него я могу достучаться и до ApiGateway — никаких дополнительных действий не требуется.

И последний организационный момент, на котором стоит заострить внимание: через один VPC Endpoint можно обращаться к нескольким ApiGateway, в связи с этим CF-описание VPC Endpoint ресурса было добавлено в CF-стек для глобальных ресурсов. Это значит, что, например, в рамках нижних окружений каждый отдельный Application-стек (например, dev/stg/qa) будет использовать один глобальный VPC Endpoint, созданный в стеке с глобальными ресурсами.

Подведение итогов

В этой части я собрал все свои лямбда функции в одно API. Это API, как и все мое приложение разворачивается при помощи двух sam команд. Приложение по-прежнему запускается в режиме отладки и разворачивается из VSCode.

API работает только в VPC, где развернута ранее БД.

Приложение готово. Код можно найти по ссылке.

В последней части я рассмотрю вопросы, без которых выходить в прод рискованно:

  • настрою CI/CD для своего проекта;

  • рассмотрю логи приложения, как их настроить, где найти;

  • воспользуюсь AWS сервисом для трассировки;

  • закончу борьбу с cold start.

Теги:
Хабы:
+4
Комментарии 0
Комментарии Комментировать

Публикации

Информация

Сайт
www.lineate.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель

Истории