In this article we are going to describe how we realized our RDS infrastructure using Ansible as automation tool. We’ve completely avoided using AWS GUI, both for implementation and management activities.
Our aim was to develop a parametric infrastructure, able to adapt to all of our projects simply by changing few parameters in the config files.
Please note that at the time of writing we’re using Ansible 2.2.2.
First of all, we need to declare somewhere every needed variable. A var file, to be included where required, fits well.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
project: “project_name” rds_env: “project_env” database_instance_type: "{{ (rds_env == 'prod') | ternary('db.XX.XXXX','db.XX.XXXX') }}" billing_tag_value: "{{ (rds_env == 'prod') | ternary(project+'_prod',project+'_dev') }}" type_tag_value: "{{ rds_env }}_database" az_a: "{{ default_region }}a" az_b: "{{ default_region }}b" dns_ttl_expire: XX #Destroy all data and configurations rds_delete_all: "{{ (destroy == 'true') | ternary('yes','no') }}" rds_database_count_start: 0 rds_database_count_end: "{{ (rds_env == 'prod') | ternary(N,n) }}" rds_database_size: XXX rds_database_engine: MySQL rds_database_multi_zone: yes rds_database_name: "{{ project }}" rds_database_version: "5.X.X" rds_database_maint_window: "XXX:XX:XX-XXX:XX:XX" rds_database_option_group: "default:{{ rds_database_engine | lower }}-{{ rds_database_version.split('.')[0] }}-{{ rds_database_version.split('.')[1] }}" rds_database_parameter_group: "{{ project }}-{{ rds_env }}-{{ rds_database_version.replace('.', '-') }}" rds_database_port: 3306 rds_database_publicly_accessible: no rds_database_region: "{{ default_region }}" rds_database_upgrade: yes rds_database_zone: "{{ default_region }}" rds_database_disk_iops: XXXX rds_database_backup_retention: XX rds_database_backup_window: XX:XX-XX:XX #Security group firewall rules firewall_rules: - proto: all from_port: X to_port: X cidr_ip: IP #Parameters group for my.cnf parameters_my_cnf: max_connections: "XXX" |
Changing the file values it is possible to create N RDS servers and link them to the existing project’s VPC. As shown later, the infrastructure playbook will be invoked with a “rds_env” parameter. This way the infrastructure scripts can gather every VPC references for the project (specified by the “project” var) and create proper network connections towards the VPC itself.
Here are some examples of how it works:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
- name: INFRASTRUCTURE RDS | Getting vpc ID ec2_vpc_subnet_facts: filters: "tag:Name": "{{ project }}_{{ rds_env }}_vpc_subnet_0" register: vpc - name: INFRASTRUCTURE RDS | Create security_group RDS ec2_group: name: "{{ project }}_{{ rds_env }}_local_rds_sg" description: "{{ project | capitalize}}_{{ rds_env }} local RDS security group" vpc_id: "{{ vpc.subnets.0.vpc_id }}" rules: "{{ firewall_rules }}" register: security_group - name: INFRASTRUCTURE RDS | Tags security_group RDS ec2_tag: state: present tags: Name: "{{ project }}_{{ rds_env }}_local_rds_sg" billing: "{{ billing_tag_value }}" resource: "{{ security_group.group_id }}" - name: INFRASTRUCTURE RDS | Create subnet 0 for RDS ec2_vpc_subnet: state: present vpc_id: "{{ vpc.subnets.0.vpc_id }}" cidr: "10.0.{{ vpc.subnets.0.cidr_block.split('.')[2] | int + 1 }}.0/24" az: "{{ az_a }}" resource_tags: Name: "{{ project }}_{{ rds_env }}_rds_subnet_0" register: database_subnet_0 - name: INFRASTRUCTURE RDS | Create subnet 1 for RDS ec2_vpc_subnet: state: present vpc_id: "{{ vpc.subnets.0.vpc_id }}" cidr: "10.0.{{ vpc.subnets.0.cidr_block.split('.')[2] | int + 2 }}.0/24" az: "{{ az_b }}" resource_tags: Name: "{{ project }}_{{ rds_env }}_rds_subnet_1" register: database_subnet_1 - name: INFRASTRUCTURE RDS | Tags subnet 0 RDS ec2_tag: state: present tags: Name: "{{ project }}_{{ rds_env }}_local_rds_subnet" billing: "{{ billing_tag_value }}" resource: "{{ database_subnet_0.subnet.id }}" - name: INFRASTRUCTURE RDS | Tags subnet 1 RDS ec2_tag: state: present tags: Name: "{{ project }}_{{ rds_env }}_local_rds_subnet" billing: "{{ billing_tag_value }}" resource: "{{ database_subnet_1.subnet.id }}" - name: INFRASTRUCTURE RDS | Setting up RDS subnet group rds_subnet_group: state: present name: "{{ project }}_{{ rds_env }}_rds_vpc_subnet" description: RDS Subnet Group subnets: - "{{ database_subnet_0.subnet.id }}" - "{{ database_subnet_1.subnet.id }}" |
Now the RDS networking is set up and we can move on to the RDS instances creation.
As you might guess, the number of instances is defined in the vars file:
1 2 |
rds_database_count_start: 0 rds_database_count_end: "{{ (rds_env == 'prod') | ternary(N,n) }}" |
Here is the full command for instances creation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
- name: INFRASTRUCTURE RDS | Create Instance RDS rds: command: create instance_name: "{{ project }}-{{ rds_env }}-database-rds-{{ item }}" wait: yes wait_timeout: 3600 db_engine: "{{ rds_database_engine }}" db_name: "{{ rds_database_name }}" engine_version: "{{ rds_database_version }}" size: "{{ rds_database_size }}" instance_type: "{{ database_instance_type }}" username: "{{ mysql.root_username }}" password: "{{ mysql.root_password }}" multi_zone: "{{ rds_database_multi_zone }}" maint_window: "{{ rds_database_maint_window }}" parameter_group: "{{ rds_database_parameter_group }}" option_group: "{{ rds_database_option_group }}" port: "{{ rds_database_port }}" publicly_accessible: "{{ rds_database_publicly_accessible }}" region: "{{ rds_database_region }}" upgrade: "{{ rds_database_upgrade }}" subnet: "{{ project }}_{{ rds_env }}_rds_vpc_subnet" iops: "{{ rds_database_disk_iops }}" vpc_security_groups: "{{ security_group.group_id }}" backup_retention: "{{ rds_database_backup_retention }}" backup_window: "{{ rds_database_backup_window }}" tags: Name: "{{ project }}_{{ rds_env }}_database_{{ item }}" billing: "{{ billing_tag_value }}" env: "{{ project }}_{{ rds_env }}" role: "database" type: "{{ type_tag_value }}" register: rds_create_gathering with_sequence: start="{{ rds_database_count_start }}" end="{{ rds_database_count_end }}" |
Moreover, we chose to assign a DNS name using Route53, to facilitate instances management:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- name: INFRASTRUCTURE RDS | Gathering Instance RDS rds: command: facts instance_name: "{{ project }}-{{ rds_env }}-database-rds-{{ item }}" register: rds_create_gathering with_sequence: start="{{ rds_database_count_start }}" end="{{ rds_database_count_end }}" - name: INFRASTRUCTURE NUVOLA GENERIC | Create RDS DNS route53: command: create zone: "{{ domain_tld }}" record: "{{ project }}-{{ rds_env }}-database-rds-{{ item.1.instance.id.split('-')[4] }}.{{ domain_tld }}" type: CNAME value: "{{ item.1.instance.endpoint }}" overwrite: yes ttl: "{{ dns_ttl_expire }}" register: rds_dns_create with_indexed_items: '{{ rds_create_gathering.results }}' |
Last but not least, we also chose to automate, thanks to Ansible, the whole environment destruction. Of course we set up strict controls to avoid destroying or corrupting production environments.
Here are some examples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
- name: INFRASTRUCTURE RDS | Fact Instance RDS rds: command: facts instance_name: "{{ project }}-{{ rds_env }}-database-rds-{{ item }}" register: rds_create_gathering with_sequence: start="{{ rds_database_count_start }}" end="{{ rds_database_count_end }}" when: rds_delete_all == "yes" - name: INFRASTRUCTURE RDS | Delete Instance RDS rds: command: delete instance_name: "{{ project }}-{{ rds_env }}-database-rds-{{ item }}" wait: yes with_sequence: start=0 end="{{ rds_database_count_end }}" when: rds_delete_all == "yes" - name: INFRASTRUCTURE RDS | Delete RDS parameter groups rds_param_group: state: absent name: "{{ rds_database_parameter_group }}" when: rds_delete_all == "yes" - name: INFRASTRUCTURE RDS | Delete RDS subnet group rds_subnet_group: state: absent name: "{{ project }}_{{ rds_env }}_rds_vpc_subnet" when: rds_delete_all == "yes" - name: INFRASTRUCTURE RDS | Delete subnet 0 for RDS ec2_vpc_subnet: state: absent vpc_id: "{{ vpc.subnets.0.vpc_id }}" cidr: "10.0.{{ vpc.subnets_1 }}.0/24" when: rds_delete_all == "yes" - name: INFRASTRUCTURE RDS | Delete subnet 1 for RDS ec2_vpc_subnet: state: absent vpc_id: "{{ vpc.subnets.0.vpc_id }}" cidr: "10.0.{{ vpc.subnets_2 }}.0/24" when: rds_delete_all == "yes" - name: INFRASTRUCTURE RDS | Delete security_group RDS ec2_group: name: "{{ project }}_{{ rds_env }}_local_rds_sg" description: "{{ project | capitalize}}_{{ rds_env }} local RDS security group" state: absent register: security_group when: rds_delete_all == "yes" - name: INFRASTRUCTURE NUVOLA GENERIC | Delete RDS DNS route53: command: delete zone: "{{ domain_tld }}" record: "{{ project }}-{{ rds_env }}-database-rds-{{ item.0 }}.{{ domain_tld }}" type: CNAME value: "{{ item.1.instance.endpoint }}" ttl: "{{ dns_ttl_expire }}" with_indexed_items: '{{ rds_create_gathering.results }}' when: rds_delete_all == "yes" |
Now let’s glue the pieces together making use of a playbook, named infrastructure_rds.yml. As shown, secrets vars (db users, password, …) are kept in a different file.
1 2 3 4 5 6 7 |
vars_files: - vars/vars_rds_common.yml - vars/vars_rds_secure.yml - "inventories/group_vars/regions.yml" tasks: - include: roles/infrastructure/tasks/infrastructure_rds.yml |
Finally, for convenience, we wrapped it up with a bash script named infrastructure_rds.sh
1 2 3 4 5 6 7 8 9 10 |
#!/bin/bash … echo -e "\n\e[1m\e[42mSTARTING INFRASTRUCTURING OF [${RDS_ENV}] RDS INFRASTRUCTURE \e[0m" ansible-playbook --vault-password-file secrets/vars_rds_secure.secret \ ansible/infrastructure_rds.yml \ $TAGS_OPTION -e"$EXTRA_OPTIONS $RDS_ENV" echo -e "\n\e[1m\e[42m[${RDS_ENV}] RDS INFRASTRUCTURE COMPLETED\e[0m" |
Now the RDS infrastructure management is automated and written as code! We hope you can find it useful and remember that comments are always welcomed.
Thank you a lot!
Just so everyone knows, there is no way to create an encrypted RDS instance using ansible modules. You would have to use cloudformation, a call to the AWS CLI or the API to do so.
All three can be done using Ansible.
One solution is to integrate AWS command line into Ansible tasks.
– name: INFRASTRUCTURE RDS | Create Instance RDS
command: “aws rds create-db-instance
–db-instance-identifier {{ project }}-{{ rds_env }}-database-rds-{{ item }}
–db-instance-class {{ database_instance_type }}
–db-name {{ rds_database_name }}
–{{ (rds_database_multi_zone == ‘no’) | ternary(‘no-multi-az’,’multi-az’) }}
–engine {{ rds_database_engine }}
–engine-version {{ rds_database_version }}
–db-parameter-group-name {{ rds_database_parameter_group }}
–option-group-name {{ rds_database_option_group }}
–storage-type {{ rds_database_storage_type }}
–{{ (rds_database_encrypt_storage == ‘yes’) | ternary(‘storage-encrypted’,’no-storage-encrypted’) }}
–allocated-storage {{ rds_database_size }}
–master-username {{ mysql.root_username }}
–master-user-password {{ mysql.root_password }}
–vpc-security-group-ids {{ security_group.group_id }}
–port {{ rds_database_port }}
–db-subnet-group-name {{ project }}_{{ rds_env }}_rds_vpc_subnet
–{{ (rds_database_publicly_accessible == ‘yes’) | ternary(‘publicly_accessible’,’no-publicly-accessible’) }}
–preferred-maintenance-window {{ rds_database_maint_window }}
–{{ (rds_database_upgrade == ‘yes’) | ternary(‘auto-minor-version-upgrade’,’no-auto-minor-version-upgrade’) }}
–backup-retention-period {{ rds_database_backup_retention }}
–preferred-backup-window {{ rds_database_backup_window }}
–tags ‘Key=Name,Value={{ project }}_{{ rds_env }}_database_{{ item }}’ ‘Key=billing,Value={{ billing_tag_value }}'”
register: rds_create_gathering
with_sequence: start=”{{ rds_database_count_start }}” end=”{{ rds_database_count_end }}”
when: rds_delete_all == “no”
Thank you so much !!