How Can I Release Code On 50% Machine In Ansible
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:
- Ansible Installed: You should have Ansible installed on your control machine. If not, you can install it using pip:
pip install ansible
- 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:
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.aws configure
- 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. - 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:
-
Install
boto
andboto3
: These are the AWS SDKs for Python, which theaws_ec2
plugin uses to communicate with AWS:pip install boto boto3
-
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 theaws_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
). -
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']) return instances
def main(): instances = get_ec2_instances() inventory = 'tag_Name_ad_server', '_meta': 'hostvars' } }
for reservation in instances['Reservations']: for instance in reservation['Instances']: instance_id = instance['InstanceId'] inventory['tag_Name_ad_server']['hosts'].append(instance_id) inventory['_meta']['hostvars'][instance_id] = { 'public_ip': instance.get('PublicIpAddress'), 'private_ip': instance.get('PrivateIpAddress') } if len(sys.argv) == 2 and sys.argv[1] == '--list': print(json.dumps(inventory, indent=4)) elif len(sys.argv) == 3 and sys.argv[1] == '--host': host_details = inventory['_meta']['hostvars'].get(sys.argv[2], {}) print(json.dumps(host_details, indent=4)) else: print('Usage: {} (--list | --host <hostname>)'.format(sys.argv[0]))
if name == 'main': main()
-
Make the Script Executable:
chmod +x ec2.py
-
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
. -
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:
- Gather Facts: Collect information about the target machines.
- Calculate the Subset: Determine 50% of the instances to deploy to.
- 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: "Number of instances to update: {{ subset_size }}"
- name: Select a subset of instances
set_fact:
subset_hosts: "{{ ansible_play_hosts | slice(0, subset_size) | list }}"
- name: Display the subset of instances
debug:
msg: "Subset of hosts: {{ subset_hosts }}"
- name: Deploy code to the subset of instances
block:
- name: Stop the application
# Replace with your actual task to stop the application
command: echo "Stopping the application"
- name: Deploy new code
# Replace with your actual deployment task
command: echo "Deploying new code"
- name: Start the application
# Replace with your actual task to start the application
command: echo "Starting the application"
delegate_to: "{{ item }}"
loop: "{{ subset_hosts }}"
- name: Display completion message
debug:
msg: "Code deployment completed on {{ subset_size }} instances."
Explanation
hosts: tag_Name_ad_server
: This line specifies that the playbook will run on hosts that match the tagName=ad_server
. Ansible uses the dynamic inventory to resolve these hosts.become: yes
: This directive tells Ansible to use privilege escalation (sudo) for the tasks, as deployment often requires administrative privileges.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.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.vars
: Defines variables that can be used in the playbook.percentage: 50
: Sets the percentage of machines to deploy to.
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 calculatedsubset_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 firstsubset_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 byitem
.loop: "{{ subset_hosts }}"
: Iterates over thesubset_hosts
list, withitem
representing the current host.
- Display completion message:
debug
: Prints a completion message to the console.
- Calculate the number of instances to update:
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
orsystemd
module to stop the application service. - Copying new code: Use the
copy
orsynchronize
module to transfer the new code to the target machines. - Running database migrations: Use the
command
orshell
module to execute migration scripts. - Starting the application: Use the
service
orsystemd
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:
- "--delete"
- 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: "{{ item }}"
loop: "{{ subset_hosts }}"
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.