Improper usage of cross-stack references in CloudFormation close to failure
В сентябре прошлого года AWS представила довольно интересный функционал - Cross-stack refereces в CloudFormation. Он позволяет ссылаться на значение 'Outputs' из других стеков в пределах одного региона. Смотрелся этот функционал очень заманчиво и удобно с точки зрения разработки шаблонов, а с учетом того, что у нас и так были проблемы с количеством параметров, мы недолго думая решили его попробовать. Возможным последствиям, к которым мог привести столь необдуманный шаг и желание облегчить разработку и будет посвящен этот пост.
Преамбула
Для начала немного опишу структуру наших шаблонов и проекта в общих чертах. Вся инфраструктура, начиная от VPC, SG, IAM policies и заканчивая RDS и EC2 описана с помощью шаблонов CloudFormation.
У нас есть главный стек, который включает в себя разные шаблоны, логически разбивающие инфраструктуру. Выглядит это примерно так:
main.json
├── network.json
├── queues.json
├── databases.json
└── ecs.json
├── private-cluster.json
└── public-cluster.json
...
Этот пример не отражает реальную инфраструктуру, а приведен в качестве отправной точки, для более корректного описания дальнейших нюансов.
Концепция
Так как у нас была проблема с передачей параметров во вложенные стеки из-за лимита в 60 значений мы реализовали custom-resource, с помощью lambda-функции, который предоставлял доступ к нужным данным внутри наших стеков, более детально я об этом писал в одном из предыдущих постов. В результате мы имели свой мини API внутри CloudFormation, данные из которого извлекались следующим образом:
"Fn::GetAtt": [
"CustomQueryResource",
"DB.Name"
]
Основным источником данных служил git-репозиторий. И собственно его-то мы и экспортировали с помощью cross-reference в CloudFormation. На вход нашему ресурсу нужно было передавать ссылку на репозиторий, где находились данные, и еще несколько параметров.
Было 2 варианта реализации:
- Передавать git-url в каждый стек из главного через ‘Parameters’ и ‘Ref’
- Экспортировать его в одном стеке и использовать в других стеках напрямую
Второй вариант выглядел очень привлекательно, несмотря на следующие ограничения:
- You can’t delete a stack if another stack references one of its outputs.
- You can’t modify or remove an output value that is referenced by another stack.
В итоге в каждом стеке мы имели Custom Resource, который импортировал данные из git-репозитория и мы указывали их в параметрах других ресурсов.
Проекту было уже несколько лет, за это время репозиторий не менялся и в принципе ничего не предвещало беды. Пока вечером я не получил уведомление от клиента о изменении доменного имени нашего git-репозитория. А это означало, что вся автоматизация сломается, как только они настроят редиректы.
Основное действие
Естественно вся наша инфраструктура, в том числе CI/CD после успешной миграции на новый домен всего Atlassian-suite перестала работать. Команда заблокирована, а мне предстояло выяснить, что работает, что нет и что чинить в первую очередь.
После починки CI, через который происходит создание и обновление наших стеков, я попробовал сделать тестовый деплоймент и естественно наткнулся на ошибку, о которой AWS предупреждал:
Embedded stack arn:aws:cloudformation:eu-west-1:000000000000:stack/CustomQueryResource-Y4TV5RZZH7XB/123a0d20-ef99-22e6-ac0e-12d5ca789ee6 was not successfully updated. Currently in UPDATE_ROLLBACK_IN_PROGRESS with reason: Export env-name-GitRepository cannot be updated as it is in use by env-name-ECSStack-00THLRVNSPUZ, env-name-ECSStack-00THLRVNSPUZ-ECSService1-1LBW2OVGL6IW8 and env-name-ECSStack-00THLRVNSPUZ-ECSService2-12INPIX4KD72Y (and 24 more)
Для нашей автоматизации это означало, что все ресурсы, зависящее от этой переменной должны быть удалены. Потом, отдельным деплойментом, обновляется экспортируемый параметр, и только после этого восстанавливаются ресурсы. Звучит вроде бы неплохо, но вот только примерный список того, что пришлось бы удалить:
- 2 ECS clusters
- ~25 ECS services
То есть весь бекэнд ушел бы в downtime где-то на час, с учетом размера нашей инфраструктуры и скорости работы CloudFormation. На всех средах.
На этом моменте я все-таки решил перепроверить есть ли альтернативы… Изначально я попытался минимизировать последствия и проверил возможность отключить только сервисы, обновив все остальное. Планируемый downtime - ~15 минут. После изменения необходимых параметров меня ждал второй подвох:
Failed to update resource. See the details in CloudWatch Log Stream: 2017/07/04/[$LATEST]bb1ecfdb878e4f858a34fb6311ff6e7f
В логах CloudWatch, уже lambda-функция ссылалась на ошибки, при попытке склонировать репозиторий и вот тут возник в каком-то смысле тупик. Изменить на новый репозиторий мы не можем, старый репозиторий уже не работает. И возникает куда более неприятный вопрос, а худший ли расклад - downtime в 1 час? Получится ли вообще хоть как-то восстановить работоспособность стека, без отключения отдельных ресурсов и общения с тех-поддержкой AWS?
Кульминация
Спасательным кругом оказалась сама lambda-функция, на которой все было завязано. Она обновлялась первой так как все остальное зависело от нее. Первой идеей было захардкодить в нее новые репозитории и последовательно, по очереди обновить все среды. Не самый элегантный способ, но спасал ситуацию при отсутствии downtime. К сожалению, он тоже не прошел из-за того, что были ресурсы, которые использовали другой git-репозиторий и даже с if-else не получалось корректно разграничить их.
К счастью, JavaScript предоставляет целый ряд полезных инструментов при работе со строками. И после пары тестов, решение выглядело примерно так:
const oldURLDomain = 'git.subdomain.customer.com';
const newURLDomain = 'newgit.subdomain.customer.com';
gitURL = gitURL.replace(oldURLDomain, newURLDomain);
После этого lambda-функция сама изменяла URL и все прекрасно заработало, несмотря на изменения.
Выводы
Куда уж без них.
- Во-первых, осторожней относится к новому функционалу;
- Во-вторых, как минимум оценить последствия ‘worst case scenario’ и решить изначально приемлемы ли они или нет;
- В-третьих, удобство иногда может стоить очень дорого в эквиваленте downtime’а.
Слава Богу, все закончилось без downtime и особых проблем. Но похоже этот функционал для этого проекта не приемлем и я надеюсь будет аккуратно убран.
Спасибо за внимание!
P.S.
Исходя из сложившейся ситуации, решение использовать этот функционал - спорное, но были следующие факторы, из-за которых мы его приняли:
- У нас уже были проблемы из-за большого количества параметров (60 максимум) и перекидывать их из стека в стек тоже не лучший выход;
- Мы не предполагали, что эти данные будут изменятся в последствии.
Есть определенные причины, по которым это не было согласовано, обсуждено заранее и спланировано. Мир аутсорса и аутстафа имеет свои нюансы, но несмотря на это мы постарались сделать все максимально аккуратно и без последствий.
Наверно самым оптимальным для нас вариантом был бы CNAME на старый репозиторий, чтобы ссылки работали без редиректов, но по разным причинам мы решали эту задачу самостоятельно.
Начало недели было продуктивным :)
