diff --git a/lib/ansible/modules/network/f5/bigip_iapp_service.py b/lib/ansible/modules/network/f5/bigip_iapp_service.py
index eead532ae58..45e627445e5 100644
--- a/lib/ansible/modules/network/f5/bigip_iapp_service.py
+++ b/lib/ansible/modules/network/f5/bigip_iapp_service.py
@@ -19,9 +19,7 @@ short_description: Manages TCL iApp services on a BIG-IP
 description:
   - Manages TCL iApp services on a BIG-IP.
   - If you are looking for the API that is communicated with on the BIG-IP,
-    the one the is used is C(/mgmt/tm/sys/application/service/). There are a
-    couple of APIs in a BIG-IP that might seem like they are relevant to iApp
-    Services, but the API mentioned here is the one that is used.
+    the one the is used is C(/mgmt/tm/sys/application/service/).
 version_added: 2.4
 options:
   name:
@@ -32,13 +30,13 @@ options:
     description:
       - The iApp template from which to instantiate a new service. This
         template must exist on your BIG-IP before you can successfully
-        create a service. This parameter is required if the C(state)
-        parameter is C(present).
+        create a service.
+      - When creating a new service, this parameter is required.
   parameters:
     description:
       - A hash of all the required template variables for the iApp template.
         If your parameters are stored in a file (the more common scenario)
-        it is recommended you use either the `file` or `template` lookups
+        it is recommended you use either the C(file) or C(template) lookups
         to supply the expected parameters.
       - These parameters typically consist of the C(lists), C(tables), and
         C(variables) fields.
@@ -74,7 +72,7 @@ options:
         iApp.
       - When C(no), allows updates outside of the iApp.
       - If this option is specified in the Ansible task, it will take precedence
-        over any similar setting in the iApp Server payload that you provide in
+        over any similar setting in the iApp Service payload that you provide in
         the C(parameters) field.
     default: yes
     type: bool
@@ -85,9 +83,30 @@ options:
         this value is not specified, the default of C(/Common/traffic-group-1)
         will be used.
       - If this option is specified in the Ansible task, it will take precedence
-        over any similar setting in the iApp Server payload that you provide in
+        over any similar setting in the iApp Service payload that you provide in
         the C(parameters) field.
     version_added: 2.5
+  metadata:
+    description:
+      - Metadata associated with the iApp service.
+      - If this option is specified in the Ansible task, it will take precedence
+        over any similar setting in the iApp Service payload that you provide in
+        the C(parameters) field.
+    version_added: 2.7
+  description:
+    description:
+      - Description of the iApp service.
+      - If this option is specified in the Ansible task, it will take precedence
+        over any similar setting in the iApp Service payload that you provide in
+        the C(parameters) field.
+    version_added: 2.7
+  device_group:
+    description:
+      - The device group for the iApp service.
+      - If this option is specified in the Ansible task, it will take precedence
+        over any similar setting in the iApp Service payload that you provide in
+        the C(parameters) field.
+    version_added: 2.7
 extends_documentation_fragment: f5
 author:
   - Tim Rupp (@caphrim007)
@@ -210,6 +229,22 @@ EXAMPLES = r'''
                 - 0
         - name: server_pools__servers
   delegate_to: localhost
+
+- name: Override metadata that may or may not exist in parameters
+  bigip_iapp_service:
+    name: foo-service
+    template: f5.http
+    parameters: "{{ lookup('file', 'f5.http.parameters.json') }}"
+    metadata:
+      - persist: yes
+        name: data 1
+      - persist: yes
+        name: data 2
+    password: secret
+    server: lb.mydomain.com
+    state: present
+    user: admin
+  delegate_to: localhost
 '''
 
 RETURN = r'''
@@ -228,6 +263,7 @@ try:
     from library.module_utils.network.f5.common import cleanup_tokens
     from library.module_utils.network.f5.common import fq_name
     from library.module_utils.network.f5.common import f5_argument_spec
+    from library.module_utils.network.f5.common import flatten_boolean
     try:
         from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
     except ImportError:
@@ -240,6 +276,7 @@ except ImportError:
     from ansible.module_utils.network.f5.common import cleanup_tokens
     from ansible.module_utils.network.f5.common import fq_name
     from ansible.module_utils.network.f5.common import f5_argument_spec
+    from ansible.module_utils.network.f5.common import flatten_boolean
     try:
         from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
     except ImportError:
@@ -250,17 +287,45 @@ class Parameters(AnsibleF5Parameters):
     api_map = {
         'strictUpdates': 'strict_updates',
         'trafficGroup': 'traffic_group',
+        'deviceGroup': 'device_group',
     }
 
-    returnables = []
-
-    api_attributes = [
-        'tables', 'variables', 'template', 'lists', 'deviceGroup',
-        'inheritedDevicegroup', 'inheritedTrafficGroup', 'trafficGroup',
-        'strictUpdates'
+    returnables = [
+        'tables',
+        'variables',
+        'lists',
+        'strict_updates',
+        'traffic_group',
+        'device_group',
+        'metadata',
+        'template',
+        'description',
     ]
 
-    updatables = ['tables', 'variables', 'lists', 'strict_updates', 'traffic_group']
+    api_attributes = [
+        'tables',
+        'variables',
+        'template',
+        'lists',
+        'deviceGroup',
+        'inheritedDevicegroup',
+        'inheritedTrafficGroup',
+        'trafficGroup',
+        'strictUpdates',
+        # 'metadata',
+        'description',
+    ]
+
+    updatables = [
+        'tables',
+        'variables',
+        'lists',
+        'strict_updates',
+        'device_group',
+        'traffic_group',
+        'metadata',
+        'description',
+    ]
 
     def to_return(self):
         result = {}
@@ -269,12 +334,8 @@ class Parameters(AnsibleF5Parameters):
         result = self._filter_params(result)
         return result
 
-    @property
-    def tables(self):
+    def normalize_tables(self, tables):
         result = []
-        if not self._values['tables']:
-            return None
-        tables = self._values['tables']
         for table in tables:
             tmp = dict()
             name = table.get('name', None)
@@ -296,16 +357,8 @@ class Parameters(AnsibleF5Parameters):
         result = sorted(result, key=lambda k: k['name'])
         return result
 
-    @tables.setter
-    def tables(self, value):
-        self._values['tables'] = value
-
-    @property
-    def variables(self):
+    def normalize_variables(self, variables):
         result = []
-        if not self._values['variables']:
-            return None
-        variables = self._values['variables']
         for variable in variables:
             tmp = dict((str(k), str(v)) for k, v in iteritems(variable))
             if 'encrypted' not in tmp:
@@ -323,16 +376,8 @@ class Parameters(AnsibleF5Parameters):
         result = sorted(result, key=lambda k: k['name'])
         return result
 
-    @variables.setter
-    def variables(self, value):
-        self._values['variables'] = value
-
-    @property
-    def lists(self):
+    def normalize_list(self, lists):
         result = []
-        if not self._values['lists']:
-            return None
-        lists = self._values['lists']
         for list in lists:
             tmp = dict((str(k), str(v)) for k, v in iteritems(list) if k != 'value')
             if 'encrypted' not in list:
@@ -349,39 +394,148 @@ class Parameters(AnsibleF5Parameters):
         result = sorted(result, key=lambda k: k['name'])
         return result
 
-    @lists.setter
-    def lists(self, value):
-        self._values['lists'] = value
-
-    @property
-    def parameters(self):
-        result = dict(
-            tables=self.tables,
-            variables=self.variables,
-            lists=self.lists
-        )
+    def normalize_metadata(self, metadata):
+        result = []
+        for item in metadata:
+            name = item.get('name', None)
+            persist = flatten_boolean(item.get('persist', "no"))
+            if persist == "yes":
+                persist = "true"
+            else:
+                persist = "false"
+            result.append({
+                "name": name,
+                "persist": persist
+            })
         return result
 
-    @parameters.setter
-    def parameters(self, value):
-        if value is None:
-            return
-        if 'tables' in value:
-            self.tables = value['tables']
-        if 'variables' in value:
-            self.variables = value['variables']
-        if 'lists' in value:
-            self.lists = value['lists']
-        if 'deviceGroup' in value:
-            self.deviceGroup = value['deviceGroup']
-        if 'inheritedDevicegroup' in value:
-            self.inheritedDevicegroup = value['inheritedDevicegroup']
-        if 'inheritedTrafficGroup' in value:
-            self.inheritedTrafficGroup = value['inheritedTrafficGroup']
-        if 'trafficGroup' in value:
-            self.trafficGroup = value['trafficGroup']
-        if 'strictUpdates' in value:
-            self.strictUpdates = value['strictUpdates']
+
+class ApiParameters(Parameters):
+    @property
+    def metadata(self):
+        if self._values['metadata'] is None:
+            return None
+        return self._values['metadata']
+
+    @property
+    def tables(self):
+        if self._values['tables'] is None:
+            return None
+        return self.normalize_tables(self._values['tables'])
+
+    @property
+    def lists(self):
+        if self._values['lists'] is None:
+            return None
+        return self.normalize_list(self._values['lists'])
+
+    @property
+    def variables(self):
+        if self._values['variables'] is None:
+            return None
+        return self.normalize_variables(self._values['variables'])
+
+    @property
+    def device_group(self):
+        if self._values['device_group'] in [None, 'none']:
+            return None
+        return self._values['device_group']
+
+
+class ModuleParameters(Parameters):
+    @property
+    def param_lists(self):
+        if self._values['parameters'] is None:
+            return None
+        result = self._values['parameters'].get('lists', None)
+        return result
+
+    @property
+    def param_tables(self):
+        if self._values['parameters'] is None:
+            return None
+        result = self._values['parameters'].get('tables', None)
+        return result
+
+    @property
+    def param_variables(self):
+        if self._values['parameters'] is None:
+            return None
+        result = self._values['parameters'].get('variables', None)
+        return result
+
+    @property
+    def param_metadata(self):
+        if self._values['parameters'] is None:
+            return None
+        result = self._values['parameters'].get('metadata', None)
+        return result
+
+    @property
+    def param_description(self):
+        if self._values['parameters'] is None:
+            return None
+        result = self._values['parameters'].get('description', None)
+        return result
+
+    @property
+    def param_traffic_group(self):
+        if self._values['parameters'] is None:
+            return None
+        result = self._values['parameters'].get('trafficGroup', None)
+        if not result:
+            return result
+        return fq_name(self.partition, result)
+
+    @property
+    def param_device_group(self):
+        if self._values['parameters'] is None:
+            return None
+        result = self._values['parameters'].get('deviceGroup', None)
+        if not result:
+            return result
+        return fq_name(self.partition, result)
+
+    @property
+    def param_strict_updates(self):
+        if self._values['parameters'] is None:
+            return None
+        result = self._values['parameters'].get('strictUpdates', None)
+        return flatten_boolean(result)
+
+    @property
+    def tables(self):
+        if self._values['tables']:
+            return self.normalize_tables(self._values['tables'])
+        elif self.param_tables:
+            return self.normalize_tables(self.param_tables)
+        return None
+
+    @property
+    def lists(self):
+        if self._values['lists']:
+            return self.normalize_list(self._values['lists'])
+        elif self.param_lists:
+            return self.normalize_list(self.param_lists)
+        return None
+
+    @property
+    def variables(self):
+        if self._values['variables']:
+            return self.normalize_variables(self._values['variables'])
+        elif self.param_variables:
+            return self.normalize_variables(self.param_variables)
+        return None
+
+    @property
+    def metadata(self):
+        if self._values['metadata']:
+            result = self.normalize_metadata(self._values['metadata'])
+        elif self.param_metadata:
+            result = self.normalize_metadata(self.param_metadata)
+        else:
+            return None
+        return result
 
     @property
     def template(self):
@@ -389,55 +543,67 @@ class Parameters(AnsibleF5Parameters):
             return None
         return fq_name(self.partition, self._values['template'])
 
-    @template.setter
-    def template(self, value):
-        self._values['template'] = value
-
     @property
-    def strict_updates(self):
-        if self._values['strict_updates'] is None and self.strictUpdates is None:
-            return None
-
-        # Specifying the value overrides any associated value in the payload
-        elif self._values['strict_updates'] is True:
-            return 'enabled'
-        elif self._values['strict_updates'] is False:
-            return 'disabled'
-
-        # This will be automatically `None` if it was not set by the
-        # `parameters` setter
-        elif self.strictUpdates:
-            return self.strictUpdates
+    def device_group(self):
+        if self._values['device_group'] not in [None, 'none']:
+            result = fq_name(self.partition, self._values['device_group'])
+        elif self.param_device_group not in [None, 'none']:
+            result = self.param_device_group
         else:
-            return self._values['strict_updates']
+            return None
+        if not result.startswith('/Common/'):
+            raise F5ModuleError(
+                "Device groups can only exist in /Common"
+            )
+        return result
 
     @property
     def traffic_group(self):
-        if self._values['traffic_group'] is None and self.trafficGroup is None:
+        if self._values['traffic_group']:
+            result = fq_name(self.partition, self._values['traffic_group'])
+        elif self.param_traffic_group:
+            result = self.param_traffic_group
+        else:
             return None
-
-        # Specifying the value overrides any associated value in the payload
-        elif self._values['traffic_group']:
-            result = fq_name(self.partition, self._values['traffic_group'])
-
-        # This will be automatically `None` if it was not set by the
-        # `parameters` setter
-        elif self.trafficGroup:
-            result = fq_name(self.partition, self.trafficGroup)
-        else:
-            result = fq_name(self.partition, self._values['traffic_group'])
-        if result.startswith('/Common/'):
-            return result
-        else:
+        if not result.startswith('/Common/'):
             raise F5ModuleError(
                 "Traffic groups can only exist in /Common"
             )
+        return result
+
+    @property
+    def strict_updates(self):
+        if self._values['strict_updates'] is not None:
+            result = flatten_boolean(self._values['strict_updates'])
+        elif self.param_strict_updates is not None:
+            result = self.param_strict_updates
+        else:
+            return None
+        if result == 'yes':
+            return 'enabled'
+        return 'disabled'
+
+    @property
+    def description(self):
+        if self._values['description']:
+            return self._values['description']
+        elif self.param_description:
+            return self.param_description
+        return None
 
 
 class Changes(Parameters):
     pass
 
 
+class UsableChanges(Changes):
+    pass
+
+
+class ReportableChanges(Changes):
+    pass
+
+
 class Difference(object):
     def __init__(self, want, have=None):
         self.want = want
@@ -460,9 +626,17 @@ class Difference(object):
             return attr1
 
     @property
-    def traffic_group(self):
-        if self.want.traffic_group != self.have.traffic_group:
-            return self.want.traffic_group
+    def metadata(self):
+        if self.want.metadata is None:
+            return None
+        if self.have.metadata is None:
+            return self.want.metadata
+        want = [(k, v) for d in self.want.metadata for k, v in iteritems(d)]
+        have = [(k, v) for d in self.have.metadata for k, v in iteritems(d)]
+        if set(want) != set(have):
+            return dict(
+                metadata=self.want.metadata
+            )
 
 
 class ModuleManager(object):
@@ -470,8 +644,8 @@ class ModuleManager(object):
         self.module = kwargs.get('module', None)
         self.client = kwargs.get('client', None)
         self.have = None
-        self.want = Parameters(params=self.module.params)
-        self.changes = Changes()
+        self.want = ModuleParameters(params=self.module.params)
+        self.changes = UsableChanges()
 
     def _set_changed_options(self):
         changed = {}
@@ -479,7 +653,7 @@ class ModuleManager(object):
             if getattr(self.want, key) is not None:
                 changed[key] = getattr(self.want, key)
         if changed:
-            self.changes = Changes(params=changed)
+            self.changes = UsableChanges(params=changed)
 
     def _update_changed_options(self):
         diff = Difference(self.want, self.have)
@@ -495,7 +669,7 @@ class ModuleManager(object):
                 else:
                     changed[k] = change
         if changed:
-            self.changes = Changes(params=changed)
+            self.changes = UsableChanges(params=changed)
             return True
         return False
 
@@ -532,8 +706,12 @@ class ModuleManager(object):
 
     def create(self):
         self._set_changed_options()
-        if self.want.traffic_group is None and self.want.trafficGroup is None:
+        if self.want.traffic_group is None:
             self.want.update({'traffic_group': '/Common/traffic-group-1'})
+        if not self.template_exists():
+            raise F5ModuleError(
+                "The specified template does not exist in the provided partition."
+            )
         if self.module.check_mode:
             return True
         self.create_on_device()
@@ -555,13 +733,20 @@ class ModuleManager(object):
         return False
 
     def update_on_device(self):
-        params = self.want.api_params()
-        params['execute-action'] = 'definition'
+        params = self.changes.api_params()
         resource = self.client.api.tm.sys.application.services.service.load(
             name=self.want.name,
             partition=self.want.partition
         )
-        resource.update(**params)
+        if params:
+            params['execute-action'] = 'definition'
+            resource.update(**params)
+        if self.changes.metadata:
+            params = {'execute-action': 'definition'}
+            resource.update(
+                metadata=self.changes.metadata,
+                **params
+            )
 
     def read_current_from_device(self):
         result = self.client.api.tm.sys.application.services.service.load(
@@ -569,15 +754,26 @@ class ModuleManager(object):
             partition=self.want.partition
         ).to_dict()
         result.pop('_meta_data', None)
-        return Parameters(params=result)
+        return ApiParameters(params=result)
+
+    def template_exists(self):
+        name = fq_name(self.want.partition, self.want.template)
+        parts = name.split('/')
+        result = self.client.api.tm.sys.application.templates.template.exists(
+            name=parts[2],
+            partition=parts[1]
+        )
+        return result
 
     def create_on_device(self):
-        params = self.want.api_params()
-        self.client.api.tm.sys.application.services.service.create(
+        params = self.changes.api_params()
+        resource = self.client.api.tm.sys.application.services.service.create(
             name=self.want.name,
             partition=self.want.partition,
             **params
         )
+        if self.changes.metadata:
+            resource.update(metadata=self.changes.metadata)
 
     def absent(self):
         if self.exists():
@@ -598,6 +794,13 @@ class ModuleManager(object):
             partition=self.want.partition
         )
         if resource:
+            # Metadata needs to be zero'd before the service is removed because
+            # otherwise, the API will error out saying that "configuration items"
+            # currently exist.
+            #
+            # In other words, the REST API is not able to delete a service while
+            # there is existing metadata
+            resource.update(metadata=[])
             resource.delete()
 
 
@@ -607,6 +810,8 @@ class ArgumentSpec(object):
         argument_spec = dict(
             name=dict(required=True),
             template=dict(),
+            description=dict(),
+            device_group=dict(),
             parameters=dict(
                 type='dict'
             ),
@@ -622,6 +827,7 @@ class ArgumentSpec(object):
                 type='bool',
                 default='yes'
             ),
+            metadata=dict(type='list'),
             traffic_group=dict(),
             partition=dict(
                 default='Common',