SparkleFormation

Если вы серьёзно используете AWS (Amazon Web Services), то наверняка знаете про возможность описать инфраструктуру с помощью JSON шаблонов. В AWS этот сервис называется CloudFormation. По сути это решение позволяет вам описать желаемое состояние любых ресурсов, доступных в AWS (инстансы, слои opsworks, ELB, security groups и т.д.). Набор ресурсов называется стеком. После загрузки CloudFormation шаблона система сама либо создаст необходимые ресурсы в стеке, если их ещё нет, либо попытается обновить существующие до желаемого состояния.

Это хорошо работает если у вас есть небольшое количество ресурсов, но как только инфраструктура разрастается появляются проблемы:
  • В JSON нет возможности использовать циклы и для похожих ресурсов приходится повторять одни и те же параметры и в случае изменения тоже (не DRY)
  • Для записи конфигурации для cloud-init нужен двойной escaping
  • В JSON нет комментариев и он имеет плохую человеко-читаеммость

Для того чтобы избежать подобных проблем инженеры из Heavy Water написали на ruby DSL и CLI для генерации и работы с этими шаблонами под названием SparkleFormation (github).

DRY


Когда я пришёл на свой текущий проект у нас был CloudFormation шаблон, содержащий около 1500 строк описания ресурсов и около 0 строк комментариев. После использования SparkleFormation шаблон стал занимать 300 строк, многие из которых комментарии. Как мы это добились? Для начала посмотрим как работает CloudFormation, типичное описание ресурса выглядит так:
Создание ELB
    "AppsElb": {
      "Type": "AWS::ElasticLoadBalancing::LoadBalancer",
      "Properties": {
        "Scheme": "internal",
        "Subnets": [
          {"Ref": "Subnet1"},
          {"Ref": "Subnet2"}
        ],
        "SecurityGroups": [
          {"Ref": "SG"}
        ],
        "HealthCheck": {
          "HealthyThreshold": "2",
          "Interval": "5",
          "Target": "TCP:80",
          "Timeout": "2",
          "UnhealthyThreshold": "2"
        },
        "Listeners": [
          {
            "InstancePort": "80",
            "LoadBalancerPort": "80",
            "Protocol": "TCP",
            "InstanceProtocol": "TCP"
          },
          {
            "InstancePort": "22",
            "LoadBalancerPort": "2222",
            "Protocol": "TCP",
            "InstanceProtocol": "TCP"
          },
          {
            "InstancePort": "8500",
            "LoadBalancerPort": "8500",
            "Protocol": "TCP",
            "InstanceProtocol": "TCP"
          }
        ]
      }
    }


Поскольку SparkleFormation позволяет использовать обычный ruby код внутри DSL, то переписать это можно так:
Создание ELB в SparkleFormation
  resources(:AppsElb) do
    type 'AWS::ElasticLoadBalancing::LoadBalancer'
    properties do
      scheme 'internal'
      subnets [PARAMS[:Subnet1], PARAMS[:Subnet2]]
      security_groups [ref!(:SG)]
      # port mapping 80->80, 22 -> 2222, etc.
      listeners = { :'80' => 80, :'2222' => 22, :'8500' => 8500 }.map do |k, v|
        { 'LoadBalancerPort' => k.to_s,
          'InstancePort' => v,
          'Protocol' => 'TCP',
          'InstanceProtocol' => 'TCP' }
      end
      listeners listeners
      health_check do
        target 'TCP:80'
        healthy_threshold '2'
        unhealthy_threshold '2'
        interval '5'
        timeout '2'
      end
    end
  end


Как можно заметить мы больше не повторяемся в описании каждого порта и добавление нового займёт у нас только одну строчку. Более того если у нам необходимо создать много почти однотипных ресурсов, но отличающихся по 1-2 параметрам, SparkleFormation предоставляет такую сущность как dynamics, где вы можете описать абстрактный ресурс, которому передают параметры:
Пример из документации
# dynamics/node.rb
SparkleFormation.dynamic(:node) do |_name, _config={}|
  unless(_config[:ssh_key])
    parameters.set!("#{_name}_ssh_key".to_sym) do
      type 'String'
    end
  end
  dynamic!(:ec2_instance, _name).properties do
    key_name _config[:ssh_key] ? _config[:ssh_key] : ref!("#{_name}_ssh_key".to_sym)
  end
end

А потом мы можем вызвать этот абстрактный ресурс в шаблоне:
SparkleFormation.new(:node_stack) do
  dynamic!(:node, :fubar)
  dynamic!(:node, :foobar, :ssh_key => 'default')
end


Таким образом мы можем повторно использовать нужные нам ресурсы и при необходимости изменения поменять все в 1 месте.

Cloud-init


Мы часто пользуемся возможностью передавать инстансу при загрузке cloud-init конфиг в виде yaml-файла и с помощью него выполнять установку пакетов, конфигурацию CoreOS, отдельных сервисов и других настроек. Проблема в том, что yaml должен передавать инстансу в user-data в CloudFormation шаблоне и выглядело это примерно так:
Безумный escaping
        "UserData": {
          "Fn::Base64": {
            "Fn::Join": [
              "",
              [
                "#cloud-config\n",
                "\n",
                "coreos:\n",
                "  etcd:\n",
                "    discovery: ", {"Ref": "AppDiscoveryURL"}, "\n",
                "    addr: $private_ipv4:4001\n",
                "    peer-addr: $private_ipv4:7001\n",
                "  etcd2:\n", 
...


Как можно видеть это абсолютно не читаемо, уродливо и плохо поддерживаемо, не говоря уже о том что о подсветке синтаксиса можно забыть. Благодаря тому что внутри DSL можно использовать ruby код, то весь yaml можно вынести в отдельный файл и просто вызывать:
user_data Base64.encode64(IO.read('files/cloud-init.yml'))

Как видно это намного приятнее чем редактировать его внутри JSON. Вместо IO.read можно использовать и HTTP вызов для любых параметров, если вам это нужно.

CLI


У себя в проекте мы используем собственную обёртку для управления шаблонами, но эта же команда предоставляет CLI (Command Line Interface) для управления шаблонами, называемый sfn. С помощью него можно загружать, удалять и обновлять CloudFormation стеки, командами sfn create, sfn destroy и sfn update. Там так же реализована интеграция с knife.

В целом после 4 месяцев использования SparkleFormation я им доволен и надеюсь больше не вернусь на plain JSON для описания инфраструктуры. В планах попробовать весь workflow, включая sfn, предлагаемый командой Heavy Water.

Комментарии (0)