Repository: ansible/ansible

Issue:
AWS Application Load Balancer module (elb_application_lb) update fails for Listener actions that contain anything other than 'forward'
<!---
Verify first that your issue/request is not already reported on GitHub.
THIS FORM WILL BE READ BY A MACHINE, COMPLETE ALL SECTIONS AS DESCRIBED.
Also test if the latest release, and devel branch are affected too.
ALWAYS add information AFTER (OUTSIDE) these html comments.
Otherwise it may end up being automatically closed by our bot. -->

##### SUMMARY
<!--- Explain the problem briefly -->
AWS recently launched a new ALB (Application Load Balancer) featured that allows authentication to be performed by the load balancer using Cognito. This authentication is configured by adding an authentication action to a listener rule. ALB resources with this new authentication feature can be sucessfully created by ansible, however subsequent runs against the resource fail.

##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
<!--- Insert, BELOW THIS COMMENT, the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path-->
elb_application_lb

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.5
  config file = /home/jevers/misc_dev/ansible_alb_test/ansible.cfg
  configured module search path = ['/home/jevers/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/jevers/.local/lib/python3.6/site-packages/ansible
  executable location = /home/jevers/.local/bin/ansible
  python version = 3.6.5 (default, Mar 29 2018, 18:20:46) [GCC 8.0.1 20180317 (Red Hat 8.0.1-0.19)]
```

##### CONFIGURATION
<!--- If using Ansible 2.4 or above, paste, BELOW THIS COMMENT, the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).-->
ANSIBLE_FORCE_COLOR(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = True
ANSIBLE_NOCOWS(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = True
ANSIBLE_PIPELINING(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = True
ANSIBLE_SSH_ARGS(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = -o ForwardAgent=yes -o PreferredAuthentications=keyboard-interactive,gssapi-with-mic,gssapi-keyex,hostbased,pub>
DEFAULT_FORKS(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = 100
DEFAULT_GATHERING(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = smart
DEFAULT_HOST_LIST(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = ['/home/jevers/misc_dev/ansible_alb_test/inventory']
DEFAULT_LOG_PATH(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = /home/jevers/misc_dev/ansible_alb_test/log/ansible.log
DEFAULT_MANAGED_STR(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = Ansible managed: DO NOT EDIT
DEFAULT_MODULE_NAME(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = shell
DEFAULT_REMOTE_USER(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = pi
DEFAULT_SCP_IF_SSH(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = True
DEFAULT_TIMEOUT(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = 15
DEPRECATION_WARNINGS(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = True
HOST_KEY_CHECKING(/home/jevers/misc_dev/ansible_alb_test/ansible.cfg) = False

##### OS / ENVIRONMENT
<!--- Mention, BELOW THIS COMMENT, the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.-->
Running Ansible on Fedora 28 (Linux) , managing AWS resources

##### STEPS TO REPRODUCE
<!--- For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used. -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
---
- hosts: localhost
  connection: local
  gather_facts: false
  vars:
      role_account: "{{ACCTID}}"
      role_name: "{{ROLENAME}}"
      resource_state: present
      resource_prefix: alb-auth-test
      vpc_id: "{{VPC}}"
      subnets:
          - "{{SUBNET1}}"
          - "{{SUBNET2}}"
      AuthenticateCognitoConfig: 
          UserPoolArn: "{{USER_POOL_ARN}}"
          UserPoolClientId: "{{USER_POOL_APP_CLIENT_ID}}"
          UserPoolDomain: "{{USER_POOL_DOMAIN}}"
          SessionCookieName: "AWSELBAuthSessionCookie"
          Scope: "openid"
          SessionTimeout: 604800
          OnUnauthenticatedRequest: "authenticate"
  tasks:
      - name: assume role in target account
        sts_assume_role:
            role_arn: "arn:aws:iam::{{role_account}}:role/{{role_name}}"
            role_session_name: "ansible_workstation"
        register: assumed_role

      - name: create alb security group
        ec2_group:
            aws_access_key: "{{assumed_role.sts_creds.access_key}}"
            aws_secret_key: "{{assumed_role.sts_creds.secret_key}}"
            security_token: "{{assumed_role.sts_creds.session_token}}"

            name: "{{resource_prefix}}-sg"
            state: "{{resource_state}}"
            description: "security group for {{resource_prefix}}-alb"
            vpc_id: "{{vpc_id}}"
            rules:
                - proto: tcp
                  from_port: 80
                  to_port: 80
                  cidr_ip: 10.10.3.0/24
                - proto: tcp
                  from_port: 443
                  to_port: 443
                  cidr_ip: 10.10.3.0/24
        register: alb_sg

      - name: create target group
        elb_target_group:
            aws_access_key: "{{assumed_role.sts_creds.access_key}}"
            aws_secret_key: "{{assumed_role.sts_creds.secret_key}}"
            security_token: "{{assumed_role.sts_creds.session_token}}"

            name: "{{resource_prefix}}-tg"
            state: "{{resource_state}}"
            protocol: http
            port: 80
            vpc_id: "{{vpc_id}}"
        register: app_tg
          
      - name: create alb
        elb_application_lb:
            aws_access_key: "{{assumed_role.sts_creds.access_key}}"
            aws_secret_key: "{{assumed_role.sts_creds.secret_key}}"
            security_token: "{{assumed_role.sts_creds.session_token}}"

            name: "{{resource_prefix}}-alb"
            state: present

            security_groups:
                - "{{alb_sg.group_id}}"
            subnets: "{{subnets}}"
            scheme: internal
            listeners:
                - Protocol: HTTPS
                  Port: 443
                  DefaultActions:
                      - Type: forward
                        TargetGroupArn: "{{app_tg.target_group_arn }}"
                  Certificates:
                      - CertificateArn: "{{acm_cert_arn}}"
                  SslPolicy: ELBSecurityPolicy-2016-08
                  Rules:
                      - Conditions:
                          - Field: path-pattern
                            Values:
                                - "*"
                        Priority: '1'
                        Actions:
                            - Type: forward
                              TargetGroupArn: "{{app_tg.target_group_arn }}"
                              Order: 2
                            - Type: authenticate-cognito
                              AuthenticateCognitoConfig: "{{AuthenticateCognitoConfig}}"
                              Order: 1
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
1. On the first playbook run, the resource is created
2. the Playbook can be run against the new ALB resource without failure

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
The first Playbook runs sucessfully, and the ALB resource is created.
The playbook is run Again and the 'create alb' task fails with a 'KeyError: 'TargetGroupArn'' Exception
<!--- Paste verbatim command output between quotes below -->
```
TASK [create alb] *********************************************************************************************************************************************************************
task path: /home/jevers/misc_dev/ansible_alb_test/alb.yml:63
Using module file /home/jevers/.local/lib/python3.6/site-packages/ansible/modules/cloud/amazon/elb_application_lb.py
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: jevers
<localhost> EXEC /bin/sh -c 'python3 && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py", line 1028, in <module>
    main()
  File "/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py", line 1023, in main
    create_or_update_elb(connection, connection_ec2, module)
  File "/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py", line 915, in create_or_update_elb
    listener_changed = create_or_update_elb_listeners(connection, module, elb)
  File "/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py", line 762, in create_or_update_elb_listeners
    rules_to_add, rules_to_modify, rules_to_delete = compare_rules(connection, module, current_listeners, listener)
  File "/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py", line 693, in compare_rules
    modified_rule = compare_rule(current_rule, new_rule)
  File "/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py", line 600, in compare_rule
    if current_rule['Actions'][0]['TargetGroupArn'] != new_rule['Actions'][0]['TargetGroupArn']:
KeyError: 'TargetGroupArn'

fatal: [localhost]: FAILED! => {
    "changed": false,
    "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py\", line 1028, in <module>\n    main()\n  File \"/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py\", line 1023, in main\n    create_or_update_elb(connection, connection_ec2, module)\n  File \"/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py\", line 915, in create_or_update_elb\n    listener_changed = create_or_update_elb_listeners(connection, module, elb)\n  File \"/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py\", line 762, in create_or_update_elb_listeners\n    rules_to_add, rules_to_modify, rules_to_delete = compare_rules(connection, module, current_listeners, listener)\n  File \"/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py\", line 693, in compare_rules\n    modified_rule = compare_rule(current_rule, new_rule)\n  File \"/tmp/ansible_7qen__0n/ansible_module_elb_application_lb.py\", line 600, in compare_rule\n    if current_rule['Actions'][0]['TargetGroupArn'] != new_rule['Actions'][0]['TargetGroupArn']:\nKeyError: 'TargetGroupArn'\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE",
    "rc": 1
}
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/cloud/amazon/elb_application_lb.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/amazon/elb_application_lb.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @wimnat
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
It is not clearly documented but you can see it in the examples:

`# Required. Only 'forward' is accepted at this time`

And in the code... https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/aws/elbv2.py#L661

Obviously that's not true any more but the elbv2 code will need updating to manage the new modes.


If 'forward' actions are currently the only supported action type, shouldn't the rule comparison logic check that the rule is a forward before trying to extract the target group arn from the dictionary? That way a warning message stating that the rule isn't a supported type can be displayed instead of a KeyError exception.
Yeh sounds good. I didn't know AWS were going to bring in new actions when
I wrote it but I'm open to PRs.

On Tue., 26 Jun. 2018, 02:29 Jesse Evers, <notifications@github.com> wrote:

> If 'forward' actions are currently the only supported action type,
> shouldn't the rule comparison logic check that the rule is a forward before
> trying to extract the target group arn from the dictionary? That way a
> warning message stating that the rule isn't a supported type can be
> displayed instead of a KeyError exception.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/ansible/ansible/issues/41861#issuecomment-400013351>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AHFORoBbgnGn4eLvdOflUjTZG8JteYvIks5uAQ_ggaJpZM4U0YB->
> .
>


Resolving patch:
diff --git a/lib/ansible/module_utils/aws/elbv2.py b/lib/ansible/module_utils/aws/elbv2.py
--- a/lib/ansible/module_utils/aws/elbv2.py
+++ b/lib/ansible/module_utils/aws/elbv2.py
@@ -466,13 +466,20 @@ def _ensure_listeners_default_action_has_arn(self, listeners):
         if not listeners:
             listeners = []
 
+        fixed_listeners = []
         for listener in listeners:
-            if 'TargetGroupName' in listener['DefaultActions'][0]:
-                listener['DefaultActions'][0]['TargetGroupArn'] = convert_tg_name_to_arn(self.connection, self.module,
-                                                                                         listener['DefaultActions'][0]['TargetGroupName'])
-                del listener['DefaultActions'][0]['TargetGroupName']
-
-        return listeners
+            fixed_actions = []
+            for action in listener['DefaultActions']:
+                if 'TargetGroupName' in action:
+                    action['TargetGroupArn'] = convert_tg_name_to_arn(self.connection,
+                                                                      self.module,
+                                                                      action['TargetGroupName'])
+                    del action['TargetGroupName']
+                fixed_actions.append(action)
+            listener['DefaultActions'] = fixed_actions
+            fixed_listeners.append(listener)
+
+        return fixed_listeners
 
     def compare_listeners(self):
         """
@@ -540,12 +547,47 @@ def _compare_listener(self, current_listener, new_listener):
             modified_listener['Certificates'][0]['CertificateArn'] = new_listener['Certificates'][0]['CertificateArn']
 
         # Default action
-        #   We wont worry about the Action Type because it is always 'forward'
-        if current_listener['DefaultActions'][0]['TargetGroupArn'] != new_listener['DefaultActions'][0]['TargetGroupArn']:
-            modified_listener['DefaultActions'] = []
-            modified_listener['DefaultActions'].append({})
-            modified_listener['DefaultActions'][0]['TargetGroupArn'] = new_listener['DefaultActions'][0]['TargetGroupArn']
-            modified_listener['DefaultActions'][0]['Type'] = 'forward'
+
+        # Check proper rule format on current listener
+        if len(current_listener['DefaultActions']) > 1:
+            for action in current_listener['DefaultActions']:
+                if 'Order' not in action:
+                    self.module.fail_json(msg="'Order' key not found in actions. "
+                                              "installed version of botocore does not support "
+                                              "multiple actions, please upgrade botocore to version "
+                                              "1.10.30 or higher")
+
+        # If the lengths of the actions are the same, we'll have to verify that the
+        # contents of those actions are the same
+        if len(current_listener['DefaultActions']) == len(new_listener['DefaultActions']):
+            # if actions have just one element, compare the contents and then update if
+            # they're different
+            if len(current_listener['DefaultActions']) == 1 and len(new_listener['DefaultActions']) == 1:
+                if current_listener['DefaultActions'] != new_listener['DefaultActions']:
+                    modified_listener['DefaultActions'] = new_listener['DefaultActions']
+            # if actions have multiple elements, we'll have to order them first before comparing.
+            # multiple actions will have an 'Order' key for this purpose
+            else:
+                current_actions_sorted = sorted(current_listener['DefaultActions'], key=lambda x: x['Order'])
+                new_actions_sorted = sorted(new_listener['DefaultActions'], key=lambda x: x['Order'])
+
+                # the AWS api won't return the client secret, so we'll have to remove it
+                # or the module will always see the new and current actions as different
+                # and try to apply the same config
+                new_actions_sorted_no_secret = []
+                for action in new_actions_sorted:
+                    # the secret is currently only defined in the oidc config
+                    if action['Type'] == 'authenticate-oidc':
+                        action['AuthenticateOidcConfig'].pop('ClientSecret')
+                        new_actions_sorted_no_secret.append(action)
+                    else:
+                        new_actions_sorted_no_secret.append(action)
+
+                if current_actions_sorted != new_actions_sorted_no_secret:
+                    modified_listener['DefaultActions'] = new_listener['DefaultActions']
+        # If the action lengths are different, then replace with the new actions
+        else:
+            modified_listener['DefaultActions'] = new_listener['DefaultActions']
 
         if modified_listener:
             return modified_listener
@@ -577,7 +619,12 @@ def add(self):
                 self.listener.pop('Rules')
             AWSRetry.jittered_backoff()(self.connection.create_listener)(LoadBalancerArn=self.elb_arn, **self.listener)
         except (BotoCoreError, ClientError) as e:
-            self.module.fail_json_aws(e)
+            if '"Order", must be one of: Type, TargetGroupArn' in str(e):
+                self.module.fail_json(msg="installed version of botocore does not support "
+                                          "multiple actions, please upgrade botocore to version "
+                                          "1.10.30 or higher")
+            else:
+                self.module.fail_json_aws(e)
 
     def modify(self):
 
@@ -587,7 +634,12 @@ def modify(self):
                 self.listener.pop('Rules')
             AWSRetry.jittered_backoff()(self.connection.modify_listener)(**self.listener)
         except (BotoCoreError, ClientError) as e:
-            self.module.fail_json_aws(e)
+            if '"Order", must be one of: Type, TargetGroupArn' in str(e):
+                self.module.fail_json(msg="installed version of botocore does not support "
+                                          "multiple actions, please upgrade botocore to version "
+                                          "1.10.30 or higher")
+            else:
+                self.module.fail_json_aws(e)
 
     def delete(self):
 
@@ -629,12 +681,18 @@ def _ensure_rules_action_has_arn(self, rules):
         :return: the same list of dicts ensuring that each rule Actions dict has TargetGroupArn key. If a TargetGroupName key exists, it is removed.
         """
 
+        fixed_rules = []
         for rule in rules:
-            if 'TargetGroupName' in rule['Actions'][0]:
-                rule['Actions'][0]['TargetGroupArn'] = convert_tg_name_to_arn(self.connection, self.module, rule['Actions'][0]['TargetGroupName'])
-                del rule['Actions'][0]['TargetGroupName']
+            fixed_actions = []
+            for action in rule['Actions']:
+                if 'TargetGroupName' in action:
+                    action['TargetGroupArn'] = convert_tg_name_to_arn(self.connection, self.module, action['TargetGroupName'])
+                    del action['TargetGroupName']
+                fixed_actions.append(action)
+            rule['Actions'] = fixed_actions
+            fixed_rules.append(rule)
 
-        return rules
+        return fixed_rules
 
     def _get_elb_listener_rules(self):
 
@@ -654,7 +712,12 @@ def _compare_condition(self, current_conditions, condition):
         condition_found = False
 
         for current_condition in current_conditions:
-            if current_condition['Field'] == condition['Field'] and current_condition['Values'][0] == condition['Values'][0]:
+            if current_condition.get('SourceIpConfig'):
+                if (current_condition['Field'] == condition['Field'] and
+                        current_condition['SourceIpConfig']['Values'][0] == condition['SourceIpConfig']['Values'][0]):
+                    condition_found = True
+                    break
+            elif current_condition['Field'] == condition['Field'] and current_condition['Values'][0] == condition['Values'][0]:
                 condition_found = True
                 break
 
@@ -669,16 +732,57 @@ def _compare_rule(self, current_rule, new_rule):
         modified_rule = {}
 
         # Priority
-        if current_rule['Priority'] != new_rule['Priority']:
+        if int(current_rule['Priority']) != new_rule['Priority']:
             modified_rule['Priority'] = new_rule['Priority']
 
         # Actions
-        #   We wont worry about the Action Type because it is always 'forward'
-        if current_rule['Actions'][0]['TargetGroupArn'] != new_rule['Actions'][0]['TargetGroupArn']:
-            modified_rule['Actions'] = []
-            modified_rule['Actions'].append({})
-            modified_rule['Actions'][0]['TargetGroupArn'] = new_rule['Actions'][0]['TargetGroupArn']
-            modified_rule['Actions'][0]['Type'] = 'forward'
+
+        # Check proper rule format on current listener
+        if len(current_rule['Actions']) > 1:
+            for action in current_rule['Actions']:
+                if 'Order' not in action:
+                    self.module.fail_json(msg="'Order' key not found in actions. "
+                                              "installed version of botocore does not support "
+                                              "multiple actions, please upgrade botocore to version "
+                                              "1.10.30 or higher")
+
+        # If the lengths of the actions are the same, we'll have to verify that the
+        # contents of those actions are the same
+        if len(current_rule['Actions']) == len(new_rule['Actions']):
+            # if actions have just one element, compare the contents and then update if
+            # they're different
+            if len(current_rule['Actions']) == 1 and len(new_rule['Actions']) == 1:
+                if current_rule['Actions'] != new_rule['Actions']:
+                    modified_rule['Actions'] = new_rule['Actions']
+                    print("modified_rule:")
+                    print(new_rule['Actions'])
+            # if actions have multiple elements, we'll have to order them first before comparing.
+            # multiple actions will have an 'Order' key for this purpose
+            else:
+                current_actions_sorted = sorted(current_rule['Actions'], key=lambda x: x['Order'])
+                new_actions_sorted = sorted(new_rule['Actions'], key=lambda x: x['Order'])
+
+                # the AWS api won't return the client secret, so we'll have to remove it
+                # or the module will always see the new and current actions as different
+                # and try to apply the same config
+                new_actions_sorted_no_secret = []
+                for action in new_actions_sorted:
+                    # the secret is currently only defined in the oidc config
+                    if action['Type'] == 'authenticate-oidc':
+                        action['AuthenticateOidcConfig'].pop('ClientSecret')
+                        new_actions_sorted_no_secret.append(action)
+                    else:
+                        new_actions_sorted_no_secret.append(action)
+
+                if current_actions_sorted != new_actions_sorted_no_secret:
+                    modified_rule['Actions'] = new_rule['Actions']
+                    print("modified_rule:")
+                    print(new_rule['Actions'])
+        # If the action lengths are different, then replace with the new actions
+        else:
+            modified_rule['Actions'] = new_rule['Actions']
+            print("modified_rule:")
+            print(new_rule['Actions'])
 
         # Conditions
         modified_conditions = []
@@ -746,7 +850,12 @@ def create(self):
             self.rule['Priority'] = int(self.rule['Priority'])
             AWSRetry.jittered_backoff()(self.connection.create_rule)(**self.rule)
         except (BotoCoreError, ClientError) as e:
-            self.module.fail_json_aws(e)
+            if '"Order", must be one of: Type, TargetGroupArn' in str(e):
+                self.module.fail_json(msg="installed version of botocore does not support "
+                                          "multiple actions, please upgrade botocore to version "
+                                          "1.10.30 or higher")
+            else:
+                self.module.fail_json_aws(e)
 
         self.changed = True
 
@@ -761,7 +870,12 @@ def modify(self):
             del self.rule['Priority']
             AWSRetry.jittered_backoff()(self.connection.modify_rule)(**self.rule)
         except (BotoCoreError, ClientError) as e:
-            self.module.fail_json_aws(e)
+            if '"Order", must be one of: Type, TargetGroupArn' in str(e):
+                self.module.fail_json(msg="installed version of botocore does not support "
+                                          "multiple actions, please upgrade botocore to version "
+                                          "1.10.30 or higher")
+            else:
+                self.module.fail_json_aws(e)
 
         self.changed = True
 
diff --git a/lib/ansible/modules/cloud/amazon/elb_application_lb.py b/lib/ansible/modules/cloud/amazon/elb_application_lb.py
--- a/lib/ansible/modules/cloud/amazon/elb_application_lb.py
+++ b/lib/ansible/modules/cloud/amazon/elb_application_lb.py
@@ -157,7 +157,7 @@
         Certificates: # The ARN of the certificate (only one certficate ARN should be provided)
           - CertificateArn: arn:aws:iam::12345678987:server-certificate/test.domain.com
         DefaultActions:
-          - Type: forward # Required. Only 'forward' is accepted at this time
+          - Type: forward # Required.
             TargetGroupName: # Required. The name of the target group
     state: present
 
@@ -181,7 +181,7 @@
         Certificates: # The ARN of the certificate (only one certficate ARN should be provided)
           - CertificateArn: arn:aws:iam::12345678987:server-certificate/test.domain.com
         DefaultActions:
-          - Type: forward # Required. Only 'forward' is accepted at this time
+          - Type: forward # Required.
             TargetGroupName: # Required. The name of the target group
     state: present
 
@@ -212,6 +212,31 @@
             Actions:
               - TargetGroupName: test-target-group
                 Type: forward
+          - Conditions:
+              - Field: path-pattern
+                Values:
+                  - "/redirect-path/*"
+            Priority: '2'
+            Actions:
+              - Type: redirect
+                RedirectConfig:
+                  Host: "#{host}"
+                  Path: "/example/redir" # or /#{path}
+                  Port: "#{port}"
+                  Protocol: "#{protocol}"
+                  Query: "#{query}"
+                  StatusCode: "HTTP_302" # or HTTP_301
+          - Conditions:
+              - Field: path-pattern
+                Values:
+                  - "/fixed-response-path/"
+            Priority: '3'
+            Actions:
+              - Type: fixed-response
+                FixedResponseConfig:
+                  ContentType: "text/plain"
+                  MessageBody: "This is the page you're looking for"
+                  StatusCode: "200"
     state: present
 
 # Remove an ELB


Test patch:
diff --git a/hacking/aws_config/testing_policies/compute-policy.json b/hacking/aws_config/testing_policies/compute-policy.json
--- a/hacking/aws_config/testing_policies/compute-policy.json
+++ b/hacking/aws_config/testing_policies/compute-policy.json
@@ -132,10 +132,12 @@
                 "elasticloadbalancing:CreateListener",
                 "elasticloadbalancing:CreateLoadBalancer",
                 "elasticloadbalancing:CreateLoadBalancerListeners",
+                "elasticloadbalancing:CreateRule",
                 "elasticloadbalancing:CreateTargetGroup",
                 "elasticloadbalancing:DeleteListener",
                 "elasticloadbalancing:DeleteLoadBalancer",
                 "elasticloadbalancing:DeleteLoadBalancerListeners",
+                "elasticloadbalancing:DeleteRule",
                 "elasticloadbalancing:DeleteTargetGroup",
                 "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
                 "elasticloadbalancing:DescribeInstanceHealth",
@@ -143,7 +145,9 @@
                 "elasticloadbalancing:DescribeTags",
                 "elasticloadbalancing:DisableAvailabilityZonesForLoadBalancer",
                 "elasticloadbalancing:EnableAvailabilityZonesForLoadBalancer",
+                "elasticloadbalancing:ModifyListener",
                 "elasticloadbalancing:ModifyLoadBalancerAttributes",
+                "elasticloadbalancing:ModifyRule",
                 "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
                 "elasticloadbalancing:RemoveTags"
             ],
diff --git a/hacking/aws_config/testing_policies/security-policy.json b/hacking/aws_config/testing_policies/security-policy.json
--- a/hacking/aws_config/testing_policies/security-policy.json
+++ b/hacking/aws_config/testing_policies/security-policy.json
@@ -122,7 +122,10 @@
             "Effect": "Allow",
             "Action": [
                 "iam:ListServerCertificates",
-                "iam:UploadServerCertificate"
+                "iam:UploadServerCertificate",
+                "iam:UpdateServerCertificate",
+                "iam:DeleteServerCertificate",
+                "iam:GetServerCertificate"
             ],
             "Resource": "*"
         }
diff --git a/test/integration/targets/elb_application_lb/meta/main.yml b/test/integration/targets/elb_application_lb/meta/main.yml
--- a/test/integration/targets/elb_application_lb/meta/main.yml
+++ b/test/integration/targets/elb_application_lb/meta/main.yml
@@ -1,3 +1,2 @@
 dependencies:
-  - prepare_tests
-  - setup_ec2
+  - setup_remote_tmp_dir
diff --git a/test/integration/targets/elb_application_lb/tasks/full_test.yml b/test/integration/targets/elb_application_lb/tasks/full_test.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/elb_application_lb/tasks/full_test.yml
@@ -0,0 +1,259 @@
+- block:
+
+  - name: set connection information for all tasks
+    set_fact:
+      aws_connection_info: &aws_connection_info
+        aws_access_key: "{{ aws_access_key }}"
+        aws_secret_key: "{{ aws_secret_key }}"
+        security_token: "{{ security_token }}"
+        region: "{{ aws_region }}"
+      awscli_connection_info: &awscli_connection_info
+        AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
+        AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
+        AWS_SESSION_TOKEN: "{{ security_token }}"
+        AWS_DEFAULT_REGION: "{{ aws_region }}"
+    no_log: yes
+
+  - name: create VPC
+    ec2_vpc_net:
+      cidr_block: 10.228.228.0/22
+      name: "{{ resource_prefix }}_vpc"
+      state: present
+      <<: *aws_connection_info
+    register: vpc
+
+  - name: create internet gateway
+    ec2_vpc_igw:
+      vpc_id: "{{ vpc.vpc.id }}"
+      state: present
+      tags:
+        Name: "{{ resource_prefix }}"
+      <<: *aws_connection_info
+    register: igw
+
+  - name: create public subnet
+    ec2_vpc_subnet:
+      cidr: "{{ item.cidr }}"
+      az: "{{ aws_region}}{{ item.az }}"
+      vpc_id: "{{ vpc.vpc.id }}"
+      state: present
+      tags:
+        Public: "{{ item.public|string }}"
+        Name: "{{ item.public|ternary('public', 'private') }}-{{ item.az }}"
+      <<: *aws_connection_info
+    with_items:
+      - cidr: 10.228.228.0/24
+        az: "a"
+        public: "True"
+      - cidr: 10.228.229.0/24
+        az: "b"
+        public: "True"
+      - cidr: 10.228.230.0/24
+        az: "a"
+        public: "False"
+      - cidr: 10.228.231.0/24
+        az: "b"
+        public: "False"
+    register: subnets
+
+  - ec2_vpc_subnet_info:
+      filters:
+        vpc-id: "{{ vpc.vpc.id }}"
+      <<: *aws_connection_info
+    register: vpc_subnets
+
+  - name: create list of subnet ids
+    set_fact:
+      alb_subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `True`].id') }}"
+      private_subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public != `True`].id') }}"
+
+  - name: create a route table
+    ec2_vpc_route_table:
+      vpc_id: "{{ vpc.vpc.id }}"
+      <<: *aws_connection_info
+      tags:
+        Name: igw-route
+        Created: "{{ resource_prefix }}"
+      subnets: "{{ alb_subnets + private_subnets }}"
+      routes:
+        - dest: 0.0.0.0/0
+          gateway_id: "{{ igw.gateway_id }}"
+    register: route_table
+
+  - ec2_group:
+      name: "{{ resource_prefix }}"
+      description: "security group for Ansible ALB integration tests"
+      state: present
+      vpc_id: "{{ vpc.vpc.id }}"
+      rules:
+        - proto: tcp
+          from_port: 1
+          to_port: 65535
+          cidr_ip: 0.0.0.0/0
+      <<: *aws_connection_info
+    register: sec_group
+
+  - name: create a target group for testing
+    elb_target_group:
+      name: "{{ tg_name }}"
+      protocol: http
+      port: 80
+      vpc_id: "{{ vpc.vpc.id }}"
+      state: present
+      <<: *aws_connection_info
+    register: tg
+
+  - name: create privatekey for testing
+    openssl_privatekey:
+      path: ./ansible_alb_test.pem
+      size: 2048
+
+  - name: create csr for cert
+    openssl_csr:
+      path: ./ansible_alb_test.csr
+      privatekey_path: ./ansible_alb_test.pem
+      C: US
+      ST: AnyPrincipality
+      L: AnyTown
+      O: AnsibleIntegrationTest
+      OU: Test
+      CN: ansible-alb-test.example.com
+
+  - name: create certificate
+    openssl_certificate:
+      path: ./ansible_alb_test.crt
+      privatekey_path: ./ansible_alb_test.pem
+      csr_path: ./ansible_alb_test.csr
+      provider: selfsigned
+
+  # This really should be an ACM Cert, but there is no acm_cert resource module
+  - name: upload server cert to iam
+    iam_cert:
+      name: "{{ alb_name }}"
+      state: present
+      cert: ./ansible_alb_test.crt
+      key: ./ansible_alb_test.pem
+      <<: *aws_connection_info
+    register: cert_upload
+
+  - name: register certificate arn to acm_arn fact
+    set_fact:
+        cert_arn: "{{ cert_upload.arn }}"
+
+  - include_tasks: test_alb_bad_listener_options.yml
+  - include_tasks: test_alb_tags.yml
+  - include_tasks: test_creating_alb.yml
+  - include_tasks: test_alb_with_asg.yml
+  - include_tasks: test_modifying_alb_listeners.yml
+  - include_tasks: test_deleting_alb.yml
+  - include_tasks: test_multiple_actions.yml
+
+  always:
+  #############################################################################
+  # TEAR DOWN STARTS HERE
+  #############################################################################
+    - name: destroy ALB
+      elb_application_lb:
+        name: "{{ alb_name }}"
+        state: absent
+        wait: yes
+        wait_timeout: 600
+        <<: *aws_connection_info
+      ignore_errors: yes
+
+    - name: destroy target group if it was created
+      elb_target_group:
+        name: "{{ tg_name }}"
+        protocol: http
+        port: 80
+        vpc_id: "{{ vpc.vpc.id }}"
+        state: absent
+        wait: yes
+        wait_timeout: 600
+        <<: *aws_connection_info
+      register: remove_tg
+      retries: 5
+      delay: 3
+      until: remove_tg is success
+      when: tg is defined
+      ignore_errors: yes
+
+    - name: destroy acm certificate
+      iam_cert:
+        name: "{{ alb_name }}"
+        state: absent
+        <<: *aws_connection_info
+      register: remove_cert
+      retries: 5
+      delay: 3
+      until: remove_cert is success
+      when: cert_arn is defined
+      ignore_errors: yes
+
+    - name: destroy sec group
+      ec2_group:
+        name: "{{ sec_group.group_name }}"
+        description: "security group for Ansible ALB integration tests"
+        state: absent
+        vpc_id: "{{ vpc.vpc.id }}"
+        <<: *aws_connection_info
+      register: remove_sg
+      retries: 10
+      delay: 5
+      until: remove_sg is success
+      ignore_errors: yes
+
+    - name: remove route table
+      ec2_vpc_route_table:
+        vpc_id: "{{ vpc.vpc.id }}"
+        route_table_id: "{{ route_table.route_table.route_table_id }}"
+        lookup: id
+        state: absent
+        <<: *aws_connection_info
+      register: remove_rt
+      retries: 10
+      delay: 5
+      until: remove_rt is success
+      ignore_errors: yes
+
+    - name: destroy subnets
+      ec2_vpc_subnet:
+        cidr: "{{ item.cidr }}"
+        vpc_id: "{{ vpc.vpc.id }}"
+        state: absent
+        <<: *aws_connection_info
+      register: remove_subnet
+      retries: 10
+      delay: 5
+      until: remove_subnet is success
+      with_items:
+        - cidr: 10.228.228.0/24
+        - cidr: 10.228.229.0/24
+        - cidr: 10.228.230.0/24
+        - cidr: 10.228.231.0/24
+      ignore_errors: yes
+
+    - name: destroy internet gateway
+      ec2_vpc_igw:
+        vpc_id: "{{ vpc.vpc.id }}"
+        tags:
+          Name: "{{ resource_prefix }}"
+        state: absent
+        <<: *aws_connection_info
+      register: remove_igw
+      retries: 10
+      delay: 5
+      until: remove_igw is success
+      ignore_errors: yes
+
+    - name: destroy VPC
+      ec2_vpc_net:
+        cidr_block: 10.228.228.0/22
+        name: "{{ resource_prefix }}_vpc"
+        state: absent
+        <<: *aws_connection_info
+      register: remove_vpc
+      retries: 10
+      delay: 5
+      until: remove_vpc is success
+      ignore_errors: yes
diff --git a/test/integration/targets/elb_application_lb/tasks/main.yml b/test/integration/targets/elb_application_lb/tasks/main.yml
--- a/test/integration/targets/elb_application_lb/tasks/main.yml
+++ b/test/integration/targets/elb_application_lb/tasks/main.yml
@@ -1,204 +1,44 @@
-- block:
-
-  - name: set connection information for all tasks
-    set_fact:
-      aws_connection_info: &aws_connection_info
-        aws_access_key: "{{ aws_access_key }}"
-        aws_secret_key: "{{ aws_secret_key }}"
-        security_token: "{{ security_token }}"
-        region: "{{ aws_region }}"
-    no_log: yes
-
-  - name: create VPC
-    ec2_vpc_net:
-      cidr_block: 10.228.228.0/22
-      name: "{{ resource_prefix }}_vpc"
-      state: present
-      <<: *aws_connection_info
-    register: vpc
-
-  - name: create internet gateway
-    ec2_vpc_igw:
-      vpc_id: "{{ vpc.vpc.id }}"
-      state: present
-      tags:
-        Name: "{{ resource_prefix }}"
-      <<: *aws_connection_info
-    register: igw
-
-  - name: create public subnet
-    ec2_vpc_subnet:
-      cidr: "{{ item.cidr }}"
-      az: "{{ aws_region}}{{ item.az }}"
-      vpc_id: "{{ vpc.vpc.id }}"
-      state: present
-      tags:
-        Public: "{{ item.public|string }}"
-        Name: "{{ item.public|ternary('public', 'private') }}-{{ item.az }}"
-      <<: *aws_connection_info
-    with_items:
-      - cidr: 10.228.228.0/24
-        az: "a"
-        public: "True"
-      - cidr: 10.228.229.0/24
-        az: "b"
-        public: "True"
-      - cidr: 10.228.230.0/24
-        az: "a"
-        public: "False"
-      - cidr: 10.228.231.0/24
-        az: "b"
-        public: "False"
-    register: subnets
-
-  - ec2_vpc_subnet_info:
-      filters:
-        vpc-id: "{{ vpc.vpc.id }}"
-      <<: *aws_connection_info
-    register: vpc_subnets
-
-  - name: create list of subnet ids
-    set_fact:
-      alb_subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `True`].id') }}"
-      private_subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public != `True`].id') }}"
-
-  - name: create a route table
-    ec2_vpc_route_table:
-      vpc_id: "{{ vpc.vpc.id }}"
-      <<: *aws_connection_info
-      tags:
-        Name: igw-route
-        Created: "{{ resource_prefix }}"
-      subnets: "{{ alb_subnets + private_subnets }}"
-      routes:
-        - dest: 0.0.0.0/0
-          gateway_id: "{{ igw.gateway_id }}"
-    register: route_table
-
-  - ec2_group:
-      name: "{{ resource_prefix }}"
-      description: "security group for Ansible ALB integration tests"
-      state: present
-      vpc_id: "{{ vpc.vpc.id }}"
-      rules:
-        - proto: tcp
-          from_port: 1
-          to_port: 65535
-          cidr_ip: 0.0.0.0/0
-      <<: *aws_connection_info
-    register: sec_group
-
-  - name: create a target group for testing
-    elb_target_group:
-      name: "{{ tg_name }}"
-      protocol: http
-      port: 80
-      vpc_id: "{{ vpc.vpc.id }}"
-      state: present
-      <<: *aws_connection_info
-    register: tg
-
-  - include_tasks: test_alb_bad_listener_options.yml
-  - include_tasks: test_alb_tags.yml
-  - include_tasks: test_creating_alb.yml
-  - include_tasks: test_alb_with_asg.yml
-  - include_tasks: test_modifying_alb_listeners.yml
-  - include_tasks: test_deleting_alb.yml
-
-  always:
-  #############################################################################
-  # TEAR DOWN STARTS HERE
-  #############################################################################
-    - name: destroy ALB
-      elb_application_lb:
-        name: "{{ alb_name }}"
-        state: absent
-        wait: yes
-        wait_timeout: 600
-        <<: *aws_connection_info
-      ignore_errors: yes
-
-    - name: destroy target group if it was created
-      elb_target_group:
-        name: "{{ tg_name }}"
-        protocol: http
-        port: 80
-        vpc_id: "{{ vpc.vpc.id }}"
-        state: absent
-        wait: yes
-        wait_timeout: 600
-        <<: *aws_connection_info
-      register: remove_tg
-      retries: 5
-      delay: 3
-      until: remove_tg is success
-      when: tg is defined
-      ignore_errors: yes
-
-    - name: destroy sec group
-      ec2_group:
-        name: "{{ sec_group.group_name }}"
-        description: "security group for Ansible ALB integration tests"
-        state: absent
-        vpc_id: "{{ vpc.vpc.id }}"
-        <<: *aws_connection_info
-      register: remove_sg
-      retries: 10
-      delay: 5
-      until: remove_sg is success
-      ignore_errors: yes
-
-    - name: remove route table
-      ec2_vpc_route_table:
-        vpc_id: "{{ vpc.vpc.id }}"
-        route_table_id: "{{ route_table.route_table.route_table_id }}"
-        lookup: id
-        state: absent
-        <<: *aws_connection_info
-      register: remove_rt
-      retries: 10
-      delay: 5
-      until: remove_rt is success
-      ignore_errors: yes
-
-    - name: destroy subnets
-      ec2_vpc_subnet:
-        cidr: "{{ item.cidr }}"
-        vpc_id: "{{ vpc.vpc.id }}"
-        state: absent
-        <<: *aws_connection_info
-      register: remove_subnet
-      retries: 10
-      delay: 5
-      until: remove_subnet is success
-      with_items:
-        - cidr: 10.228.228.0/24
-        - cidr: 10.228.229.0/24
-        - cidr: 10.228.230.0/24
-        - cidr: 10.228.231.0/24
-      ignore_errors: yes
-
-    - name: destroy internet gateway
-      ec2_vpc_igw:
-        vpc_id: "{{ vpc.vpc.id }}"
-        tags:
-          Name: "{{ resource_prefix }}"
-        state: absent
-        <<: *aws_connection_info
-      register: remove_igw
-      retries: 10
-      delay: 5
-      until: remove_igw is success
-      ignore_errors: yes
-
-    - name: destroy VPC
-      ec2_vpc_net:
-        cidr_block: 10.228.228.0/22
-        name: "{{ resource_prefix }}_vpc"
-        state: absent
-        <<: *aws_connection_info
-      register: remove_vpc
-      retries: 10
-      delay: 5
-      until: remove_vpc is success
-      ignore_errors: yes
+- set_fact:
+    virtualenv: "{{ remote_tmp_dir }}/virtualenv"
+    virtualenv_command: "{{ ansible_python_interpreter }} -m virtualenv"
+
+- set_fact:
+    virtualenv_interpreter: "{{ virtualenv }}/bin/python"
+
+- pip:
+    name: virtualenv
+
+- pip:
+    name:
+      - 'botocore<1.10.30'
+      - boto3
+      - boto
+      - coverage
+      - cryptography
+    virtualenv: "{{ virtualenv }}"
+    virtualenv_command: "{{ virtualenv_command }}"
+    virtualenv_site_packages: no
+
+- include_tasks: multiple_actions_fail.yml
+  vars:
+    ansible_python_interpreter: "{{ virtualenv_interpreter }}"
+
+
+- pip:
+    name:
+      - 'botocore>=1.10.30'
+      - boto3
+      - boto
+      - coverage
+      - cryptography
+    virtualenv: "{{ virtualenv }}"
+    virtualenv_command: "{{ virtualenv_command }}"
+    virtualenv_site_packages: no
+
+- include_tasks: full_test.yml
+  vars:
+    ansible_python_interpreter: "{{ virtualenv_interpreter }}"
+
+- file:
+    path: "{{ virtualenv }}"
+    state: absent
diff --git a/test/integration/targets/elb_application_lb/tasks/multiple_actions_fail.yml b/test/integration/targets/elb_application_lb/tasks/multiple_actions_fail.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/elb_application_lb/tasks/multiple_actions_fail.yml
@@ -0,0 +1,253 @@
+- block:
+
+    - name: set connection information for all tasks
+      set_fact:
+        aws_connection_info: &aws_connection_info
+          aws_access_key: "{{ aws_access_key }}"
+          aws_secret_key: "{{ aws_secret_key }}"
+          security_token: "{{ security_token }}"
+          region: "{{ aws_region }}"
+        awscli_connection_info: &awscli_connection_info
+          AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
+          AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
+          AWS_SESSION_TOKEN: "{{ security_token }}"
+          AWS_DEFAULT_REGION: "{{ aws_region }}"
+      no_log: yes
+
+    - name: create VPC
+      ec2_vpc_net:
+        cidr_block: 10.228.228.0/22
+        name: "{{ resource_prefix }}_vpc"
+        state: present
+        <<: *aws_connection_info
+      register: vpc
+
+    - name: create internet gateway
+      ec2_vpc_igw:
+        vpc_id: "{{ vpc.vpc.id }}"
+        state: present
+        tags:
+          Name: "{{ resource_prefix }}"
+        <<: *aws_connection_info
+      register: igw
+
+    - name: create public subnet
+      ec2_vpc_subnet:
+        cidr: "{{ item.cidr }}"
+        az: "{{ aws_region}}{{ item.az }}"
+        vpc_id: "{{ vpc.vpc.id }}"
+        state: present
+        tags:
+          Public: "{{ item.public|string }}"
+          Name: "{{ item.public|ternary('public', 'private') }}-{{ item.az }}"
+        <<: *aws_connection_info
+      with_items:
+        - cidr: 10.228.228.0/24
+          az: "a"
+          public: "True"
+        - cidr: 10.228.229.0/24
+          az: "b"
+          public: "True"
+        - cidr: 10.228.230.0/24
+          az: "a"
+          public: "False"
+        - cidr: 10.228.231.0/24
+          az: "b"
+          public: "False"
+      register: subnets
+
+    - ec2_vpc_subnet_facts:
+        filters:
+          vpc-id: "{{ vpc.vpc.id }}"
+        <<: *aws_connection_info
+      register: vpc_subnets
+
+    - name: create list of subnet ids
+      set_fact:
+        alb_subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public == `True`].id') }}"
+        private_subnets: "{{ vpc_subnets|json_query('subnets[?tags.Public != `True`].id') }}"
+
+    - name: create a route table
+      ec2_vpc_route_table:
+        vpc_id: "{{ vpc.vpc.id }}"
+        <<: *aws_connection_info
+        tags:
+          Name: igw-route
+          Created: "{{ resource_prefix }}"
+        subnets: "{{ alb_subnets + private_subnets }}"
+        routes:
+          - dest: 0.0.0.0/0
+            gateway_id: "{{ igw.gateway_id }}"
+      register: route_table
+
+    - ec2_group:
+        name: "{{ resource_prefix }}"
+        description: "security group for Ansible ALB integration tests"
+        state: present
+        vpc_id: "{{ vpc.vpc.id }}"
+        rules:
+          - proto: tcp
+            from_port: 1
+            to_port: 65535
+            cidr_ip: 0.0.0.0/0
+        <<: *aws_connection_info
+      register: sec_group
+
+    - name: create a target group for testing
+      elb_target_group:
+        name: "{{ tg_name }}"
+        protocol: http
+        port: 80
+        vpc_id: "{{ vpc.vpc.id }}"
+        state: present
+        <<: *aws_connection_info
+      register: tg
+
+    - name: create privatekey for testing
+      openssl_privatekey:
+        path: ./ansible_alb_test.pem
+        size: 2048
+
+    - name: create csr for cert
+      openssl_csr:
+        path: ./ansible_alb_test.csr
+        privatekey_path: ./ansible_alb_test.pem
+        C: US
+        ST: AnyPrincipality
+        L: AnyTown
+        O: AnsibleIntegrationTest
+        OU: Test
+        CN: ansible-alb-test.example.com
+
+    - name: create certificate
+      openssl_certificate:
+        path: ./ansible_alb_test.crt
+        privatekey_path: ./ansible_alb_test.pem
+        csr_path: ./ansible_alb_test.csr
+        provider: selfsigned
+
+    # This really should be an ACM Cert, but there is no acm_cert resource module
+    - name: upload server cert to iam
+      iam_cert:
+        name: "{{ alb_name }}"
+        state: present
+        cert: ./ansible_alb_test.crt
+        key: ./ansible_alb_test.pem
+        <<: *aws_connection_info
+      register: cert_upload
+
+    - name: register certificate arn to acm_arn fact
+      set_fact:
+        cert_arn: "{{ cert_upload.arn }}"
+
+    - include_tasks: test_multiple_actions_fail.yml
+
+  always:
+    #############################################################################
+    # TEAR DOWN STARTS HERE
+    #############################################################################
+    - name: destroy ALB
+      elb_application_lb:
+        name: "{{ alb_name }}"
+        state: absent
+        wait: yes
+        wait_timeout: 600
+        <<: *aws_connection_info
+      ignore_errors: yes
+
+    - name: destroy target group if it was created
+      elb_target_group:
+        name: "{{ tg_name }}"
+        protocol: http
+        port: 80
+        vpc_id: "{{ vpc.vpc.id }}"
+        state: absent
+        wait: yes
+        wait_timeout: 600
+        <<: *aws_connection_info
+      register: remove_tg
+      retries: 10
+      delay: 5
+      until: remove_tg is success
+      when: tg is defined
+      ignore_errors: yes
+
+    - name: destroy acm certificate
+      iam_cert:
+        name: "{{ alb_name }}"
+        state: absent
+        <<: *aws_connection_info
+      register: remove_cert
+      retries: 10
+      delay: 5
+      until: remove_cert is success
+      when: cert_arn is defined
+      ignore_errors: yes
+
+    - name: destroy sec group
+      ec2_group:
+        name: "{{ sec_group.group_name }}"
+        description: "security group for Ansible ALB integration tests"
+        state: absent
+        vpc_id: "{{ vpc.vpc.id }}"
+        <<: *aws_connection_info
+      register: remove_sg
+      retries: 10
+      delay: 5
+      until: remove_sg is success
+      ignore_errors: yes
+
+    - name: remove route table
+      ec2_vpc_route_table:
+        vpc_id: "{{ vpc.vpc.id }}"
+        route_table_id: "{{ route_table.route_table.route_table_id }}"
+        lookup: id
+        state: absent
+        <<: *aws_connection_info
+      register: remove_rt
+      retries: 10
+      delay: 5
+      until: remove_rt is success
+      ignore_errors: yes
+
+    - name: destroy subnets
+      ec2_vpc_subnet:
+        cidr: "{{ item.cidr }}"
+        vpc_id: "{{ vpc.vpc.id }}"
+        state: absent
+        <<: *aws_connection_info
+      register: remove_subnet
+      retries: 10
+      delay: 5
+      until: remove_subnet is success
+      with_items:
+        - cidr: 10.228.228.0/24
+        - cidr: 10.228.229.0/24
+        - cidr: 10.228.230.0/24
+        - cidr: 10.228.231.0/24
+      ignore_errors: yes
+
+    - name: destroy internet gateway
+      ec2_vpc_igw:
+        vpc_id: "{{ vpc.vpc.id }}"
+        tags:
+          Name: "{{ resource_prefix }}"
+        state: absent
+        <<: *aws_connection_info
+      register: remove_igw
+      retries: 10
+      delay: 5
+      until: remove_igw is success
+      ignore_errors: yes
+
+    - name: destroy VPC
+      ec2_vpc_net:
+        cidr_block: 10.228.228.0/22
+        name: "{{ resource_prefix }}_vpc"
+        state: absent
+        <<: *aws_connection_info
+      register: remove_vpc
+      retries: 10
+      delay: 5
+      until: remove_vpc is success
+      ignore_errors: yes
diff --git a/test/integration/targets/elb_application_lb/tasks/test_multiple_actions.yml b/test/integration/targets/elb_application_lb/tasks/test_multiple_actions.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/elb_application_lb/tasks/test_multiple_actions.yml
@@ -0,0 +1,467 @@
+- block:
+
+  - name: set connection information for all tasks
+    set_fact:
+      aws_connection_info: &aws_connection_info
+        aws_access_key: "{{ aws_access_key }}"
+        aws_secret_key: "{{ aws_secret_key }}"
+        security_token: "{{ security_token }}"
+        region: "{{ aws_region }}"
+    no_log: yes
+
+  - name: register dummy OIDC config
+    set_fact:
+      AuthenticateOidcActionConfig:
+        AuthorizationEndpoint: "https://www.example.com/auth"
+        ClientId: "eeeeeeeeeeeeeeeeeeeeeeeeee"
+        ClientSecret: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
+        Issuer: "https://www.example.com/issuer"
+        OnUnauthenticatedRequest: "authenticate"
+        Scope: "openid"
+        SessionCookieName: "AWSELBAuthSessionCookie"
+        SessionTimeout: 604800
+        TokenEndpoint: "https://www.example.com/token"
+        UserInfoEndpoint: "https://www.example.com/userinfo"
+        UseExistingClientSecret: true
+
+  - name: register fixed response action
+    set_fact:
+      FixedResponseActionConfig:
+        ContentType: "text/plain"
+        MessageBody: "This is the page you're looking for"
+        StatusCode: "200"
+
+  - name: register redirect action
+    set_fact:
+      RedirectActionConfig:
+        Host: "#{host}"
+        Path: "/example/redir" # or /#{path}
+        Port: "#{port}"
+        Protocol: "#{protocol}"
+        Query: "#{query}"
+        StatusCode: "HTTP_302" # or HTTP_301
+
+  - name: delete existing ALB to avoid target group association issues
+    elb_application_lb:
+      name: "{{ alb_name }}"
+      state: absent
+      <<: *aws_connection_info
+      wait: yes
+      wait_timeout: 600
+
+  - name: cleanup tg to avoid target group association issues
+    elb_target_group:
+      name: "{{ tg_name }}"
+      protocol: http
+      port: 80
+      vpc_id: "{{ vpc.vpc.id }}"
+      state: absent
+      wait: yes
+      wait_timeout: 600
+      <<: *aws_connection_info
+    register: cleanup_tg
+    retries: 5
+    delay: 3
+    until: cleanup_tg is success
+
+  - name: recreate a target group
+    elb_target_group:
+      name: "{{ tg_name }}"
+      protocol: http
+      port: 80
+      vpc_id: "{{ vpc.vpc.id }}"
+      state: present
+      <<: *aws_connection_info
+    register: tg
+
+  - name: create ALB with redirect DefaultAction
+    elb_application_lb:
+      name: "{{ alb_name }}"
+      subnets: "{{ alb_subnets }}"
+      security_groups: "{{ sec_group.group_id }}"
+      state: present
+      listeners:
+        - Protocol: HTTPS
+          Port: 443
+          DefaultActions:
+            - Type: redirect
+              RedirectConfig: "{{ RedirectActionConfig }}"
+          Certificates:
+            - CertificateArn: "{{ cert_arn }}"
+          SslPolicy: ELBSecurityPolicy-2016-08
+      <<: *aws_connection_info
+    register: alb
+
+  - assert:
+      that:
+        - alb.changed
+        - alb.listeners|length == 1
+        - alb.listeners[0].rules[0].actions|length == 1
+        - alb.listeners[0].rules[0].actions[0].type == "redirect"
+
+  - name: test idempotence with redirect DefaultAction
+    elb_application_lb:
+      name: "{{ alb_name }}"
+      subnets: "{{ alb_subnets }}"
+      security_groups: "{{ sec_group.group_id }}"
+      state: present
+      listeners:
+        - Protocol: HTTPS
+          Port: 443
+          DefaultActions:
+            - Type: redirect
+              RedirectConfig: "{{ RedirectActionConfig }}"
+          Certificates:
+            - CertificateArn: "{{ cert_arn }}"
+          SslPolicy: ELBSecurityPolicy-2016-08
+      <<: *aws_connection_info
+    register: alb
+
+  - assert:
+      that:
+        - not alb.changed
+        - alb.listeners|length == 1
+        - alb.listeners[0].rules[0].actions|length == 1
+        - alb.listeners[0].rules[0].actions[0].type == "redirect"
+
+  - name: update ALB with fixed-response DefaultAction
+    elb_application_lb:
+      name: "{{ alb_name }}"
+      subnets: "{{ alb_subnets }}"
+      security_groups: "{{ sec_group.group_id }}"
+      state: present
+      listeners:
+        - Protocol: HTTPS
+          Port: 443
+          DefaultActions:
+            - Type: fixed-response
+              FixedResponseConfig: "{{ FixedResponseActionConfig }}"
+          Certificates:
+            - CertificateArn: "{{ cert_arn }}"
+          SslPolicy: ELBSecurityPolicy-2016-08
+      <<: *aws_connection_info
+    register: alb
+
+  - assert:
+      that:
+        - alb.changed
+        - alb.listeners|length == 1
+        - alb.listeners[0].rules[0].actions|length == 1
+        - alb.listeners[0].rules[0].actions[0].type == "fixed-response"
+
+  - name: test idempotence with fixed-response DefaultAction
+    elb_application_lb:
+      name: "{{ alb_name }}"
+      subnets: "{{ alb_subnets }}"
+      security_groups: "{{ sec_group.group_id }}"
+      state: present
+      listeners:
+        - Protocol: HTTPS
+          Port: 443
+          DefaultActions:
+            - Type: fixed-response
+              FixedResponseConfig: "{{ FixedResponseActionConfig }}"
+          Certificates:
+            - CertificateArn: "{{ cert_arn }}"
+          SslPolicy: ELBSecurityPolicy-2016-08
+      <<: *aws_connection_info
+    register: alb
+
+  - assert:
+      that:
+        - not alb.changed
+        - alb.listeners|length == 1
+        - alb.listeners[0].rules[0].actions|length == 1
+        - alb.listeners[0].rules[0].actions[0].type == "fixed-response"
+
+  - name: test multiple non-default rules
+    elb_application_lb:
+      name: "{{ alb_name }}"
+      subnets: "{{ alb_subnets }}"
+      security_groups: "{{ sec_group.group_id }}"
+      state: present
+      listeners:
+        - Protocol: HTTPS
+          Port: 443
+          DefaultActions:
+            - Type: fixed-response
+              FixedResponseConfig: "{{ FixedResponseActionConfig }}"
+          Certificates:
+            - CertificateArn: "{{ cert_arn }}"
+          SslPolicy: ELBSecurityPolicy-2016-08
+          Rules:
+            - Conditions:
+                - Field: path-pattern
+                  Values:
+                    - "/forward-path/*"
+              Priority: 1
+              Actions:
+                - Type: forward
+                  TargetGroupName: "{{ tg_name }}"
+            - Conditions:
+                - Field: path-pattern
+                  Values:
+                    - "/redirect-path/*"
+              Priority: 2
+              Actions:
+                - Type: redirect
+                  RedirectConfig: "{{ RedirectActionConfig }}"
+            - Conditions:
+                - Field: path-pattern
+                  Values:
+                    - "/fixed-response-path/"
+              Priority: 3
+              Actions:
+                - Type: fixed-response
+                  FixedResponseConfig: "{{ FixedResponseActionConfig }}"
+      <<: *aws_connection_info
+    register: alb
+
+  - assert:
+      that:
+        - alb.changed
+        - alb.listeners|length == 1
+        - alb.listeners[0].rules|length == 4 ## defaultactions is included as a rule
+        - alb.listeners[0].rules[0].actions|length == 1
+        - alb.listeners[0].rules[0].actions[0].type == "forward"
+        - alb.listeners[0].rules[1].actions|length == 1
+        - alb.listeners[0].rules[1].actions[0].type == "redirect"
+        - alb.listeners[0].rules[2].actions|length == 1
+        - alb.listeners[0].rules[2].actions[0].type == "fixed-response"
+
+  - name: test idempotence multiple non-default rules
+    elb_application_lb:
+      name: "{{ alb_name }}"
+      subnets: "{{ alb_subnets }}"
+      security_groups: "{{ sec_group.group_id }}"
+      state: present
+      listeners:
+        - Protocol: HTTPS
+          Port: 443
+          DefaultActions:
+            - Type: fixed-response
+              FixedResponseConfig: "{{ FixedResponseActionConfig }}"
+          Certificates:
+            - CertificateArn: "{{ cert_arn }}"
+          SslPolicy: ELBSecurityPolicy-2016-08
+          Rules:
+            - Conditions:
+                - Field: path-pattern
+                  Values:
+                    - "/forward-path/*"
+              Priority: 1
+              Actions:
+                - Type: forward
+                  TargetGroupName: "{{ tg_name }}"
+            - Conditions:
+                - Field: path-pattern
+                  Values:
+                    - "/redirect-path/*"
+              Priority: 2
+              Actions:
+                - Type: redirect
+                  RedirectConfig: "{{ RedirectActionConfig }}"
+            - Conditions:
+                - Field: path-pattern
+                  Values:
+                    - "/fixed-response-path/"
+              Priority: 3
+              Actions:
+                - Type: fixed-response
+                  FixedResponseConfig: "{{ FixedResponseActionConfig }}"
+      <<: *aws_connection_info
+    register: alb
+
+  - assert:
+      that:
+        - not alb.changed
+        - alb.listeners|length == 1
+        - alb.listeners[0].rules|length == 4 ## defaultactions is included as a rule
+        - alb.listeners[0].rules[0].actions|length == 1
+        - alb.listeners[0].rules[0].actions[0].type == "forward"
+        - alb.listeners[0].rules[1].actions|length == 1
+        - alb.listeners[0].rules[1].actions[0].type == "redirect"
+        - alb.listeners[0].rules[2].actions|length == 1
+        - alb.listeners[0].rules[2].actions[0].type == "fixed-response"
+
+
+#  - name: test creating ALB with a default listener with multiple actions
+#    elb_application_lb:
+#      name: "{{ alb_name }}"
+#      subnets: "{{ alb_subnets }}"
+#      security_groups: "{{ sec_group.group_id }}"
+#      state: present
+#      listeners:
+#        - Protocol: HTTPS
+#          Port: 443
+#          DefaultActions:
+#            - Type: forward
+#              TargetGroupName: "{{ tg_name }}"
+#              Order: 2
+#            - Type: authenticate-oidc
+#              AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+#              Order: 1
+#          Certificates:
+#            - CertificateArn: "{{ cert_arn }}"
+#          SslPolicy: ELBSecurityPolicy-2016-08
+#      <<: *aws_connection_info
+#    register: alb
+#
+#  - assert:
+#      that:
+#        - alb.listeners|length == 1
+#        - alb.listeners[0].rules[0].actions|length == 2
+#
+#  - name: test changing order of actions
+#    elb_application_lb:
+#      name: "{{ alb_name }}"
+#      subnets: "{{ alb_subnets }}"
+#      security_groups: "{{ sec_group.group_id }}"
+#      state: present
+#      listeners:
+#        - Protocol: HTTPS
+#          Port: 443
+#          DefaultActions:
+#            - Type: authenticate-oidc
+#              AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+#              Order: 1
+#            - Type: forward
+#              TargetGroupName: "{{ tg_name }}"
+#              Order: 2
+#          Certificates:
+#            - CertificateArn: "{{ cert_arn }}"
+#          SslPolicy: ELBSecurityPolicy-2016-08
+#      <<: *aws_connection_info
+#    register: alb
+#
+#  - assert:
+#      that:
+#        - not alb.changed
+#        - alb.listeners|length == 1
+#        - alb.listeners[0].rules[0].actions|length == 2
+#
+#  - name: test non-default rule with multiple actions
+#    elb_application_lb:
+#      name: "{{ alb_name }}"
+#      subnets: "{{ alb_subnets }}"
+#      security_groups: "{{ sec_group.group_id }}"
+#      state: present
+#      listeners:
+#        - Protocol: HTTPS
+#          Port: 443
+#          DefaultActions:
+#            - Type: authenticate-oidc
+#              AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+#              Order: 1
+#            - Type: forward
+#              TargetGroupName: "{{ tg_name }}"
+#              Order: 2
+#          Certificates:
+#            - CertificateArn: "{{ cert_arn }}"
+#          SslPolicy: ELBSecurityPolicy-2016-08
+#          Rules:
+#            - Conditions:
+#                - Field: path-pattern
+#                  Values:
+#                    - "*"
+#              Priority: 1
+#              Actions:
+#                - Type: forward
+#                  TargetGroupName: "{{ tg_name }}"
+#                  Order: 2
+#                - Type: authenticate-oidc
+#                  AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+#                  Order: 1
+#      <<: *aws_connection_info
+#    register: alb
+#
+#  - assert:
+#      that:
+#        - alb.changed
+#        - alb.listeners|length == 1
+#        - alb.listeners[0].rules[0].actions|length == 2
+#        - alb.listeners[0].rules[1].actions|length == 2
+#
+#  - name: test idempotency non-default rule with multiple actions
+#    elb_application_lb:
+#      name: "{{ alb_name }}"
+#      subnets: "{{ alb_subnets }}"
+#      security_groups: "{{ sec_group.group_id }}"
+#      state: present
+#      listeners:
+#        - Protocol: HTTPS
+#          Port: 443
+#          DefaultActions:
+#            - Type: authenticate-oidc
+#              AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+#              Order: 1
+#            - Type: forward
+#              TargetGroupName: "{{ tg_name }}"
+#              Order: 2
+#          Certificates:
+#            - CertificateArn: "{{ cert_arn }}"
+#          SslPolicy: ELBSecurityPolicy-2016-08
+#          Rules:
+#            - Conditions:
+#                - Field: path-pattern
+#                  Values:
+#                    - "*"
+#              Priority: 1
+#              Actions:
+#                - Type: forward
+#                  TargetGroupName: "{{ tg_name }}"
+#                  Order: 2
+#                - Type: authenticate-oidc
+#                  AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+#                  Order: 1
+#      <<: *aws_connection_info
+#    register: alb
+#
+#  - assert:
+#      that:
+#        - not alb.changed
+#        - alb.listeners|length == 1
+#        - alb.listeners[0].rules[0].actions|length == 2
+#        - alb.listeners[0].rules[1].actions|length == 2
+#
+#  - name: test non-default rule action order change
+#    elb_application_lb:
+#      name: "{{ alb_name }}"
+#      subnets: "{{ alb_subnets }}"
+#      security_groups: "{{ sec_group.group_id }}"
+#      state: present
+#      listeners:
+#        - Protocol: HTTPS
+#          Port: 443
+#          DefaultActions:
+#            - Type: authenticate-oidc
+#              AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+#              Order: 1
+#            - Type: forward
+#              TargetGroupName: "{{ tg_name }}"
+#              Order: 2
+#          Certificates:
+#            - CertificateArn: "{{ cert_arn }}"
+#          SslPolicy: ELBSecurityPolicy-2016-08
+#          Rules:
+#            - Conditions:
+#                - Field: path-pattern
+#                  Values:
+#                    - "*"
+#              Priority: 1
+#              Actions:
+#                - Type: authenticate-oidc
+#                  AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+#                  Order: 1
+#                - Type: forward
+#                  TargetGroupName: "{{ tg_name }}"
+#                  Order: 2
+#      <<: *aws_connection_info
+#    register: alb
+#
+#  - assert:
+#      that:
+#        - not alb.changed
+#        - alb.listeners|length == 1
+#        - alb.listeners[0].rules[0].actions|length == 2
+#        - alb.listeners[0].rules[1].actions|length == 2
diff --git a/test/integration/targets/elb_application_lb/tasks/test_multiple_actions_fail.yml b/test/integration/targets/elb_application_lb/tasks/test_multiple_actions_fail.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/elb_application_lb/tasks/test_multiple_actions_fail.yml
@@ -0,0 +1,53 @@
+- block:
+
+  - name: set connection information for all tasks
+    set_fact:
+      aws_connection_info: &aws_connection_info
+        aws_access_key: "{{ aws_access_key }}"
+        aws_secret_key: "{{ aws_secret_key }}"
+        security_token: "{{ security_token }}"
+        region: "{{ aws_region }}"
+    no_log: yes
+
+  - name: register dummy OIDC config
+    set_fact:
+        AuthenticateOidcActionConfig:
+            AuthorizationEndpoint: "https://www.example.com/auth"
+            ClientId: "eeeeeeeeeeeeeeeeeeeeeeeeee"
+            ClientSecret: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
+            Issuer: "https://www.example.com/issuer"
+            OnUnauthenticatedRequest: "authenticate"
+            Scope: "openid"
+            SessionCookieName: "AWSELBAuthSessionCookie"
+            SessionTimeout: 604800
+            TokenEndpoint: "https://www.example.com/token"
+            UserInfoEndpoint: "https://www.example.com/userinfo"
+
+  - name: create ALB with multiple DefaultActions
+    elb_application_lb:
+      name: "{{ alb_name }}"
+      subnets: "{{ alb_subnets }}"
+      security_groups: "{{ sec_group.group_id }}"
+      state: present
+      listeners:
+        - Protocol: HTTPS
+          Port: 443
+          DefaultActions:
+            - Type: forward
+              TargetGroupName: "{{ tg_name }}"
+              Order: 2
+            - Type: authenticate-oidc
+              AuthenticateOidcConfig: "{{ AuthenticateOidcActionConfig }}"
+              Order: 1
+          Certificates:
+              - CertificateArn: "{{ cert_arn }}"
+          SslPolicy: ELBSecurityPolicy-2016-08
+      <<: *aws_connection_info
+    register: alb
+    ignore_errors: yes
+
+  - name: check for a graceful failure message
+    assert:
+      that:
+        - alb.failed
+        - 'alb.msg == "installed version of botocore does not support multiple actions, please upgrade botocore to version 1.10.30 or higher"'
diff --git a/test/integration/targets/setup_remote_tmp_dir/handlers/main.yml b/test/integration/targets/setup_remote_tmp_dir/handlers/main.yml
--- a/test/integration/targets/setup_remote_tmp_dir/handlers/main.yml
+++ b/test/integration/targets/setup_remote_tmp_dir/handlers/main.yml
@@ -2,6 +2,7 @@
   file:
     path: "{{ remote_tmp_dir }}"
     state: absent
+  no_log: yes
 
 - name: delete temporary directory (windows)
   win_file: