company_banner

Amazon + Ansible

    image + image

    В данной заметке хочу рассказать о некоторых приемах решений задач по связке ansible + amazon, может кому-то будет полезно или кто-то подскажет решения лучше. О установке/настройке ansible информации уже много, так что её я пропущу. О работе с amazon тоже ничего оригинального добавить не смогу. Итак, приступим.

    Введение


    Попросили помочь с разовой настройкой проекта, конкретно часть ansible + amazon, а так же конфигурация серверов на базе ubuntu и сервисов. Требования к проекты были выставлены такие:
    • Сервера балансировщики на nginx
    • N-е количество бэкендов nginx + nodejs
    • Сервер redis master
    • N-е количество redis slave
    • N-е количество windows servers ( что-то для криптографии, с условием готового шаблона)
    • Создавать очереди SQS
    • Создавать security groups


    Примечание

    Заказчик пожелал, что не нужно вносить правки на уже работающие сервера, если надо что-то поменять — меняют скрипты создания, сносят старое и создают новое… Хозяин — барин.

    Работаем

    Для большинства задач можно использовать модуль ec2. Модуль замечательный :), с большим количеством “подмодулей” почитать их описание можно тут: /usr/share/ansible/cloud (для ubuntu/debian).
    В инвентарный файл ansible (hosts) нужно прописать всего 1 хост:

    [local]
    localhost
    


    Я начал с самого простого, с windows server. Приведу пример playbook:

    - hosts: localhost
      connection: local
      gather_facts: False
      vars:
        hostname: Windows
        ec2_access_key: "Secret"
        ec2_secret_key: "Secret_key”
        instance_type: "t2.micro"
        image: "ami-xxxxxxxx"
        group: "launch-wizard-1"
        region: "us-west-2"
      tasks:
        - name: make one instance
          ec2: image={{ image }}
               instance_type={{ instance_type }}
               aws_access_key={{ ec2_access_key }}
               aws_secret_key={{ ec2_secret_key }}
               instance_tags='{ "Name":"{{ hostname }}" }'
               region={{ region }}
               group={{ group }}
               wait=true
    


    Тут все просто, ansible используя ваши aws_access_key и aws_secret_key посылает запрос в amazon на создание машинки с именем hostname=”Windows” типа instance_type=«t2.micro» из ранее созданого образа image=«ami-xxxxxxxx» в регионе region=«us-west-2» и присваивает sequrity group group=«launch-wizard-1» и больше ничего.

    Далее сложнее, будем делать бэкенды nginx. Для начала все то же самое, создаем машину, но нужно с ней работать в этом же плэйбуке.
    Создадим в кабинете amazon keypair, назовем его, например, aws_ansible. Скачиваем ключик и копируем его в ~/.ssh/id_rsa пользователя из под которого запускаете плэйбуки.

    Приведу в пример плэйбук создания бэкенда:

    - hosts: localhost
      connection: local
      gather_facts: False
      vars:
        hostname: Nginx_nodejs
        ec2_access_key: “Secret"
        ec2_secret_key: "Secret_key"
        keypair: "aws_ansible"
        instance_type: "t2.micro"
        image: "ami-33db9803"
        group: "launch-wizard-1"
        region: "us-west-2"
      tasks:
        - name: make one instance
          ec2: image={{ image }}
               instance_type={{ instance_type }}
               aws_access_key={{ ec2_access_key }}
               aws_secret_key={{ ec2_secret_key }}
               keypair={{ keypair }}
               instance_tags='{ "Name":"{{ hostname }}" , "Group":"nginx_backend" }'
               region={{ region }}
               group={{ group }}
               wait=true
          register: ec2_info
    
        - debug: var=ec2_info
        - debug: var=item
          with_items: ec2_info.instance_ids
    
        - add_host: hostname={{ item.public_ip }} groupname=ec2hosts
          with_items: ec2_info.instances
    
        - name: wait for instances to listen on port:22
          wait_for:
            state=started
            host={{ item.public_dns_name }}
            port=22
          with_items: ec2_info.instances
    
    - hosts: ec2hosts
      gather_facts: True
      user: ubuntu
      sudo: True
      vars:
         connections : "4096"
    
      tasks:
         - include: nginx/tasks/setup.yml
      handlers:
         - name: restart nginx
           action: service name=nginx state=restarted
    
    - hosts: ec2hosts
      gather_facts: True
      user: ubuntu
      sudo: True
      
      tasks:
     - include: nodejs/tasks/setup.yml
    


    Теперь, что поменялось:
    Мы указали, что для этой машинки использовать наш ключ keypair: «aws_ansible»
    Указали образ чистой ubuntu image: «ami-33db9803».
    С помощью registr и debug мы получили public_ip новой машинки и записали её во временную инвентаризацию в группу ec2hosts, записывать в hosts файл из плэйбука нельзя ( я не нашел как).
    Следующим действием “wait for instances to listen on port:22” ждем когда станет доступен ssh.
    И после всего этого выполняем обычные сценарии с обычным сервером, в моем случае установка/настройка nginx и nodejs

    Еще я добавил Tag «Group»:«nginx_backend», это нужно для того что бы работать со всеми бэкендами сразу. Как? Есть скрипт подходящий для инвентаризации серверов amazon в ansible. Почитать о нем, а так же скачать его можно тут docs.ansible.com/intro_dynamic_inventory.html#id6.

    Отлично, но у меня ситуация не много иная, мне нужно сделать upstream в nginx с неизвестным заранее количеством бэкендов. Побороздив просторы документации по ansible, не нашел как делать динамические списки. То есть подставлять динамически ip бэкенда — пожалуйста, а вот их количество менять… Как всегда на выручку пришел старый способ, написал велосипед ан python. не большой скрипт который вызывается из плэйбука до настройки nginx и генерирует конфиг с upstream.

    Листинг:

    #!/usr/bin/env python
    
    import  sys, os
    from commands import *
    
    group = '"tag_Group_nginx_backend": ['
    template = "/etc/ansible/playbooks/nginx/templates/balance.conf.j2"
    list_ip = []
    #Create ec2_list
    data = getoutput("/etc/ansible/ec2.py --refresh-cache")
    
    flag = 0
    for line in data.split("\n"):
        if flag:
            if line.strip() != "],":
                list_ip.append(line.strip().strip(",").strip("\""))
            else:
                break
        if line.strip() == group:
            flag = 1
    
    f = open(template, 'w')
    f.write('''# upstream list
    upstream backend {''')
    f.close()
    for ip in list_ip:
        f = open(template, 'a')
        f.write('''
        server '''+ip+''':80 weight=3 fail_timeout=15s;''')
        f.close()
    f = open(template, 'a')
    f.write('''
    }''')
    f.close()
    


    Еще одна проблема была с redis master, его ip нужно было прописывать на каждый слэйв. Решил сделать с помощью include_vars.

    При создании мастера до проверки доступности ssh делаю так:

       - replace: dest={{ redis_master_ip }} regexp='^(\s+)(master\:)\s(.*)$' replace='\1\2 {{ item.public_ip }}'
          with_items: ec2_info.instances
    

    В переменных указал:

      redis_master_ip: "/etc/ansible/playbooks/redis/files/master_ip.yml"
    

    Сам файл изначально должен быть и выглядит примерно так:

     master: 1.2.3.4
    

    Затем в плейбуке настройки redis slave добавляем:

    - name: Get master IP
      include_vars: "{{ redis_master_ip }}"
    


    Используем переменную {{ master }} в шаблоне.

    Sequrity group создается просто, используем модуль ec2_group:

    - hosts: localhost
      connection: local
    
      tasks:
        - name: nginx  ec2 group
          local_action:
            module: ec2_group
            name: nginx
            description: an nginx EC2 group
            region: us-west-2
            aws_secret_key: "Secret"
            aws_access_key: "Secret"
            rules:
              - proto: tcp
                from_port: 80
                to_port: 80
                cidr_ip: 192.168.0.0/24
              - proto: tcp
                from_port: 22
                to_port: 22
                cidr_ip: 0.0.0.0/0
            rules_egress:
              - proto: all
                cidr_ip: 0.0.0.0/0
    


    Сложнее оказалось с очередями, модуля для них не нашлось, пытался даже доделать чьи-то попытки его написать. Но быстро опомнился и сделал через cloudformation.

    Так выглядит плэйбук:

    - hosts: localhost
      connection: local
      gather_facts: False
      vars:
        sqs_access_key: “Secret"
        sqs_secret_key: "Secret"
        region: "us-west-2”
      tasks:
      - name: launch some aws services
        cloudformation: >
          stack_name="TEST"
          region={{ region }}
          template=files/cloudformation.json
    <\code>
    
    А так template:
    
    <code>
    {
      "AWSTemplateFormatVersion" : "2010-09-09",
      "Description" : "AWS CloudFormation SQS”
      "Resources" : {
        "MyQueue" : {
          "Type" : "AWS::SQS::Queue"
        }
      },
      "Outputs" : {
        "QueueURL" : {
          "Description" : "URL of newly created SQS Queue",
          "Value" : { "Ref" : "MyQueue" }
        },
        "QueueARN" : {
          "Description" : "ARN of newly created SQS Queue",
          "Value" : { "Fn::GetAtt" : ["MyQueue", "Arn"]}
        }
      }
    }
    


    Резюме

    Популярность облачных решений и систем управления конфигурациями растет, но я потратил порядочно времени выискивая варианты того или иного решения, собирая информацию и тд. Отсюда и родилась идея написать эту статью.

    Автор: Бурнашев Роман, главный системный администратор компании centos-admin.ru
    • +4
    • 10,4k
    • 2
    Southbridge
    401,16
    Обеспечиваем стабильную работу highload-проектов
    Поделиться публикацией

    Похожие публикации

    Комментарии 2

      0
      Я предпочитаю для таких целей в AWS использовать их родное средство — CloudFormation.
        0
        Спасибо за статью.
        Не пробовали deploy-ить amazon каким-либо другим прибором, кроме ansible?
        Я не так силен в Ansible, но глядя на python-оский велосипед и строчки вида:
           - replace: dest={{ redis_master_ip }} regexp='^(\s+)(master\:)\s(.*)$' replace='\1\2 {{ item.public_ip }}'
        

        Становится немного страшно. По-моему это не очень удобно и интересно решается ли задача из статьи другими средствами более элегантно.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое