diff --git a/roles/_exit/tasks/main.yml b/roles/_exit/tasks/main.yml index 981986e0..55676bad 100644 --- a/roles/_exit/tasks/main.yml +++ b/roles/_exit/tasks/main.yml @@ -1,15 +1,6 @@ --- -# If we are operating on an AWS ASG then resume autoscaling. -- name: Enable all autoscale processes on ASG. - ansible.builtin.command: > - aws autoscaling resume-processes --auto-scaling-group-name {{ aws_asg.name }} --region {{ aws_asg.region }} - delegate_to: localhost - run_once: true - when: - - aws_asg.name is defined - - aws_asg.name | length > 0 - - name: Delete the lock file. ansible.builtin.file: path: "{{ lock_file }}" state: absent + when: deploy_operation != 'deploy' diff --git a/roles/_init/README.md b/roles/_init/README.md index a1e37f95..7338e398 100644 --- a/roles/_init/README.md +++ b/roles/_init/README.md @@ -3,14 +3,6 @@ Mandatory role that must run before any other `ce-deploy` roles when executing a These variables **must** be set in a common variables file if you do not wish to use defaults. -In order to manipulate an AWS Autoscaling Group (ASG) your `deploy` user must have an AWS CLI profile for a user with the following IAM permissions: -* `autoscaling:ResumeProcesses` -* `autoscaling:SuspendProcesses` -* `autoscaling:DescribeScalingProcessTypes` -* `autoscaling:DescribeAutoScalingGroups` - -Set the `aws_asg.name` to the machine name of your ASG in order to automatically suspend and resume autoscaling on build. - diff --git a/roles/_init/defaults/main.yml b/roles/_init/defaults/main.yml index 00c14563..807ee9c6 100644 --- a/roles/_init/defaults/main.yml +++ b/roles/_init/defaults/main.yml @@ -11,11 +11,6 @@ install_php_cachetool: true # set to false if you don't need cachetool, e.g. for ce_deploy_version: 1.x lock_file: /tmp/ce-deploy-lock provision_lock_file: /tmp/ce-provision-lock # must match _init.lock_file in ce-provision -# AWS ASG variables to allow for the suspension of autoscaling during a code deployment. -aws_asg: - name: "" # if the deploy is on an ASG put the name here - region: "eu-west-1" - suspend_processes: "Launch Terminate" # space separated string, see https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html # Application specific variables. drupal: drush_verbose_output: false diff --git a/roles/_init/tasks/drupal8.yml b/roles/_init/tasks/drupal8.yml index eb3bdae9..ad15846f 100644 --- a/roles/_init/tasks/drupal8.yml +++ b/roles/_init/tasks/drupal8.yml @@ -14,4 +14,6 @@ - name: Ensure we have a cachetool binary. ansible.builtin.import_role: name: cli/cachetool - when: install_php_cachetool + when: + - install_php_cachetool + - deploy_operation == 'deploy' diff --git a/roles/_init/tasks/main.yml b/roles/_init/tasks/main.yml index 34340018..b0c47a66 100644 --- a/roles/_init/tasks/main.yml +++ b/roles/_init/tasks/main.yml @@ -23,6 +23,7 @@ path: "{{ lock_file }}" state: touch mode: 0644 + when: deploy_operation == 'deploy' # Ensure default values for common variables. - name: Define deploy user. @@ -70,6 +71,9 @@ - cache_clear_opcache.cachetool_bin | length > 0 - name: Manipulate variables for SquashFS builds. + when: + - deploy_code.mount_type is defined + - deploy_code.mount_type == "squashfs" block: - name: Define image builds base path. ansible.builtin.set_fact: @@ -97,9 +101,6 @@ ansible.builtin.file: path: "{{ build_base_path }}" state: directory - when: - - deploy_code.mount_type is defined - - deploy_code.mount_type == "squashfs" - name: Set the previous deploy's path for later use where we need to manipulate the live site. ansible.builtin.set_fact: @@ -140,21 +141,10 @@ ansible.builtin.stat: path: "{{ role_path }}/tasks/{{ project_type }}.yml" register: _project_type_task_result - delegate_to: "localhost" + delegate_to: localhost # Project specific init tasks. - name: Include project init tasks. ansible.builtin.include_tasks: "{{ project_type }}.yml" when: - _project_type_task_result.stat.exists - -# If we are operating on an AWS ASG then pause autoscaling. -# @TODO - the autoscaling_group module can do this - https://docs.ansible.com/ansible/latest/collections/amazon/aws/autoscaling_group_module.html -- name: Disable autoscale processes on ASG. - ansible.builtin.command: > - aws autoscaling suspend-processes --auto-scaling-group-name {{ aws_asg.name }} --scaling-processes {{ aws_asg.suspend_processes }} --region {{ aws_asg.region }} - delegate_to: localhost - run_once: true - when: - - aws_asg.name is defined - - aws_asg.name | length > 0 diff --git a/roles/asg_management/README.md b/roles/asg_management/README.md new file mode 100644 index 00000000..10dc1a79 --- /dev/null +++ b/roles/asg_management/README.md @@ -0,0 +1,64 @@ +# ASG Management +This role should be called in a separate playbook, e.g. a separate command in CI, to the rest of the build with the host set as `localhost` and not the target ASG group. The available hosts in the ASG group may change after it has run, so Ansible needs to interogate the ASG host group after this play has run and before building. This cannot be done unless Ansible stops and starts again. + +In order to manipulate an AWS Autoscaling Group (ASG) your `deploy` user must have an AWS CLI profile for a user with the following IAM permissions: +* `autoscaling:ResumeProcesses` +* `autoscaling:SuspendProcesses` +* `autoscaling:DescribeScalingProcessTypes` +* `autoscaling:DescribeAutoScalingGroups` + +Set the `asg_management.name` to the machine name of your ASG in order to automatically suspend and resume autoscaling on build. + +## Recommended playbook setup +To use this role the recommended approach is two different playbooks and separate Ansible commands. Don't forget to add the `asg_management` variables to your variables file as well. Below you will find a GitLab CI example and suggested variables. + +### `.gitlab-ci.yml` + +```yaml +--- +stages: + - deploy + +deploy_dev: + stage: deploy + script: + - /bin/sh /home/deploy/ce-deploy/scripts/deploy.sh --workspace "$CI_PROJECT_DIR" --playbook deploy/asg.yml --ansible-extra-vars "{\"build_type\":\"$BUILD_TYPE\",\"install_php_cachetool\":false}" --build-number ${CI_PIPELINE_IID} --build-id acme-dev --boto-profile acme + - /bin/sh /home/deploy/ce-deploy/scripts/build.sh --workspace "$CI_PROJECT_DIR" --playbook deploy/deploy-dev.yml --build-number ${CI_PIPELINE_IID} --build-id acme-dev --boto-profile acme + - /bin/sh /home/deploy/ce-deploy/scripts/cleanup.sh --workspace "$CI_PROJECT_DIR" --playbook deploy/asg.yml --ansible-extra-vars "{\"build_type\":\"$BUILD_TYPE\",\"install_php_cachetool\":false}" --build-number ${CI_PIPELINE_IID} --build-id acme-dev --boto-profile acme + rules: + - if: '$CI_PIPELINE_SOURCE != "web" && $CI_COMMIT_BRANCH == "dev"' + - if: '$CI_PIPELINE_SOURCE == "web" && $SYNC == "no" && $BUILD_TYPE == "dev"' +``` + +### `asg.yml` + +```yaml +--- +- hosts: localhost + vars_files: + - vars/common.yml + - "vars/{{ build_type }}.yml" + roles: + - asg_management +``` + +### `deploy-dev.yml` + +```yaml +--- +- hosts: _ce_www_acme_codeenigma_net # ASG group name from EC2 discovery + vars_files: + - vars/common.yml + - vars/dev.yml + roles: + - _meta/deploy-drupal8 +``` + +### Process explained +The example is a single development environment Drupal build. Your CI will call `asg.yml` with the `deploy.sh` script which will *only* run the `deploy` operation, therefore it will try to suspend ASG processes and wait until the ASG has settled down before continuing. After that we call a normal Drupal build, `deploy-dev.yml`, same as you would if it were a standalone server or a static cluster. Finally, we call `asg.yml` again but this time with the `cleanup.sh` script which will *only* run the `cleanup` operation, therefore it will re-enable the suspended ASG processes. + + + + + + diff --git a/roles/asg_management/defaults/main.yml b/roles/asg_management/defaults/main.yml new file mode 100644 index 00000000..67371acd --- /dev/null +++ b/roles/asg_management/defaults/main.yml @@ -0,0 +1,8 @@ +--- +# AWS ASG variables to allow for the suspension of autoscaling during a code deployment. +asg_management: + name: "" # if the deploy is on an ASG put the name here + #profile: "example" # optional, the boto profile name to use if not the system default + region: "eu-west-1" + suspend_processes: "Launch Terminate HealthCheck" # space separated string, see https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html + pause: 5 # how long in seconds to wait before polling the AWS API again for instance statuses diff --git a/roles/asg_management/tasks/asg_target_health.yml b/roles/asg_management/tasks/asg_target_health.yml new file mode 100644 index 00000000..78f3b21b --- /dev/null +++ b/roles/asg_management/tasks/asg_target_health.yml @@ -0,0 +1,52 @@ +--- +- name: Pause so as to not hammer the AWS API. + ansible.builtin.pause: + seconds: "{{ asg_management.pause }}" + +- name: Empty the target list. + ansible.builtin.set_fact: + _target_health_list: [] + +- name: Fetch target instance data. + when: + - _target_group.target_groups | length > 0 + - asg_management.profile is not defined + block: + - name: Fetch raw target health data with the AWS CLI. + ansible.builtin.command: >- + aws elbv2 describe-target-health + --target-group-arn {{ _target_group.target_groups[0].target_group_arn }} + --region {{ asg_management.region }} + register: _target_health + + - name: Convert raw JSON output from CLI to YAML for ease of use. + ansible.builtin.set_fact: + _target_health_yaml: "{{ _target_health.stdout | from_json }}" + +- name: Fetch target instance data with a boto profile. + when: + - _target_group.target_groups | length > 0 + - asg_management.profile is defined + - asg_management.profile | length > 0 + block: + - name: Fetch raw target health data with the AWS CLI via a specified boto profile. + ansible.builtin.command: >- + aws elbv2 describe-target-health + --target-group-arn {{ _target_group.target_groups[0].target_group_arn }} + --region {{ asg_management.region }} + --profile {{ asg_management.profile }} + register: _target_health_boto + + - name: Convert raw JSON output from CLI to YAML for ease of use. + ansible.builtin.set_fact: + _target_health_yaml: "{{ _target_health_boto.stdout | from_json }}" + +- name: Make a list of all the 'healthy' instances. + ansible.builtin.set_fact: + _target_health_list: "{{ _target_health_list + [ item.TargetHealth.State ] }}" + with_items: "{{ _target_health_yaml.TargetHealthDescriptions }}" + when: item.TargetHealth.State == 'healthy' + +- name: Run again if the number of 'healthy' instances does not match the total. + ansible.builtin.include_tasks: "./asg_target_health.yml" + when: (_target_health_yaml.TargetHealthDescriptions | length) != (_target_health_list | length) diff --git a/roles/asg_management/tasks/main.yml b/roles/asg_management/tasks/main.yml new file mode 100644 index 00000000..3d034470 --- /dev/null +++ b/roles/asg_management/tasks/main.yml @@ -0,0 +1,65 @@ +--- +- name: Suspend autoscale processes on ASG. + when: + - asg_management.name | length > 0 + - deploy_operation == 'deploy' + delegate_to: localhost + run_once: true + block: + # @TODO - the autoscaling_group module can do this - https://docs.ansible.com/ansible/latest/collections/amazon/aws/autoscaling_group_module.html + - name: Suspend autoscale processes on ASG via a specified boto profile. + ansible.builtin.command: >- + aws autoscaling suspend-processes + --auto-scaling-group-name {{ asg_management.name }} + --scaling-processes {{ asg_management.suspend_processes }} + --region {{ asg_management.region }} + --profile {{ asg_management.profile }} + when: + - asg_management.profile is defined + - asg_management.profile | length > 0 + + - name: Suspend autoscale processes on ASG. + ansible.builtin.command: >- + aws autoscaling suspend-processes + --auto-scaling-group-name {{ asg_management.name }} + --scaling-processes {{ asg_management.suspend_processes }} + --region {{ asg_management.region }} + when: + - asg_management.profile is not defined + + - name: Gather information about target group. + community.aws.elb_target_group_info: + region: "{{ asg_management.region }}" + profile: "{{ asg_management.profile | default(omit) }}" + names: + - "{{ asg_management.name }}" + register: _target_group + + - name: Loop over target instances until they are all 'healthy'. + ansible.builtin.include_tasks: asg_target_health.yml + +- name: Resume all autoscale processes on ASG. + ansible.builtin.command: >- + aws autoscaling resume-processes + --auto-scaling-group-name {{ asg_management.name }} + --region {{ asg_management.region }} + delegate_to: localhost + run_once: true + when: + - asg_management.name | length > 0 + - asg_management.profile is not defined + - deploy_operation != 'deploy' + +- name: Resume all autoscale processes on ASG via a specified boto profile. + ansible.builtin.command: >- + aws autoscaling resume-processes + --auto-scaling-group-name {{ asg_management.name }} + --region {{ asg_management.region }} + --profile {{ asg_management.profile }} + delegate_to: localhost + run_once: true + when: + - asg_management.name | length > 0 + - asg_management.profile is defined + - asg_management.profile | length > 0 + - deploy_operation != 'deploy'