Эти 6 уроков работы с cloudformation я усвоил на всю жизнь

Я начал работать с cloudformation 4 года назад. С тех пор я сломал много инфраструктур, даже те, которые уже были в продукции. Но каждый раз, когда я что-то портил, узнавал новое. Благодаря этому опыту, я поделюсь некоторыми из самых важных уроков, которые я выучил.

Эти 6 уроков работы с cloudformation я усвоил на всю жизнь

Урок 1: проверяйте изменения перед тем, как развернуть их

Я усвоил этот урок вскоре как начал работать с cloudformation. Не помню, что именно я тогда сломал, но точно помню, что использовал команду aws cloudformation update. Эта команда просто выкатывает шаблон без какой-либо проверки изменений, которые будут развернуты. Не думаю, что требуются объяснения, для чего нужно проверить все изменения перед тем, как развернуть их.

После этого провала, я сразу же изменил deployment pipeline, заменив команду update командой create-change-set

# OPERATION is either "UPDATE" or "CREATE"
changeset_id=$(aws cloudformation create-change-set 
    --change-set-name "$CHANGE_SET_NAME" 
    --stack-name "$STACK_NAME" 
    --template-body "$TPL_PATH" 
    --change-set-type "$OPERATION" 
    --parameters "$PARAMETERS" 
    --output text 
    --query Id)

aws cloudformation wait 
    change-set-create-complete --change-set-name "$changeset_id"

Когда набор изменений создан, он никак не влияет на существующий стек. В отличие от команды update, подход с использованием набора изменений не вызывает фактического развертывания. Вместо этого он создает список изменений, которые вы можете просмотреть до развертывания. Вы можете просмотреть изменения в интерфейсе консоли aws. Но если вы предпочитаете автоматизировать всё, что только можно, то проверяйте их в CLI:

# this command is presented only for demonstrational purposes.
# the real command should take pagination into account
aws cloudformation describe-change-set 
    --change-set-name "$changeset_id" 
    --query 'Changes[*].ResourceChange.{Action:Action,Resource:ResourceType,ResourceId:LogicalResourceId,ReplacementNeeded:Replacement}' 
    --output table

Эта команда должна выдать вывод, похожий на следующий:

--------------------------------------------------------------------
|                         DescribeChangeSet                        |
+---------+--------------------+----------------------+------------+
| Action  | ReplacementNeeded  |      Resource        | ResourceId |
+---------+--------------------+----------------------+------------+
|  Modify | True               |  AWS::ECS::Cluster   |  MyCluster |
|  Replace| True               |  AWS::RDS::DBInstance|  MyDB      |
|  Add    | None               |  AWS::SNS::Topic     |  MyTopic   |
+---------+--------------------+----------------------+------------+

Обратите особое внимание на изменения, где Action это Replace, Delete или где ReplacementNeeded — True. Это самые опасные изменения и обычно они приводят к потере информации.

Когда изменения просмотрены, они могут быть развернуты

aws cloudformation execute-change-set --change-set-name "$changeset_id"

operation_lowercase=$(echo "$OPERATION" | tr '[:upper:]' '[:lower:]')
aws cloudformation wait "stack-${operation_lowercase}-complete" 
    --stack-name "$STACK_NAME"

Урок 2: используйте stack policy для предотвращения замены или удаления ресурсов с сохранением состояния

Иногда простого просмотра изменений недостаточно. Все мы люди и все допускаем ошибки. Вскоре после того, как мы начали использовать наборы изменений, мой товарищ по команде неосознанно выполнил развертывание, что привело к обновлению базы данных. Ничего страшного не произошло, потому что это была среда тестирования.

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

Есть ресурсы, которые вы никогда не захотите заменить или удалить. Это statefull сервисы, такие как экземпляр базы данных RDS или кластер elastichsearch и т. д. Было бы хорошо, если бы aws автоматически отказывал в развертывании, если выполняемая операция потребует удаления такого ресурса. К счастью, cloudformation имеет встроенный способ сделать это. Это называется stack policy, и об этом можно больше узнать в документации:

STACK_NAME=$1
RESOURCE_ID=$2

POLICY_JSON=$(cat <<EOF
{
    "Statement" : [{
        "Effect" : "Deny",
        "Action" : [
            "Update:Replace",
            "Update:Delete"
        ],
        "Principal": "*",
        "Resource" : "LogicalResourceId/$RESOURCE_ID"
    }]
}
EOF
)

aws cloudformation set-stack-policy --stack-name "$STACK_NAME" 
    --stack-policy-body "$POLICY_JSON"

Урок 3: используйте UsePreviousValue, когда обновляете стек с секретными параметрами

Когда вы создаете сущность RDS mysql AWS требует от вас предоставить MasterUsername и MasterUserPassword. Поскольку лучше не хранить секреты в исходном коде, и я хотел автоматизировать абсолютно все, я реализовал «умный механизм», при котором перед развертыванием учетные данные будут получены из s3, и если учетные данные не будут найдены, новые учетные данные генерируются и хранятся в s3.

Затем эти учетные данные будут переданы в качестве параметров команде cloudformation create-change-set. Во время экспериментов со скриптом случилось, что соединение с s3 было потеряно, и мой «умный механизм» рассматривал его как сигнал для генерации новых учетных данных.

Если бы я начал использовать этот скрипт в рабочей среде, и проблема с подключением возникла бы снова, он бы обновил стек новыми учетными данными. В этом конкретном случае ничего плохого не произойдет. Однако я отказался от такого подхода и начал использовать другой, предоставляя учетные данные только один раз — при создании стека. И позже, когда стек потребует обновления, я бы вместо указания секретного значения параметра просто использовал UsePreviousValue=true:

aws cloudformation create-change-set 
    --change-set-name "$CHANGE_SET_NAME" 
    --stack-name "$STACK_NAME" 
    --template-body "$TPL_PATH" 
    --change-set-type "UPDATE" 
    --parameters "ParameterKey=MasterUserPassword,UsePreviousValue=true"

Урок 4: используйте rollback configuration

Другая команда, с которой я работал, использовала функцию cloudformation, называемую rollback configuration. Я не встречался с ней раньше и быстро понял, что это сделает развертывание моих стеков еще круче. Теперь я использую каждый раз, когда разворачиваю свой код в lambda или ECS с помощью cloudformation.

Как это работает: вы указываете CloudWatch alarm arn в параметре —rollback-configuration, когда создаете набор изменений. Позже, когда вы выполните набор изменений, aws отслеживает alarm не менее одной минуты. Он откатывает развертывание обратно, если в течение этого времени alarm изменяет состояние на ALARM.

Ниже приведен пример отрывка шаблона cloudformation, в котором я создаю cloudwatch alarm, отслеживающий пользовательскую метрику облака в виде числа ошибок в журналах облака (метрика создается через MetricFilter):

Resources:
  # this metric tracks number of errors in the cloudwatch logs. In this
  # particular case it's assumed logs are in json format and the error logs are
  # identified by level "error". See FilterPattern
  ErrorMetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      LogGroupName: !Ref LogGroup
      FilterPattern: !Sub '{$.level = "error"}'
      MetricTransformations:
      - MetricNamespace: !Sub "${AWS::StackName}-log-errors"
        MetricName: Errors
        MetricValue: 1
        DefaultValue: 0

  ErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "${AWS::StackName}-errors"
      Namespace: !Sub "${AWS::StackName}-log-errors"
      MetricName: Errors
      Statistic: Maximum
      ComparisonOperator: GreaterThanThreshold
      Period: 1 # 1 minute
      EvaluationPeriods: 1
      Threshold: 0
      TreatMissingData: notBreaching
      ActionsEnabled: yes

Сейчас alarm может быть использован как rollback триггер при выполнении набора инструментов:

ALARM_ARN=$1

ROLLBACK_TRIGGER=$(cat <<EOF
{
  "RollbackTriggers": [
    {
      "Arn": "$ALARM_ARN",
      "Type": "AWS::CloudWatch::Alarm"
    }
  ],
  "MonitoringTimeInMinutes": 1
}
EOF
)

aws cloudformation create-change-set 
    --change-set-name "$CHANGE_SET_NAME" 
    --stack-name "$STACK_NAME" 
    --template-body "$TPL_PATH" 
    --change-set-type "UPDATE" 
    --rollback-configuration "$ROLLBACK_TRIGGER"

Урок 5: убедитесь, что вы разворачиваете самую последнюю версию шаблона

Легко развернуть не самую последнюю версию шаблона cloudformation, но это нанесет большой ущерб. Однажды так было у нас: разработчик не отправил последние изменения из Git и неосознанно развернул предыдущую версию стека. Это привело к простою приложения, которое использовало этот стек.

Что-то простое, например, добавление проверки, актуальна ли ветка, прежде чем выполнять развертывание, будет хорошо (если предположить, что git — это ваш инструмент контроля версий):

git fetch
HEADHASH=$(git rev-parse HEAD)
UPSTREAMHASH=$(git rev-parse master@{upstream})

if [[ "$HEADHASH" != "$UPSTREAMHASH" ]] ; then
   echo "Branch is not up to date with origin. Aborting"
   exit 1
fi

Урок 6: не изобретайте велосипед

Может показаться, что развертывание с cloudformation — это легко. Вам просто нужна куча скриптов bash, выполняющих команды aws cli.

4 года назад я начинал с простых скриптов, которые называют aws cloudformation create-stack командой. Вскоре скрипт уже не был простым. Каждый выученный урок делал скрипт все более и более сложным. Было не только сложно, но и с кучей багов.

Сейчас я работаю в небольшом ИТ-отделе. Опыт показывает, что у каждой команды есть свой способ развертывания стеков cloudformation. И это плохо. Было бы лучше, если бы все использовали единый подход. К счастью, существует множество инструментов, которые помогают развернуть и настроить стеки cloudformation.

Эти уроки помогут вам избежать ошибок.

Источник: habr.com