cloudformation + lambda

AWS Cloudformation - замечательный сервис, но у него есть некоторые ограничения, которые к сожалению невозможно пока снять даже через тикет в службу поддержки. Это история о том как можно получить те же данные, но используя другой подход: git-repository, AWS Lambda и CloudFormation CustomResource.

Итак, для начала разберемся - зачем?

На проекте, над которым я сейчас работаю - мы используем подход Infrastructure-as-a-Code реализованный с помощью CloudFormation для управления инфраструктурой. Шаблоны используются в буквальном смысле для всего, что предоставляет AWS и соответственно те или иные ресурсы нуждаются в конфигурации посредством параметров.

Проблема с которой мы столкнулись - ограничение входящих параметров в CloudFormation, их лимит равен 60, подробнее о лимитах можно почитать тут. Выход из ситуации предложенный разработчиками AWS - использовать ‘CommaDelimitedList’, который имеет ограничение в 4096 байтов, а это весьма не мало, но возникают сразу несколько проблем:

  • Проблемы с запятыми(,) - так как они являются разделителем;
  • Проблемы удобства доступа к значениям, так как фактически вы получаете значение обращаясь как к массиву, через индекс;
  • Очень тяжело поддерживать такой code-base.

Каковы альтернативы?

В AWS есть замечательная связка, которая может решить эту проблему - AWS Lambda и AWS CloudFormation CustomResource. Идея заключается в следующем - создать Lambda-функцию, которая будет читать данные из git-репозитория или s3-bucket’a и просто возвращать прочитанные данные, тем самым передавать их внутрь CloudFormation, где они уже будут нам доступны с помощью функции Fn::GetAtt.

От идеи к реализации

Собственно наше решение будет состоять из следующих компонентов:

  • AWS Lambda реализованная на NodeJS 4.3;
  • AWS CloudFormation шаблона со всем необходимым для CustomResource’a.

Еще нам понадобиться Git-репозиторий с конфигурациями, к примеру spring-cloud-samples/config-repo.

Success criteria: данные из config-repo доступны в ‘Outputs’ секции нашего AWS CloudFormation Stack’a.

AWS Lambda

Основные задачи функции - склонировать git-репозиторий и вернуть их нашему CustomResource.

Какие у нас есть ограничения?

  • По документации размер функции, со всеми зависимостями - 50мб(запакованный)/250мб(распакованный);
  • Git-cli недоступен внутри Lambda-окружения;
  • Размер репозитория <= 500мб.

В итоге, что мы имеем?

Если репозиторий с Вашими конфигурационными файлами весит больше 500мб, тогда скорей всего Git отпадает из-за того, что вы попросту не сможете его склонировать из-за лимита в 500мб на /tmp директорию, единственное место, куда можно записывать данные. Скорей всего Ваш выбор в данном случае - AWS S3 + AWS SDK.

Второй неприятностью, которая нас ждет это собственно вопрос как клонировать, ведь git-cli нету :( Большинство библиотек, которые я пересмотрел на NPMJS, являлись wrapper’ами для того самого git-cli, кроме одной - NodeGit.

Получаются следующие варианты:

  1. Компилировать git-cli локально и добавлять в зависимости;
  2. Использовать библиотеку NodeGit.

Первый случай я попробовал и он сразу не взлетел, к тому же сборка git-cli из исходников под конкретную платформу не вписывалась во временные рамки. Поэтому я решил перейти к более гибкому и в какой-то мере правильном подходу - использование NodeGit библиотеки. Кому все таки интересно - эта же тема на StackOverflow и проект автора ответа.

Вариант номер два. Ну во первых, зачем тут вообще NodeJS?! Перефразирую всем известную: “Так сложились обстаятельства” на IT-манер: “Так сложились технические требования”. Лично мое мнение использовать event-loop asynchronous язык для написания процедурного кода на JS и всеми этими Promise’ами - это процесс болезненный. Но, тем не менее… :) Пару слов о проблемах с NodeJS и Lambda: “не все nodejs модули одинаково полезные”, а именно, как оказалось NodeGit собирается под конкретную платформу, поэтому изначально, то что я загрузил у меня выпало с ошибкой:

module initialization error: Error
at Error (native)
at Object.Module._extensions..node (module.js:434:18)
at Module.load (module.js:343:32)
at Function.Module._load (module.js:300:12)
at Module.require (module.js:353:17)
at require (internal/module.js:12:17)
at Object.<anonymous> (/var/task/node_modules/nodegit/dist/nodegit.js:11:12)
at Module._compile (module.js:409:26)
at Object.Module._extensions..js (module.js:416:10)
at Module.load (module.js:343:32)

После непродолжительного гугления, оказалось, что некоторые модули нужно собирать под “правильную” платформу, в данному случае правильная платформа - та, которая используется Lambda-окружением. Но не все оказалось так просто, у меня не получилось собрать NodeGit на:

  • Amazon Linux
  • CentOS 6

Потому, что была старая версия libstdc++, и опять таки времени разбираться как ее можно пофиксить по “workaround-методологии” не было.

Поэтому выбор пал на CentOS 7.3, но там все равно она не устанавливалась с помощью:
npm install, но в Issues на GitHub есть решение для CentOS.
Собрать с помощью: BUILD_ONLY=true npm install nodegit.

Полный список необходимых действий на CentOS 7.3 Minimal:

sudo yum groupinstall Base Development tools -y
sudo yum install -y openssl-devel
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.1/install.sh | bash
nvm install v4.3.2
# в директории с package.json
BUILD_ONLY=true npm install

После этого все заработало.

Пару слов о сборке deployment-package для AWS Lambda.

Не смотря на ограничеие в 50мб, пакет с NodeGit будет по размеру около 100мб, а в развернутом виде около 300мб. В AWS Console стоит ограничение - 10мб, поэтому я загружал ее через AWS S3-bucket и Lambda несмотря на размер спокойно работала…

Так как у меня не новый AWS аккаунт, то возможно там сняли эти ограничения через запрос в техподдержку.

AWS CloudFormation

Для тестирования нашей концепции нам понадобиться создать шаблон с минимальным набором ресурсов, а имено:

  • AWS::Lambda::Function
  • AWS::IAM::Role
  • Custom::OurResourceName

По порядку, AWS::Lambda::Function

Создает нашу функцию забирая ее из S3-bucket’a, сам S3-bucket нет смысла создавать внутри CloudFormation, так как нам туда нужно еще загрузить функцию.

"Resources": {
  "LambdaForCustomResource": {
    "Type": "AWS::Lambda::Function",
    "Properties": {
      "Code": {
        "S3Bucket": "test-bucket",
        "S3Key": "deployment-package.zip"
      },
      "Handler": "index.handler",
      "Role": {
        "Fn::GetAtt": [
          "LambdaExecutionRole",
          "Arn"
        ]
      }
    }
  }
}

AWS::IAM::Role

Нужна для определения политик к каким ресурсам у функции будет доступ.

Custom::OurResourceName

Собственно наш кастомный ресурс ServiceToken которого смотрит на AWS::Lambda::Function.

Далее мы уже в Outputs-секции нашего шаблона можем получить доступ к данным:

"Resources": {
  "CustomResourceName": {
    "Type": "Custom::OurResourceName",
    "Properties": {
      "ServiceToken": {
        "Ref": "LambdaForCustomResource"
      },
      "InputDataExample": "https://github.com/spring-cloud-samples/config-repo.git"
    }
  }
}
"Outputs": {
  "ExampleData": {
    "Description": "Example data from our repository",
    "Value": {
      "Fn::GetAtt": [
        "CustomResourceName",
        "examplevalue"
      ]
    }
  }
}

В заключение

На этом эпопея закончилась и тестовые данные оказались на выходе!

Приведенный способ не идеален и имеет некоторые ограничения, но он позволяет сделать работу с конфигурациями более аккуратной, храня ее централизированно будь-то S3-bucket или Git-репозиторий. Также позволит облегчить автоматизацию убрав скрипты с кучей sed и grep для парсинга и подстановки параметров, если у Вас таковые в наличии.

С другой стороны возможно апелировать к передаче на вход аккуратного JSON файла, который можно подать на вход aws-cli, но опять такие, туда поместятся только 60 аккуратных параметров ;)

P.S.

К сожалению, выложить примеры исходников Lambda фунции не представляеться возможным - ©