CloudFormation Parameters limit or how to clone Git repo inside AWS 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.
Получаются следующие варианты:
- Компилировать git-cli локально и добавлять в зависимости;
- Использовать библиотеку 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 фунции не представляеться возможным - ©