diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fa73f4a433c2cdd232f90397421b1e6fcdd90f31
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,22 @@
+trigger:
+- master
+
+pool:
+  vmImage: 'ubuntu-18.04'
+
+steps:
+- task: UsePythonVersion@0
+  inputs:
+    versionSpec: '3.6'
+
+- script: python -m pip install --upgrade pip setuptools wheel
+  displayName: Install tools
+
+- script: pip install pydocstyle flake8
+  displayName: Install dependencies
+
+- script: flake8 .
+  displayName: Run flake8 checks
+
+- script: pydocstyle .
+  displayName: Verify docstings
diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py
index 9e3254a3cb50df626071e10039e9ef3b58ae02e2..7c2956281b7b461c485d4af593c4eff3b39ac25d 100644
--- a/plugins/module_utils/ansible_freeipa_module.py
+++ b/plugins/module_utils/ansible_freeipa_module.py
@@ -63,9 +63,7 @@ if six.PY3:
 
 
 def valid_creds(module, principal):  # noqa
-    """
-    Get valid credintials matching the princial, try GSSAPI first
-    """
+    """Get valid credentials matching the princial, try GSSAPI first."""
     if "KRB5CCNAME" in os.environ:
         ccache = os.environ["KRB5CCNAME"]
         module.debug('KRB5CCNAME set to %s' % ccache)
@@ -103,9 +101,7 @@ def valid_creds(module, principal):  # noqa
 
 
 def temp_kinit(principal, password):
-    """
-    kinit with password using a temporary ccache
-    """
+    """Kinit with password using a temporary ccache."""
     if not password:
         raise RuntimeError("The password is not set")
     if not principal:
@@ -123,9 +119,7 @@ def temp_kinit(principal, password):
 
 
 def temp_kdestroy(ccache_dir, ccache_name):
-    """
-    Destroy temporary ticket and remove temporary ccache
-    """
+    """Destroy temporary ticket and remove temporary ccache."""
     if ccache_name is not None:
         run([paths.KDESTROY, '-c', ccache_name], raiseonerr=False)
     if ccache_dir is not None:
@@ -134,7 +128,12 @@ def temp_kdestroy(ccache_dir, ccache_name):
 
 def api_connect(context=None):
     """
-    Create environment, initialize api and connect to ldap2
+    Initialize IPA API with the provided context.
+
+    `context` can be any of:
+        * `server` (default)
+        * `ansible-freeipa`
+        * `cli_installer`
     """
     env = Env()
     env._bootstrap()
@@ -157,28 +156,24 @@ def api_connect(context=None):
 
 
 def api_command(module, command, name, args):
-    """
-    Call ipa.Command
-    """
+    """Call ipa.Command."""
     return api.Command[command](name, **args)
 
 
 def api_command_no_name(module, command, args):
-    """
-    Call ipa.Command without a name.
-    """
+    """Call ipa.Command without a name."""
     return api.Command[command](**args)
 
 
 def api_check_param(command, name):
-    """
-    Return if param exists in command param list
-    """
+    """Check if param exists in command param list."""
     return name in api.Command[command].params
 
 
 def execute_api_command(module, principal, password, command, name, args):
     """
+    Execute an API command.
+
     Get KRB ticket if not already there, initialize api, connect,
     execute command and destroy ticket again if it has been created also.
     """
@@ -300,10 +295,7 @@ def api_get_realm():
 
 
 def gen_add_del_lists(user_list, res_list):
-    """
-    Generate the lists for the addition and removal of members using the
-    provided user and ipa settings
-    """
+    """Generate the lists for the addition and removal of members."""
     add_list = list(set(user_list or []) - set(res_list or []))
     del_list = list(set(res_list or []) - set(user_list or []))
 
@@ -312,8 +304,9 @@ def gen_add_del_lists(user_list, res_list):
 
 def encode_certificate(cert):
     """
-    Encode a certificate using base64 with also taking FreeIPA and Python
-    versions into account
+    Encode a certificate using base64.
+
+    It also takes FreeIPA and Python versions into account.
     """
     if isinstance(cert, (str, unicode, bytes)):
         encoded = base64.b64encode(cert)
@@ -335,9 +328,7 @@ def is_valid_port(port):
 
 
 def is_ipv4_addr(ipaddr):
-    """
-    Test if figen IP address is a valid IPv4 address
-    """
+    """Test if given IP address is a valid IPv4 address."""
     try:
         socket.inet_pton(socket.AF_INET, ipaddr)
     except socket.error:
@@ -346,9 +337,7 @@ def is_ipv4_addr(ipaddr):
 
 
 def is_ipv6_addr(ipaddr):
-    """
-    Test if figen IP address is a valid IPv6 address
-    """
+    """Test if given IP address is a valid IPv6 address."""
     try:
         socket.inet_pton(socket.AF_INET6, ipaddr)
     except socket.error:
diff --git a/plugins/modules/ipadnsconfig.py b/plugins/modules/ipadnsconfig.py
index 4c9cf2d71a4652737adcf678bfc486d8b97dc465..0d4aa8764fc2c9c1b6a97acf21fb24b2a8e2ef20 100644
--- a/plugins/modules/ipadnsconfig.py
+++ b/plugins/modules/ipadnsconfig.py
@@ -97,11 +97,10 @@ RETURN = """
 """
 
 from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils._text import to_text
 from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
-    temp_kdestroy, valid_creds, api_connect, api_command, \
+    temp_kdestroy, valid_creds, api_connect, \
     api_command_no_name, compare_args_ipa, module_params_get, \
-    gen_add_del_lists, is_ipv4_addr, is_ipv6_addr, ipalib_errors
+    is_ipv4_addr, is_ipv6_addr
 
 
 def find_dnsconfig(module):
diff --git a/plugins/modules/ipagroup.py b/plugins/modules/ipagroup.py
index 477c505185d0cfbe0b307110b2feaf1f04ee1775..10a611441894971643f42ef36ea271bb171656b7 100644
--- a/plugins/modules/ipagroup.py
+++ b/plugins/modules/ipagroup.py
@@ -221,7 +221,10 @@ def main():
     # Get parameters
 
     # general
-    ipaadmin_principal = module_params_get(ansible_module, "ipaadmin_principal")
+    ipaadmin_principal = module_params_get(
+        ansible_module,
+        "ipaadmin_principal",
+    )
     ipaadmin_password = module_params_get(ansible_module, "ipaadmin_password")
     names = module_params_get(ansible_module, "name")
 
diff --git a/plugins/modules/ipahost.py b/plugins/modules/ipahost.py
index 9b22f5bf69f0423df9084bc069c41d4bbf47f73d..42bd7e1faf6fbb1f6f246ae72a4c623839e88ee9 100644
--- a/plugins/modules/ipahost.py
+++ b/plugins/modules/ipahost.py
@@ -875,9 +875,11 @@ def main():
                 res_find_dnsrecord = find_dnsrecord(ansible_module, name)
             except ipalib_errors.NotFound as e:
                 msg = str(e)
-                if ip_address is None and \
-                   ("DNS is not configured" in msg or \
-                    "DNS zone not found" in msg):
+                dns_not_configured = "DNS is not configured" in msg
+                dns_zone_not_found = "DNS zone not found" in msg
+                if ip_address is None and (
+                    dns_not_configured or dns_zone_not_found
+                ):
                     # IP address(es) not given and no DNS support in IPA
                     # -> Ignore failure
                     # IP address(es) not given and DNS zone is not found
diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py
index 73f16eff3edaf0d35770d414c5f2a6c42235a5d5..1867ea7be45313826e1caaf5e49df5f5527149cb 100644
--- a/plugins/modules/ipauser.py
+++ b/plugins/modules/ipauser.py
@@ -655,9 +655,9 @@ def check_parameters(module, state, action,
 
 def extend_emails(email, default_email_domain):
     if email is not None:
-        return [ "%s@%s" % (_email, default_email_domain)
-                 if "@" not in _email else _email
-                 for _email in email]
+        return ["%s@%s" % (_email, default_email_domain)
+                if "@" not in _email else _email
+                for _email in email]
     return email
 
 
diff --git a/roles/ipaclient/action_plugins/ipaclient_get_otp.py b/roles/ipaclient/action_plugins/ipaclient_get_otp.py
index b7e8056751a5ea3bc715b011c5cb66d907686aab..dcddc0aedcd97ae19921b8cac287e33f3b6f1545 100644
--- a/roles/ipaclient/action_plugins/ipaclient_get_otp.py
+++ b/roles/ipaclient/action_plugins/ipaclient_get_otp.py
@@ -33,9 +33,7 @@ from ansible.plugins.action import ActionBase
 
 
 def run_cmd(args, stdin=None):
-    """
-    Execute an external command.
-    """
+    """Execute an external command."""
     p_in = None
     p_out = subprocess.PIPE
     p_err = subprocess.PIPE
@@ -53,8 +51,10 @@ def run_cmd(args, stdin=None):
 
 def kinit_password(principal, password, ccache_name, config):
     """
-    Perform kinit using principal/password, with the specified config file
-    and store the TGT in ccache_name.
+    Perform kinit using principal/password.
+
+    It uses the specified config file to kinit and stores the TGT
+    in ccache_name.
     """
     args = ["/usr/bin/kinit", principal, '-c', ccache_name]
     old_config = os.environ.get('KRB5_CONFIG')
@@ -71,8 +71,10 @@ def kinit_password(principal, password, ccache_name, config):
 
 def kinit_keytab(principal, keytab, ccache_name, config):
     """
-    Perform kinit using principal/keytab, with the specified config file
-    and store the TGT in ccache_name.
+    Perform kinit using principal/keytab.
+
+    It uses the specified config file to kinit and stores the TGT
+    in ccache_name.
     """
     if gssapi is None:
         raise ImportError("gssapi is not available")
@@ -126,7 +128,7 @@ class ActionModule(ActionBase):
 
     def run(self, tmp=None, task_vars=None):
         """
-        handler for credential cache transfer
+        Handle credential cache transfer.
 
         ipa* commands can either provide a password or a keytab file
         in order to authenticate on the managed node with Kerberos.
@@ -142,7 +144,6 @@ class ActionModule(ActionBase):
 
         Then the IPA commands can use this credential cache file.
         """
-
         if task_vars is None:
             task_vars = dict()
 
diff --git a/roles/ipaclient/files/py3test.py b/roles/ipaclient/files/py3test.py
index d370b6870ab5a1be8e25ea7fd0abe4243f4fcd3d..6bf70ab588b48d69a82b624dd1053eb130e963e3 100644
--- a/roles/ipaclient/files/py3test.py
+++ b/roles/ipaclient/files/py3test.py
@@ -1,7 +1,7 @@
 #!/usr/bin/python3
 
 # Test ipaclient python3 binding
-from ipaclient.install.client import SECURE_PATH
+from ipaclient.install.client import SECURE_PATH  # noqa: F401
 
 # Check ipapython version to be >= 4.6
 from ipapython.version import NUM_VERSION, VERSION
diff --git a/roles/ipaclient/library/ipaclient_api.py b/roles/ipaclient/library/ipaclient_api.py
index 5ce722a907718196d611f028af2641081e1200d2..865438f3e3e9425039bf33acd5409551a5bb666e 100644
--- a/roles/ipaclient/library/ipaclient_api.py
+++ b/roles/ipaclient/library/ipaclient_api.py
@@ -100,7 +100,6 @@ def main():
 
     realm = module.params.get('realm')
     hostname = module.params.get('hostname')
-    servers = module.params.get('servers')
     debug = module.params.get('debug')
 
     host_principal = 'host/%s@%s' % (hostname, realm)
diff --git a/roles/ipaclient/library/ipaclient_get_facts.py b/roles/ipaclient/library/ipaclient_get_facts.py
index 1492199bf6a1e20a51c44d5434b1caf10ec8d219..003715efd3926c88437f99106daed5654150b976 100644
--- a/roles/ipaclient/library/ipaclient_get_facts.py
+++ b/roles/ipaclient/library/ipaclient_get_facts.py
@@ -13,7 +13,7 @@ from ansible.module_utils.basic import AnsibleModule
 
 # pylint: disable=unused-import
 try:
-    from ipalib import api
+    from ipalib import api  # noqa: F401
 except ImportError:
     HAS_IPALIB = False
 else:
@@ -27,7 +27,7 @@ else:
         from ipapython import sysrestore
 
 try:
-    import ipaserver
+    import ipaserver  # noqa: F401
 except ImportError:
     HAS_IPASERVER = False
 else:
@@ -41,7 +41,7 @@ VAR_LIB_PKI_TOMCAT = "/var/lib/pki/pki-tomcat"
 def is_ntpd_configured():
     # ntpd is configured when sysrestore.state contains the line
     # [ntpd]
-    ntpd_conf_section = re.compile('^\s*\[ntpd\]\s*$')
+    ntpd_conf_section = re.compile(r'^\s*\[ntpd\]\s*$')
 
     try:
         with open(SERVER_SYSRESTORE_STATE) as f:
@@ -56,7 +56,7 @@ def is_ntpd_configured():
 def is_dns_configured():
     # dns is configured when /etc/named.conf contains the line
     # dyndb "ipa" "/usr/lib64/bind/ldap.so" {
-    bind_conf_section = re.compile('^\s*dyndb\s+"ipa"\s+"[^"]+"\s+{$')
+    bind_conf_section = re.compile(r'^\s*dyndb\s+"ipa"\s+"[^"]+"\s+{$')
 
     try:
         with open(NAMED_CONF) as f:
diff --git a/roles/ipaclient/library/ipaclient_get_otp.py b/roles/ipaclient/library/ipaclient_get_otp.py
index 7ec67dda2d4bd91d6e5d931cbb359ff3a4411830..03e8b2b08cb18e80806bf7c12d1782835efdf9ff 100644
--- a/roles/ipaclient/library/ipaclient_get_otp.py
+++ b/roles/ipaclient/library/ipaclient_get_otp.py
@@ -135,8 +135,7 @@ if six.PY3:
 
 def get_host_diff(ipa_host, module_host):
     """
-    Compares two dictionaries containing host attributes and builds a dict
-    of differences.
+    Build a dict with the differences from two host dicts.
 
     :param ipa_host: the host structure seen from IPA
     :param module_host: the target host structure seen from the module params
@@ -164,7 +163,7 @@ def get_host_diff(ipa_host, module_host):
 
 def get_module_host(module):
     """
-    Creates a structure representing the host information
+    Create a structure representing the host information.
 
     Reads the module parameters and builds the host structure as expected from
     the module
@@ -189,7 +188,7 @@ def get_module_host(module):
 
 def ensure_host_present(module, api, ipahost):
     """
-    Ensures that the host exists in IPA and has the same attributes.
+    Ensure host exists in IPA and has the same attributes.
 
     :param module: the ansible module
     :param api: IPA api handle
@@ -246,7 +245,7 @@ def ensure_host_present(module, api, ipahost):
 
 def ensure_host_absent(module, api, host):
     """
-    Ensures that the host does not exist in IPA
+    Ensure host does not exist in IPA.
 
     :param module: the ansible module
     :param api: the IPA API handle
@@ -271,9 +270,7 @@ def ensure_host_absent(module, api, host):
 
 
 def main():
-    """
-    Main routine for the ansible module.
-    """
+
     module = AnsibleModule(
         argument_spec=dict(
             principal=dict(default='admin'),
@@ -288,7 +285,6 @@ def main():
         supports_check_mode=True,
     )
 
-    principal = module.params.get('principal', 'admin')
     ccache = module.params.get('ccache')
     fqdn = unicode(module.params.get('fqdn'))
     state = module.params.get('state')
diff --git a/roles/ipaclient/library/ipaclient_test.py b/roles/ipaclient/library/ipaclient_test.py
index 8e1d11cf24d70967abf2fbc6da351f50d57274cb..d5d7f7187941dff8e34ad8d7ba5298eaf478ad8a 100644
--- a/roles/ipaclient/library/ipaclient_test.py
+++ b/roles/ipaclient/library/ipaclient_test.py
@@ -235,7 +235,6 @@ def is_client_configured():
 
     :returns: boolean
     """
-
     return (os.path.isfile(paths.IPA_DEFAULT_CONF) and
             os.path.isfile(os.path.join(paths.IPA_CLIENT_SYSRESTORE,
                                         sysrestore.SYSRESTORE_STATEFILE)))
@@ -243,11 +242,10 @@ def is_client_configured():
 
 def get_ipa_conf():
     """
-    Return IPA configuration read from /etc/ipa/default.conf
+    Return IPA configuration read from `/etc/ipa/default.conf`.
 
     :returns: dict containing key,value
     """
-
     parser = RawConfigParser()
     parser.read(paths.IPA_DEFAULT_CONF)
     result = dict()
diff --git a/roles/ipareplica/files/py3test.py b/roles/ipareplica/files/py3test.py
index 8c60ded45310e55f903a564a5f8ba541fc136b7e..ffb009cd498fc5d17f346d4a7db9eb4327663fb7 100644
--- a/roles/ipareplica/files/py3test.py
+++ b/roles/ipareplica/files/py3test.py
@@ -2,9 +2,13 @@
 
 # Test ipaerver python3 binding
 try:
-    from ipaserver.install.server.replicainstall import install_check
+    from ipaserver.install.server.replicainstall import (  # noqa: F401
+        install_check,
+    )
 except ImportError:
-    from ipaserver.install.server.replicainstall import promote_check
+    from ipaserver.install.server.replicainstall import (  # noqa: F401
+        promote_check,
+    )
 
 # Check ipapython version to be >= 4.6
 from ipapython.version import NUM_VERSION, VERSION
diff --git a/roles/ipaserver/files/py3test.py b/roles/ipaserver/files/py3test.py
index 8f5c2d8516ea0f5bbe361c6b514e35b2833bfa63..701e342409ec1f13d17b5dcc5b4a83eb8d9695ac 100644
--- a/roles/ipaserver/files/py3test.py
+++ b/roles/ipaserver/files/py3test.py
@@ -1,7 +1,7 @@
 #!/usr/bin/python3
 
 # Test ipaerver python3 binding
-from ipaserver.install.server.install import install_check
+from ipaserver.install.server.install import install_check   # noqa: F401
 
 # Check ipapython version to be >= 4.6
 from ipapython.version import NUM_VERSION, VERSION
diff --git a/setup.cfg b/setup.cfg
index b254a8e4bb5effd6b55ba7402715523a232cb46c..d87f6f06d4666a885b32bdfc3808c793dc4b5f4b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,3 +20,13 @@ data_files =
     /usr/share/ansible/roles/ipaclient = roles/ipaclient/*
     /usr/share/ansible/roles/ipaserver = roles/ipaserver/*
     /usr/share/ansible/roles/ipareplica = roles/ipareplica/*
+
+[flake8]
+extend-ignore = E203
+per-file-ignores =
+    plugins/*:E402
+    roles/*:E402
+
+[pydocstyle]
+inherit = false
+ignore = D1,D212,D203