How Can I Release Code On 50% Machine In Ansible

by ADMIN 49 views

Introduction

When managing infrastructure as code, Ansible is a powerful tool for automating tasks across a fleet of servers. One common requirement is to perform rolling deployments, where updates are applied to a subset of machines at a time. This approach minimizes downtime and allows for easier rollback in case of issues. In this article, we'll explore how to use Ansible to release code on 50% of your machines in AWS, focusing on a scenario where you have 100 EC2 instances tagged with Name=ad_server. We'll provide a detailed walkthrough of creating an Ansible playbook, explain the concepts involved, and offer best practices for ensuring a smooth and reliable deployment process. This guide aims to help you understand how to leverage Ansible's capabilities for efficient and controlled deployments in your AWS environment.

Prerequisites

Before we dive into the playbook, let's ensure you have the necessary prerequisites in place:

  1. Ansible Installed: You should have Ansible installed on your control machine. If not, you can install it using pip:
    pip install ansible
    
  2. AWS Credentials Configured: Your Ansible control machine needs to be able to authenticate with AWS. The recommended way is to configure AWS credentials using the AWS CLI:
    aws configure
    
    This will prompt you for your AWS Access Key ID, Secret Access Key, default region, and output format. Alternatively, you can set the AWS credentials as environment variables.
  3. EC2 Instances: You should have 100 EC2 instances running in AWS, all tagged with Name=ad_server. These instances will be the target of our deployment.
  4. Ansible Inventory: You need to set up your Ansible inventory to recognize these instances. We'll cover this in detail in the next section.

Setting Up the Ansible Inventory

Ansible uses an inventory file to understand which machines it can manage. For AWS EC2 instances, we can use the aws_ec2 dynamic inventory plugin. This allows Ansible to automatically discover instances based on tags and other criteria. Here’s how to set it up:

  1. Install boto and boto3: These are the AWS SDKs for Python, which the aws_ec2 plugin uses to communicate with AWS:

    pip install boto boto3
    
  2. Create an Inventory File: Create a file named ec2.ini (or any name you prefer) in your Ansible project directory. This file will contain the configuration for the aws_ec2 plugin.

    [aws]
    regions = <your_aws_region>
    

    Replace <your_aws_region> with the AWS region where your EC2 instances are running (e.g., us-east-1).

  3. Create an Inventory Script: Create an executable script named ec2.py in the same directory. This script will be the dynamic inventory script that Ansible uses.

    You can download the script from the Ansible documentation or GitHub. Here's a basic example:

    #!/usr/bin/env python
    

    import os import sys import json import boto3

    def get_ec2_instances(): ec2 = boto3.client('ec2') instances = ec2.describe_instances(Filters=['Name' 'tag:Name', 'Values': ['ad_server']]) return instances

    def main(): instances = get_ec2_instances() inventory = 'tag_Name_ad_server' { 'hosts': [] , '_meta': 'hostvars' { } }

    for reservation in instances[&#39;Reservations&#39;]:
        for instance in reservation[&#39;Instances&#39;]:
            instance_id = instance[&#39;InstanceId&#39;]
            inventory[&#39;tag_Name_ad_server&#39;][&#39;hosts&#39;].append(instance_id)
            inventory[&#39;_meta&#39;][&#39;hostvars&#39;][instance_id] = {
                &#39;public_ip&#39;: instance.get(&#39;PublicIpAddress&#39;),
                &#39;private_ip&#39;: instance.get(&#39;PrivateIpAddress&#39;)
            }
    
    if len(sys.argv) == 2 and sys.argv[1] == &#39;--list&#39;:
        print(json.dumps(inventory, indent=4))
    elif len(sys.argv) == 3 and sys.argv[1] == &#39;--host&#39;:
        host_details = inventory[&#39;_meta&#39;][&#39;hostvars&#39;].get(sys.argv[2], {})
        print(json.dumps(host_details, indent=4))
    else:
        print(&#39;Usage: {} (--list | --host &lt;hostname&gt;)&#39;.format(sys.argv[0]))
    

    if name == 'main': main()

  4. Make the Script Executable:

    chmod +x ec2.py
    
  5. Test the Inventory: Verify that Ansible can read the inventory by running:

    ansible-inventory -i ec2.py --list
    

    This should output a JSON structure representing your EC2 instances tagged with Name=ad_server.

  6. Configure Ansible: In your ansible.cfg file, ensure that the inventory is set to use the dynamic inventory script:

    [defaults]
    inventory = ./ec2.py,
    

    If you don't have an ansible.cfg file, you can create one in your project directory or in ~/.ansible/.

With the inventory set up, Ansible can now dynamically discover your EC2 instances. Next, we'll create the playbook to deploy code to 50% of these instances.

Creating the Ansible Playbook

Now that our inventory is set up, we can create the Ansible playbook to deploy code to 50% of the machines. The playbook will perform the following steps:

  1. Gather Facts: Collect information about the target machines.
  2. Calculate the Subset: Determine 50% of the instances to deploy to.
  3. Deploy Code: Deploy the code to the selected instances.

Here’s the playbook (deploy.yml):

---  
- hosts: tag_Name_ad_server  
  become: yes  
  remote_user: ubuntu  
  gather_facts: yes

vars:
percentage: 50 # Percentage of machines to deploy to

tasks: - name: Calculate the number of instances to update set_fact: subset_size: "{{ (ansible_play_hosts | length * percentage / 100) | round(0, 'ceil') | int }}"

- name: Display the number of instances to update
  debug:
    msg: &quot;Number of instances to update: {{ subset_size }}&quot;

- name: Select a subset of instances
  set_fact:
    subset_hosts: &quot;{{ ansible_play_hosts | slice(0, subset_size) | list }}&quot;

- name: Display the subset of instances
  debug:
    msg: &quot;Subset of hosts: {{ subset_hosts }}&quot;

- name: Deploy code to the subset of instances
  block:
    - name: Stop the application
      # Replace with your actual task to stop the application
      command: echo &quot;Stopping the application&quot;  

    - name: Deploy new code
      # Replace with your actual deployment task
      command: echo &quot;Deploying new code&quot;

    - name: Start the application
      # Replace with your actual task to start the application
      command: echo &quot;Starting the application&quot;
  delegate_to: &quot;{{ item }}&quot;
  loop: &quot;{{ subset_hosts }}&quot;
  
- name: Display completion message
  debug:
    msg: &quot;Code deployment completed on {{ subset_size }} instances.&quot;

Explanation

  1. hosts: tag_Name_ad_server: This line specifies that the playbook will run on hosts that match the tag Name=ad_server. Ansible uses the dynamic inventory to resolve these hosts.
  2. become: yes: This directive tells Ansible to use privilege escalation (sudo) for the tasks, as deployment often requires administrative privileges.
  3. remote_user: ubuntu: Specifies the user account to use when connecting to the remote hosts. This is a common user on Ubuntu-based EC2 instances.
  4. gather_facts: yes: Ansible gathers facts about the target systems, such as the operating system, IP addresses, and more. This information can be used in tasks and templates.
  5. vars: Defines variables that can be used in the playbook.
    • percentage: 50: Sets the percentage of machines to deploy to.
  6. tasks: A list of tasks to be executed on the target hosts.
    • Calculate the number of instances to update:
      • set_fact: Creates a new fact (subset_size) that is the result of the calculation.
      • ansible_play_hosts | length: Gets the total number of hosts in the play.
      • * percentage / 100: Calculates the desired number of hosts based on the percentage.
      • round(0, 'ceil'): Rounds the result up to the nearest whole number.
      • int: Converts the result to an integer.
    • Display the number of instances to update:
      • debug: Prints a message to the console.
      • msg: The message to print, which includes the calculated subset_size.
    • Select a subset of instances:
      • set_fact: Creates a new fact (subset_hosts) that is a subset of the hosts.
      • ansible_play_hosts | slice(0, subset_size): Selects the first subset_size hosts from the list of hosts.
      • list: Converts the result to a list.
    • Display the subset of instances:
      • debug: Prints the selected subset of hosts to the console.
    • Deploy code to the subset of instances:
      • block: Groups multiple tasks together.
      • Stop the application: Replace the command with the actual command to stop your application.
      • Deploy new code: Replace the command with the actual deployment steps (e.g., copying files, running scripts).
      • Start the application: Replace the command with the actual command to start your application.
      • delegate_to: "{{ item }}": Executes the tasks on the host specified by item.
      • loop: "{{ subset_hosts }}": Iterates over the subset_hosts list, with item representing the current host.
    • Display completion message:
      • debug: Prints a completion message to the console.

Customizing the Deployment Tasks

The Deploy code to the subset of instances task block is where you’ll define your actual deployment steps. This might include:

  • Stopping the application: Use the service or systemd module to stop the application service.
  • Copying new code: Use the copy or synchronize module to transfer the new code to the target machines.
  • Running database migrations: Use the command or shell module to execute migration scripts.
  • Starting the application: Use the service or systemd module to start the application service.

Here’s an example of how you might customize the deployment tasks:

    - name: Deploy code to the subset of instances
      block:
        - name: Stop the application service
          service:
            name: your_application_service
            state: stopped
    - name: Deploy new code
      synchronize:
        src: /path/to/your/local/code
        dest: /path/to/your/remote/code
        rsync_opts:
          - &quot;--delete&quot;

    - name: Run database migrations
      command: /usr/bin/your_migration_command
      args:
        chdir: /path/to/your/application

    - name: Start the application service
      service:
        name: your_application_service
        state: started
  delegate_to: &quot;{{ item }}&quot;
  loop: &quot;{{ subset_hosts }}&quot;

Replace the placeholders (e.g., your_application_service, /path/to/your/local/code) with your actual values.

Running the Ansible Playbook

To run the playbook, use the ansible-playbook command:

ansible-playbook -i ec2.py deploy.yml

This command tells Ansible to use the ec2.py inventory script and execute the deploy.yml playbook. Ansible will connect to the specified hosts, execute the tasks, and provide output on the console.

Best Practices and Considerations

Error Handling

Ensure your playbook includes proper error handling. Use the ignore_errors directive or the rescue block to handle potential failures gracefully. This prevents a single failed task from halting the entire deployment.

Rollback Strategy

Implement a rollback strategy. If a deployment fails, you should have a way to revert to the previous version. This might involve keeping a backup of the previous code version or using a version control system to easily revert changes.

Configuration Management

Use Ansible to manage configuration files. This ensures consistency across all your instances. Templates can be used to create configuration files dynamically based on variables.

Secrets Management

Avoid hardcoding secrets in your playbooks. Use Ansible Vault to encrypt sensitive data, or use a secrets management tool like HashiCorp Vault.

Idempotency

Ensure your tasks are idempotent. This means that running a task multiple times should have the same effect as running it once. Ansible modules are designed to be idempotent, but custom scripts should also be written with idempotency in mind.

Testing

Test your playbook in a non-production environment before deploying to production. This helps identify potential issues and ensures that the deployment process is reliable.

Logging and Monitoring

Set up logging and monitoring to track deployments and identify issues. Ansible's output can be logged, and you can integrate with monitoring tools like Prometheus or Grafana to track application health.

Advanced Techniques

Rolling Updates with serial

For more granular control over rolling updates, you can use the serial directive. This allows you to specify how many hosts to update at a time.

- hosts: tag_Name_ad_server
  become: yes
  remote_user: ubuntu
  gather_facts: yes
  serial:
    - "25%"

The serial directive specifies that Ansible should update 25% of the hosts at a time. This provides a more controlled deployment process.

Using Handlers

Handlers are tasks that are only run when notified by another task. This is useful for tasks like restarting a service after a configuration file has been updated.

    - name: Update application configuration
      template:
        src: templates/application.conf.j2
        dest: /path/to/application.conf
      notify:
        - Restart application

handlers: - name: Restart application service: name: your_application_service state: restarted

Using Tags

Tags allow you to run specific parts of a playbook. This is useful for breaking up a large playbook into smaller, more manageable chunks.

    - name: Deploy new code
      synchronize:
        src: /path/to/your/local/code
        dest: /path/to/your/remote/code
        rsync_opts:
          - "--delete"
      tags:
        - deploy

To run only the tasks with the deploy tag, use the --tags option:

ansible-playbook -i ec2.py deploy.yml --tags deploy

Conclusion

Deploying code to a subset of machines is a critical part of managing infrastructure and applications in a scalable and reliable manner. Ansible provides the tools and flexibility to automate this process effectively. By using dynamic inventories, calculating subsets of hosts, and defining clear deployment tasks, you can ensure that your deployments are consistent, repeatable, and safe. Remember to follow best practices such as implementing error handling, having a rollback strategy, and testing your playbooks thoroughly.

This article has provided a comprehensive guide on how to release code on 50% of your machines using Ansible. By following the steps outlined here and adapting the examples to your specific environment, you can streamline your deployment process and reduce the risk of downtime. Ansible's power and flexibility make it an indispensable tool for any team managing infrastructure as code.