AWS: CloudFormation – пример использования Conditions, Fn::Equals и Fn::If

Автор: | 20/04/2020

Имеется стек, в котором среди прочего создаются VPC Peerings, в данном примере – пиринг между VPC нового кластера Elastic Kubernetes Service и VPC стека с Prometheus.

Создание стеков и вообще всей этой автоматизации есть в постах AWS Elastic Kubernetes Service: автоматизация создания кластера, часть 1 — CloudFormation и AWS Elastic Kubernetes Service: — автоматизация создания кластера, часть 2 — Ansible, eksctl.

Задача: добавить возможность выбора – создавать пиринги, или нет.

Решение: используем AWS CloudFormation Conditions: создадим параметр VPCPeering, который из параметров Jenkins-джобы будет принимать значение true или false, а затем в зависимости от этого значения – будем принимать решение о создании ресурсов пиринга (сам пиринг и два роута).

Пример немного усложняется тем, что у нас используются Nested Stacks, и ресурсы разбросаны между ними, например, для создания пиринга:

  • root-stack:
    • создаёт Region и AZ-стеки
  • Region-stack:
    • создаёт ресурс AWS::EC2::VPCPeeringConnection
    • обновляет RouteTable в стеке Мониторинга – добавляет ресурс AWS::EC2::Route (маршрут из сети монитоирнга в сеть EKS)
    • в Outputs возвращает MonitoringProdVPCPeeringConnectionID
  • AZ-стек:
    • принимает MonitoringProdVPCPeeringConnectionID
    • создаёт ресурс AWS::EC2::Route (маршрут из приватных подсетей EKS в сеть мониторинга)

Доабвляем по очереди, и смотрим как всё это можно организовать.

Root stack

В Parameters добавляем новый параметр, назовём его VPCPeeringCreate, который принимает true или false:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "AWS CloudFormation stack for Kubernetes cluster",

  "Parameters": {

...

    "VPCPeeringCreate": {
      "Description": "Create or not VPC peering connections",
      "Type": "String",
      "Default": true,
      "AllowedValues": [
          "true",
          "false"
      ]
    }

Обновляем ресурс region-стека – добавляем передачу параметра VPCPeeringCreate:

  "Resources": {

    "RegionNetworkStack": {
      "Type": "AWS::CloudFormation::Stack",
      "Properties": {
        "TemplateURL": "eks-region-networking.json",
        "Parameters": {
          "VPCCIDRBlock": { "Ref": "VPCCIDRBlock" },
          "VPCPeeringCreate": { "Ref":  "VPCPeeringCreate"}
        }
      }
    },
...

Region Stack

Принимаем параметр VPCPeeringCreate – тут уже убираем дефолтное значение, так как оно должно придти из рутового стека:

{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "AWS CloudFormation Region Networking stack for Kubernetes cluster",

  "Parameters" : {
...

    "VPCPeeringCreate": {
      "Description": "Create or not VPC peering connections",
      "Type": "String",
      "AllowedValues": [
          "true",
          "false"
      ]
    }
  },
...

Создаём Conditions, в котором с помощью Fn::Equals проверяем значение из параметра VPCPeeringCreate, и если оно true – то возвращаем тоже true:

...
    "VPCPeeringCreate": {
      "Description": "Create or not VPC peering connections",
      "Type": "String",
      "AllowedValues": [
          "true",
          "false"
      ]
    }
  },

  "Conditions" : {
    "DoVPCPeeringCreate" : {"Fn::Equals" : [ {"Ref" : "VPCPeeringCreate"}, true] }
  },

...

Добавляем проверку обоим ресурсам MonitoringProdVPCPeeringConnection и MonitoringToEksProdPeeringRoute"Condition" : "DoVPCPeeringCreate":

...
    "MonitoringProdVPCPeeringConnection": {
      "Type": "AWS::EC2::VPCPeeringConnection",
      "Condition" : "DoVPCPeeringCreate",
      "Properties": {
        "VpcId": {
          "Ref": "VPC"
        },
        "PeerVpcId": { "Fn::ImportValue" : "monitoring-production-VPC-ID" },
        "PeerRegion": { "Fn::ImportValue" : "monitoring-production-StackRegion" },
        "Tags": [
          {
            "Key": "Name",
            "Value": { "Fn::Join": [ "-", [ {"Ref": "AWS::StackName"}, "vpc-monitoring-prod"] ] }
          }
        ]
      }
    },

    "MonitoringToEksProdPeeringRoute": {
      "Type": "AWS::EC2::Route",
      "Condition" : "DoVPCPeeringCreate",
      "Properties": {
        "RouteTableId": { "Fn::ImportValue" : "monitoring-production-VPC-PublicRouteTable" },
        "DestinationCidrBlock": { "Ref": "VPCCIDRBlock" },
        "VpcPeeringConnectionId": {
          "Ref": "MonitoringProdVPCPeeringConnection"
        }
      }
    },
...

Запускаем – всё создалось:

Unresolved resource dependencies и Fn::If

А теперь запускаем с VPCPeeringCreate == false, и получаем ошибку отсутствия данных для Outputs нашего регион-стека:

Логично – мы отключили создание пиринга, а в Outpus шаблона регион-стека по-прежнему пытаемся получить ID не созданного пиринга:

...
    "MonitoringProdVPCPeeringConnectionID": {
      "Description" : "MonitoringProdVPCPeeringConnection ID",
      "Value" : {"Ref" : "MonitoringProdVPCPeeringConnection" }
    }
...

Обновляем Outputs, и используем Fn::If для выбора того – какие данные возвращать:

...
  "Outputs" : {

    "VPCID" : {
      "Description" : "EKS VPC ID",
      "Value" : { "Ref" : "VPC" }
    },

    "IGWID" : {
      "Description" : "InternetGateway ID",
      "Value" : { "Ref" : "InternetGateway" }
    },

    "MonitoringProdVPCPeeringConnectionID": {
      "Description" : "MonitoringProdVPCPeeringConnection ID",
      "Value" : { "Fn::If" : [ "DoVPCPeeringCreate", {"Ref" : "MonitoringProdVPCPeeringConnection" }, "Zero" ] }
    }

  }
}
...

Т.е. если DoVPCPeeringCreate вернёт значение false – просто передаём что угодно, тут для примера слово “Zero”.

AvailabilityZones stack

В AZ-стеке надо сделать тоже самое – добавит параметр, добавить Conditions – копируем из region-стека, и для ресурса AWS::EC2::Route добавляем проверку условия "Condition" : "DoVPCPeeringCreate":

...
    "EksToMonitoringProdPeeringRoute": {
      "Type": "AWS::EC2::Route",
      "Condition" : "DoVPCPeeringCreate",
      "Properties": {
        "RouteTableId": {
          "Ref": "PrivateRouteTable"
        },
        "DestinationCidrBlock": {
          "Fn::ImportValue" : "monitoring-production-VPC-CIDR"
        },
        "VpcPeeringConnectionId": {
          "Ref": "MonitoringProdVPCPeeringConnectionID"
        }
      }
    },
...

В root-стеке добавляем передчу параметра VPCPeeringCreate в AZ-стек:

...
    "AZNetworkStackA": {
      "Type": "AWS::CloudFormation::Stack",
      "Properties": {
        "TemplateURL": "eks-azs-networking.json",
        "Parameters": {
          "VPCID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.VPCID"] },
          "AZ": { "Fn::Select": [ "0", { "Ref": "AvailabilityZones" } ] },
          "IGWID": { "Fn::GetAtt": ["RegionNetworkStack", "Outputs.IGWID"] },
          "VPCPeeringCreate": { "Ref":  "VPCPeeringCreate"},
...

Создаём стеки – и нашего пиринга нет:

Для проверки – ещё раз запускаем с VPCPeeringCreate == true:

Готово.