Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation Update - Added a guide for using AWS EC2 Driver for molecule #4146

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .ansible-lint-ignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# This file contains ignores rule violations for ansible-lint
src/molecule/test/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/meta/main.yml role-name
src/molecule/test/scenarios/driver/delegated_invalid_role_name_with_role_name_check_equals_to_1/meta/main.yml schema[meta]

# Added no-handler exclusions the tasks the ansible-lint refers to need to be run this way and connect be run as handlers
molecule/aws_ec2/create.yml no-handler
molecule/aws_ec2/destroy.yml no-handler
106 changes: 106 additions & 0 deletions docs/guides/AWS-EC2-Driver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
## Using molecule with AWS EC2 Instances

### Introduction

This guide will show you how to use molecule with AWS EC2 instances to test your Ansible roles and playbooks.

### Prerequisites

!!! note

You will need to have the AWS CLI Tools installed and configured and this guide does not cover this topic or how to create an IAM user.
Please refer to the [official AWS documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) for instructions on how to do this.

Utilise [pip](https://pip.pypa.io/en/stable/installation/) to install boto3 and botocore packages.

```bash
pip install boto3 botocore
```

Utilise [pip](https://pip.pypa.io/en/stable/installation/) to install the molecule ec2 drivers.

```bash
pip install molecule molecule-drivers[ec2]
```

### Molecule EC2 Guide

Create a new role with molecule specifying the ec2 driver.

```bash
molecule init scenario -d ec2
```

This will create a stock standard molecule scenario, but you will still need to add and update other files within the molecule directory
as there are some missing files and configurations that are required to run molecule with the ec2 driver effectively.

The molecule directory should look like this once the steps below are completed:

```bash
molecule/
└── default/
├── templates
│ └── iam_role_ec2_trust_policy.json.j2
├── converge.yml
├── create.yml
├── destroy.yml
├── molecule.yml
└── prepare.yml
```

Create a templates folder under the molecule's scenario directory

```bash
mkdir -p molecule/default/templates
```

Create a file called `iam_role_ec2_trust_policy.json.j2` under the templates folder. When molecule runs the create step, ansible will create
a IAM Role that will be attached to the instance (as per AWS Best Practices) The instance can use this role to access AWS resources (if required).

```json title="iam_role_ec2_trust_policy.json.j2"
{!../molecule/aws_ec2/templates/iam_role_ec2_trust_policy.json.j2!}
```

!!! note

By default this role attaches the `AmazonSSMManagedInstanceCore` policy to the role. This will
allow you to use SSM to access the instance via the AWS console if required. Refer to
[AWS SSM - Session Manager Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html)
for more information

Update the `create.yml` file with the codeblock below. When molecule runs the create step the following actions will occur.

- Create a security group called `molecule`
- Create a temporary SSH Key Pair that is uploaded to the AWS Console
- Create the IAM Role called `molecule-unit-test-ec2-role`. As stated above this will be attached to the instance
- Create the EC2 Instance

```yaml
{!../molecule/aws_ec2/create.yml!}
```

Update the `destroy.yml` file with the codeblock below. When molecule runs the destroy step, it will clean up all the temporary resources
that it created within the create step.

```yaml
{!../molecule/aws_ec2/destroy.yml!}
```

Update the `molecule.yml`. This file is the main configuration file for molecule. It specifies the driver, which AMIs to use,
which vpc and subnet's to use etc. The codeblock below shows a working example that ties all the above steps together.

```yaml
{!../molecule/aws_ec2/molecule.yml!}
```

Utilise the normal molecule commands to test your role.

```bash
molecule create
molecule converge
molecule verify
molecule destroy
```

If you check the AWS EC2 Console you will see the instance that molecule created with the naming convention `molecule-unit-test-<%OS%>`
![EC2Console](../images/ec2_instance_console.png)
Binary file added docs/images/ec2_instance_console.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ nav:
- examples/podman.md
- examples/kubevirt.md
- Guides:
- guides/AWS-EC2-Driver.md
- guides/custom-image.md
- guides/docker-rootless.md
- guides/monolith.md
Expand Down
10 changes: 10 additions & 0 deletions molecule/aws_ec2/converge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
- name: Converge
hosts: all
gather_facts: true
become: yes
become_method: ansible.builtin.sudo
tasks:
- name: Deploy Ansible Role
ansible.builtin.include_role:
name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}"
182 changes: 182 additions & 0 deletions molecule/aws_ec2/create.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
- name: Create
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
vars:
ssh_port: 22

security_group_name: molecule
security_group_description: Security group for testing Molecule
security_group_rules:
- proto: tcp
from_port: "{{ ssh_port }}"
to_port: "{{ ssh_port }}"
cidr_ip: "0.0.0.0/0"
- proto: icmp
from_port: 8
to_port: -1
cidr_ip: "0.0.0.0/0"
security_group_rules_egress:
- proto: -1
from_port: 0
to_port: 0
cidr_ip: "0.0.0.0/0"

key_pair_name: molecule_key
key_pair_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"

tasks:
# Retrieve the AWS account ID which can be used in future tasks in the playbook
- name: Retrieve AWS account ID
amazon.aws.aws_caller_info:
register: caller_info

# Debug option - Uncomment to use
#- name: Show AWS Account ID
# debug:
# msg: "{{ caller_info.account }}"

- name: Find the vpc for the subnet
amazon.aws.ec2_vpc_subnet_info:
subnet_ids: "{{ item.vpc_subnet_id }}"
loop: "{{ molecule_yml.platforms }}"
register: subnet_info

- name: Create security groups
amazon.aws.ec2_security_group:
vpc_id: "{{ item.subnets[0].vpc_id }}"
name: "{{ security_group_name }}"
description: "{{ security_group_name }}"
rules: "{{ security_group_rules }}"
rules_egress: "{{ security_group_rules_egress }}"
loop: "{{ subnet_info.results }}"

- name: Test for presence of local key pair
ansible.builtin.stat:
path: "{{ key_pair_path }}"
register: key_pair_local

- name: Delete remote key pair
amazon.aws.ec2_key:
name: "{{ key_pair_name }}"
state: absent
when: not key_pair_local.stat.exists

- name: Create key pair
amazon.aws.ec2_key:
name: "{{ key_pair_name }}"
register: key_pair

- name: Persist the key pair
ansible.builtin.copy:
dest: "{{ key_pair_path }}"
content: "{{ key_pair.key.private_key }}"
mode: 0600
when: key_pair.changed

- name: Get the ec2 ami(s) by owner and name, if image not set
amazon.aws.ec2_ami_info:
owners: "{{ item.image_owner }}"
filters:
name: "{{ item.image_name }}"
loop: "{{ molecule_yml.platforms }}"
when: item.image is not defined
register: ami_info

# TODO: RESOLVE DEPRECATION WARNINGS
- name: Create IAM role for molecule unit test instance(s)
amazon.aws.iam_role:
name: molecule-unit-test-ec2-role
state: present
create_instance_profile: true
assume_role_policy_document: "{{ lookup('file', 'templates/iam_role_ec2_trust_policy.json.j2') }}"
path: "/"
description: this role is used by molecule to test ansible playbooks on ec2 instances. This role provides the necessary permissions to run the certain playbook actions.
managed_policy:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
register: role

- name: Attach the molecule IAM role to instance profile
amazon.aws.iam_instance_profile:
state: present
name: molecule-unit-test-ec2-role
role: molecule-unit-test-ec2-role

- name: Create molecule instance(s)
amazon.aws.ec2_instance:
name: "molecule-unit-test-{{ item.name }}"
iam_instance_profile: molecule-unit-test-ec2-role
key_name: "{{ key_pair_name }}"
image_id: "{{ item.image if item.image is defined else (ami_info.results[index].images | sort(attribute='creation_date', reverse=True))[0].image_id }}"
instance_type: "{{ item.instance_type }}"
vpc_subnet_id: "{{ item.vpc_subnet_id }}"
security_group: "{{ security_group_name }}"
# This tags line below is a bit a of hack to get dynamically populate the instance's username (if there are multiple in use or multiple Operating Systems in use)
# this allows the rest the Ansible playbook to add the correct username to the inventory file that is created when molecule runs the create step
tags: "{{ item.instance_tags | combine({'instance': item.name, 'ssh_user': item.ssh_user}) if item.instance_tags is defined else {'instance': item.name, 'ssh_user': item.ssh_user} }}"
wait: true
network:
assign_public_ip: true
count: 1
register: server
loop: "{{ molecule_yml.platforms }}"
loop_control:
index_var: index
async: 7200 # noqa: fqcn[action-core]
poll: 0

- name: Wait for instance(s) creation to complete
ansible.builtin.async_status:
jid: "{{ item.ansible_job_id }}"
register: ec2_jobs
until: ec2_jobs.finished
retries: 300
with_items: "{{ server.results }}"

# DEBUG OPTION - Uncomment to use
#- name: print ec2_jobs.results
# debug:
# msg: "{{ ec2_jobs.results }}"

# Mandatory configuration for Molecule to function. - DO NO MODIFY
- name: Populate instance config dict
ansible.builtin.set_fact:
instance_conf_dict:
{
"instance": "{{ item.instances[0].tags.instance }}",
"address": "{{ item.instances[0].public_ip_address }}",
"user": "{{ item.instances[0].tags['ssh_user'] }}",
"port": "{{ ssh_port }}",
"identity_file": "{{ key_pair_path }}",
"instance_ids": "{{ item.instance_ids }}",
}
with_items: "{{ ec2_jobs.results }}"
register: instance_config_dict
when: server.changed | bool

- name: Convert instance config dict to a list
ansible.builtin.set_fact:
instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"
when: server.changed | bool

- name: Dump instance config
ansible.builtin.copy:
content: "{{ instance_conf | to_json | from_json | to_yaml }}"
dest: "{{ molecule_instance_config }}"
mode: 0600
when: server.changed | bool

- name: Wait for SSH
ansible.builtin.wait_for:
port: "{{ ssh_port }}"
host: "{{ item.address }}"
search_regex: SSH
delay: 10
timeout: 320
with_items: "{{ lookup('file', molecule_instance_config) | from_yaml }}"

- name: Wait for boot process to finish
ansible.builtin.pause:
minutes: 1
57 changes: 57 additions & 0 deletions molecule/aws_ec2/destroy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
- name: Destroy
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"

tasks:
- block: ## noqa: name[missing]
- name: Populate instance config
ansible.builtin.set_fact:
instance_conf: "{{ lookup('file', molecule_instance_config) | from_yaml }}"
skip_instances: false
rescue:
- name: Populate instance config when file missing
ansible.builtin.set_fact:
instance_conf: {}
skip_instances: true

- name: Destroy molecule instance(s)
amazon.aws.ec2_instance:
state: absent
instance_ids: "{{ item.instance_ids }}"
loop: "{{ instance_conf }}"
when: not skip_instances
async: 7200
poll: 0
register: server

- name: Wait for instance(s) deletion to complete
ansible.builtin.async_status:
jid: "{{ item.ansible_job_id }}"
register: ec2_jobs
until: ec2_jobs.finished
retries: 300
with_items: "{{ server.results }}"

# TODO: RESOLVE DEPRECATION WARNINGS
- name: Delete IAM role used by molecule instance(s)
amazon.aws.iam_role:
state: absent
delete_instance_profile: true
name: molecule-unit-test-ec2-role
assume_role_policy_document: "{{ lookup('file', 'templates/iam_role_ec2_trust_policy.json.j2') }}"

# Mandatory configuration for Molecule to function.

- name: Populate instance config
ansible.builtin.set_fact:
instance_conf: {}

- name: Dump instance config
ansible.builtin.copy:
content: "{{ instance_conf | to_json | from_json | to_yaml }}"
dest: "{{ molecule_instance_config }}"
mode: 0600
when: server.changed | bool