diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..8a7407a55958ece3660f0494185ecf8f865945c4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.ssh/id_*
diff --git a/.ssh/README b/.ssh/README
new file mode 100644
index 0000000000000000000000000000000000000000..3c4542d266c9444b0f832702e3e2ad1d1f337041
--- /dev/null
+++ b/.ssh/README
@@ -0,0 +1 @@
+Add id_ansible and id_ansible.pub SSH keys
diff --git a/ansible.cfg b/ansible.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..c478a02f9a7cb7f56463cf5f73c93399b2be81e3
--- /dev/null
+++ b/ansible.cfg
@@ -0,0 +1,10 @@
+[defaults]
+gathering = smart
+remote_user = ansible
+
+[privilege_escalation]
+become = True
+
+[ssh_connection]
+pipelining = True
+ssh_args = -o ControlMaster=auto -o ControlPersist=3600s -o StrictHostKeyChecking=false -i .ssh/id_ansible
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000000000000000000000000000000000000..1b4d178ffb9b679990b78acf1f45510785f5cf52
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,2 @@
+#ansible-galaxy collection install community.general
+ansible-playbook playbook.yml -i inventory/inventory $@
diff --git a/files/GeoLite2-Country.mmdb b/files/GeoLite2-Country.mmdb
new file mode 100644
index 0000000000000000000000000000000000000000..fcc7618fad247a69a847639e4a93647deadc793a
Binary files /dev/null and b/files/GeoLite2-Country.mmdb differ
diff --git a/files/srv.mdx.incubator.geant.org.json b/files/srv.mdx.incubator.geant.org.json
new file mode 100644
index 0000000000000000000000000000000000000000..7e67288e2371445c3649187202dfd1480a0d2d83
--- /dev/null
+++ b/files/srv.mdx.incubator.geant.org.json
@@ -0,0 +1,94 @@
+{
+  "ttl": 60,
+  "max_hosts": 1,
+  "data": {
+    "": {
+      "a": [
+        [ "193.224.22.78", 10 ]
+      ]
+    },
+    "srv1": {
+      "a": [
+        [ "193.224.22.78", 10 ]
+      ]
+    },
+    "srv1-signer": {
+      "a": [
+        [ "193.224.22.78", 10 ]
+      ]
+    },
+    "srv1-proxy": {
+      "a": [
+        [ "193.224.22.78", 10 ]
+      ]
+    },
+    "srv2": {
+      "a": [
+        [ "145.100.180.185", 10 ]
+      ]
+    },
+    "srv2-signer": {
+      "a": [
+        [ "145.100.180.185", 10 ]
+      ]
+    },
+    "srv2-proxy": {
+      "a": [
+        [ "145.100.180.185", 10 ]
+      ]
+    },
+    "srv3": {
+      "a": [
+        [ "62.217.72.109", 10 ]
+      ]
+    },
+    "srv3-signer": {
+      "a": [
+        [ "62.217.72.109", 10 ]
+      ]
+    },
+    "srv3-proxy": {
+      "a": [
+        [ "62.217.72.109", 10 ]
+      ]
+    },
+    "signer": {
+      "a": [
+        [ "193.224.22.78", 10 ],
+        [ "145.100.180.185", 10 ],
+        [ "62.217.72.109", 10 ]
+      ]
+    },
+    "signer.nl": {
+      "a": [
+        [ "145.100.180.185", 10 ]
+      ]
+    },
+    "proxy": {
+      "a": [
+        [ "193.224.22.78", 10 ],
+        [ "145.100.180.185", 10 ],
+        [ "62.217.72.109", 10 ]
+      ]
+    },
+    "proxy-eg": {
+      "a": [
+        [ "193.224.22.78", 10 ],
+        [ "145.100.180.185", 10 ],
+        [ "62.217.72.109", 10 ]
+      ]
+    },
+    "proxy-tst": {
+      "a": [
+        [ "193.224.22.78", 10 ],
+        [ "145.100.180.185", 10 ],
+        [ "62.217.72.109", 10 ]
+      ]
+    },
+    "proxy.nl": {
+      "a": [
+        [ "62.217.72.109", 10 ]
+      ]
+    }
+  }
+}
diff --git a/playbook.yml b/playbook.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f89ab33a16bfb2828b5f4e8d0cc0d176c603e126
--- /dev/null
+++ b/playbook.yml
@@ -0,0 +1,25 @@
+---
+- hosts: all
+  gather_facts: true
+  roles:
+    - {role: base,      tage: ['base']}
+
+- name: geoDNS
+  hosts: geodns
+  gather_facts: false
+  roles:
+    - {role: geodns,    tags: ['geodns']}
+
+- name: MDServer
+  hosts: mdserver
+  gather_facts: false
+  roles:
+    - {role: apache,    tags: ['apache']}
+    - {role: mdserver,  tags: ['mdserver']}
+
+- name: MDProxy
+  hosts: mdproxy
+  gather_facts: false
+  roles:
+    - {role: apache,    tags: ['apache']}
+    - {role: mdproxy,   tags: ['mdserver']}
diff --git a/requirements.yml b/requirements.yml
new file mode 100644
index 0000000000000000000000000000000000000000..152daf06fcf380be8552b0c7277ca26ef631d095
--- /dev/null
+++ b/requirements.yml
@@ -0,0 +1,6 @@
+---
+# Install using
+# ansible-galaxy collection install -r requirements.yml
+
+collections:
+- name: community.general
diff --git a/roles/base/tasks/main.yml b/roles/base/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6298c1554c9d158477342c2fa9965f9e5a843e0d
--- /dev/null
+++ b/roles/base/tasks/main.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Install packages
+  apt:
+    state: present
+    name:
+      - git
diff --git a/roles/geodns/defaults/main.yml b/roles/geodns/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3e4b6044db1c1cad2908df73b603541bf29cf806
--- /dev/null
+++ b/roles/geodns/defaults/main.yml
@@ -0,0 +1,10 @@
+---
+
+go_link: https://go.dev/dl/go1.16.13.linux-amd64.tar.gz
+
+geodns_repo: https://github.com/abh/geodns.git
+geodns_dir: /opt/geodns
+geo_dns_version: v3.2.0
+geo_dns_config: "{{ geodns_dir}}/config"
+
+geolite_dir: "{{ geodns_dir }}/GeoLite2DB"
diff --git a/roles/geodns/handlers/main.yml b/roles/geodns/handlers/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a5ec62ef854354062ef4bf3776f654061c4b70cd
--- /dev/null
+++ b/roles/geodns/handlers/main.yml
@@ -0,0 +1,7 @@
+---
+- name: enable geodns job
+  systemd:
+    name: "geodns.service"
+    enabled: true
+    state: "restarted"
+    daemon_reload: true
diff --git a/roles/geodns/tasks/main.yml b/roles/geodns/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1e9080a9e859f43ddc4eb1d92056b68e560724fe
--- /dev/null
+++ b/roles/geodns/tasks/main.yml
@@ -0,0 +1,74 @@
+---
+
+- name: Check if go binary exists
+  stat:
+    path: "/opt/go/bin/go"
+  register: go
+
+- name: Download Go
+  ansible.builtin.unarchive:
+    src: "{{ go_link }}"
+    dest: /opt/
+    remote_src: yes
+  when: not go.stat.exists
+
+- name: Clone geoDNS repository
+  ansible.builtin.git:
+    repo: "{{ geodns_repo }}"
+    dest: "{{ geodns_dir }}"
+    version: "{{ geo_dns_version }}"
+  register: geodns_git
+
+- name: Check if geodns binary exists
+  stat:
+    path: "{{ geodns_dir }}/geodns"
+  register: geodns
+
+- name: Build geoDNS
+  ansible.builtin.command:
+    cmd: "/opt/go/bin/go build"
+    chdir: "{{ geodns_dir }}"
+  when: geodns_git.changed or not geodns.stat.exists
+  notify:
+    - "enable geodns job"
+
+- name: Create config dirs if it does not exist
+  ansible.builtin.file:
+    path: "{{ item }}"
+    state: directory
+    mode: '0755'
+  with_items:
+    - "{{ geo_dns_config }}"
+    - "{{ geolite_dir }}"
+
+- name: Copy geoDNS config
+  ansible.builtin.copy:
+    src: "srv.mdx.incubator.geant.org.json"
+    dest: "{{ geo_dns_config }}"
+    mode: '0644'
+  notify:
+    - "enable geodns job"
+
+- name: Copy GeoLite2DB's
+  ansible.builtin.copy:
+    src: "{{ item }}"
+    dest: "{{ geolite_dir }}"
+    mode: '0644'
+  with_items:
+    - GeoLite2-Country.mmdb
+  notify:
+    - "enable geodns job"
+
+- name: Create geoDNS config
+  ansible.builtin.template:
+    src: "geodns.conf.j2"
+    dest: "{{ geo_dns_config }}/geodns.conf"
+  notify:
+    - "enable geodns job"
+
+- name: Copy geoDNS service files
+  ansible.builtin.template:
+    src: "geodns.service.j2"
+    dest: "/etc/systemd/system/geodns.service"
+  notify:
+    - "enable geodns job"
diff --git a/roles/geodns/templates/geodns.conf.j2 b/roles/geodns/templates/geodns.conf.j2
new file mode 100644
index 0000000000000000000000000000000000000000..d16067409b7e0da0c1cbfbc4e365b6fdd41525fe
--- /dev/null
+++ b/roles/geodns/templates/geodns.conf.j2
@@ -0,0 +1,26 @@
+; GeoDNS configuration file
+;
+; It is recommended to distribute the configuration file globally
+; with your .json zone files.
+
+[geoip]
+;; Directory containing the GeoIP2 .mmdb database files; defaults
+;; to looking through a list of common directories looking for one
+;; of those that exists.
+directory={{ geolite_dir }}
+
+[querylog]
+;; directory to save query logs; disabled if not specified
+path = log/queries.log
+;; max size per file in megabytes before rotating (default 200)
+; maxsize = 100
+;; keep up to this many rotated log files (default 1)
+; keep = 2
+
+[http]
+; require basic HTTP authentication; not encrypted or safe over the public internet
+; user = stats
+; password = Aeteereun8eoth4
+
+[health]
+; directory = dns/health
diff --git a/roles/geodns/templates/geodns.service.j2 b/roles/geodns/templates/geodns.service.j2
new file mode 100644
index 0000000000000000000000000000000000000000..ede7f8c597f00e1e9c778a317d7dacd3fbc24852
--- /dev/null
+++ b/roles/geodns/templates/geodns.service.j2
@@ -0,0 +1,15 @@
+[Unit]
+Description=GeoDNS server
+After=syslog.target network.target
+
+[Service]
+Type=simple
+WorkingDirectory={{ geodns_dir }}
+ExecStart=/opt/geodns/geodns -config={{ geo_dns_config }} -log -interface 0.0.0.0 -port 53
+ExecReload=/bin/kill -HUP $MAINPID
+Restart=on-failure
+RestartSec=10
+SyslogIdentifier=geodns
+
+[Install]
+WantedBy=multi-user.target