diff --git a/ansible/inventory.yml b/ansible/inventory.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5f9038a74f88518dc4d3ce0bf4b521cbbfa944e6
--- /dev/null
+++ b/ansible/inventory.yml
@@ -0,0 +1,31 @@
+---
+# Copyright 2024 David Araújo
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+nodes:
+  hosts:
+    hub:
+      ansible_host: 192.168.56.10
+      containerlab_host: true       # Mandatory
+      microk8s_master: true         # Mandatory
+      tfs_branch: develop           # Optional
+    spoke1:
+      ansible_host: 192.168.56.11
+    spoke2:
+      ansible_host: 192.168.56.12
+  vars:
+    ansible_user: vagrant
+    ansible_ssh_private_key_file: ~/.ssh/id_rsa
+    # Prevents from having to interact to approve a new remote host fingerprint
+    ansible_ssh_common_args: -o StrictHostKeyChecking=accept-new
diff --git a/ansible/playbook.yml b/ansible/playbook.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2c49a10bf362981edb784be146108c88349a08ed
--- /dev/null
+++ b/ansible/playbook.yml
@@ -0,0 +1,279 @@
+---
+# Copyright 2024 David Araújo
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+- name: TFS Cluster platform
+  hosts: nodes
+  become: true
+  remote_user: "{{ ansible_user }}"
+
+  tasks:
+    - name: Wait for /var/lib/dpkg/lock-frontend to be released
+      ansible.builtin.shell: while lsof /var/lib/dpkg/lock-frontend; do sleep 10; done
+      register: out
+      changed_when: out.rc != 0
+
+    - name: Configure all packages
+      ansible.builtin.command: dpkg --configure -a
+      register: out
+      changed_when: out.rc != 0
+
+    - name: Update repositories
+      ansible.builtin.apt:
+        update_cache: true
+
+    - name: Fix any broken packages
+      ansible.builtin.apt:
+        name: "*"
+        state: fixed
+
+    - name: Upgrade the OS (apt-get dist-upgrade)
+      ansible.builtin.apt:
+        upgrade: dist
+
+    - name: Install dependencies
+      ansible.builtin.apt:
+        pkg:
+          - ca-certificates
+          - curl
+          - gnupg
+          - lsb-release
+          - snapd
+          - jq
+          - docker.io
+          - docker-buildx
+        state: fixed
+        update_cache: true
+
+    - name: Update Docker daemon.json with insecure-registries
+      ansible.builtin.shell: |
+        if [ -s /etc/docker/daemon.json ]; then sudo cat /etc/docker/daemon.json; else echo '{}'; fi \
+          | jq 'if has("insecure-registries") then . else .+ {"insecure-registries": []} end' -- \
+          | jq '."insecure-registries" |= (.+ ["localhost:32000"] | unique)' -- \
+          | tee tmp.daemon.json
+      register: out
+      changed_when: out.rc != 0
+
+    - name: Copy tmp.daemon.json
+      ansible.builtin.copy:
+        remote_src: true
+        src: tmp.daemon.json
+        dest: /etc/docker/daemon.json
+        owner: root
+        group: root
+        mode: "600"
+
+    - name: Remove workdir tmp.daemon.json file
+      ansible.builtin.file:
+        path: tmp.daemon.json
+        state: absent
+
+    - name: Restart docker service
+      ansible.builtin.service:
+        name: docker
+        state: restarted
+
+    - name: Add address of all hosts to all hosts
+      ansible.builtin.lineinfile:
+        dest: /etc/hosts
+        regexp: .*{{ item }}$
+        line: "{{ hostvars[item].ansible_host }} {{ item }}"
+        state: present
+      when: hostvars[item].ansible_host is defined
+      with_items: "{{ groups.all }}"
+
+    - name: Find the Containerlab host IP
+      ansible.builtin.set_fact:
+        containerlab_host_address: "{{ hostvars[item].ansible_host }}"
+      when: hostvars[item].containerlab_host is defined
+      with_items: "{{ groups['all'] }}"
+      run_once: true
+
+    - name: Specify ip route to Containerlab network
+      ansible.builtin.shell: sudo ip route add 172.100.100.0/24 via {{ containerlab_host_address }}
+      when: hostvars[inventory_hostname].containerlab_host is not defined
+      register: out
+      changed_when: out.rc != 0
+
+    - name: Install Containerlab
+      ansible.builtin.shell: |
+        curl -sL https://containerlab.dev/setup \
+        | sudo bash -s "all"
+      register: out
+      changed_when: out.rc != 0
+      when: containerlab_host is defined
+
+    - name: Install gnmic
+      ansible.builtin.shell: |
+        curl -sL https://get-gnmic.kmrd.dev \
+        | sudo bash
+      register: out
+      changed_when: out.rc != 0
+
+    - name: Install MicroK8s
+      community.general.snap:
+        name: microk8s
+        classic: true
+        channel: 1.24/stable
+
+    - name: Create kubectl alias
+      community.general.snap_alias:
+        name: microk8s.kubectl
+        alias: kubectl
+
+    - name: Ensure group "microk8s" exists
+      ansible.builtin.group:
+        name: microk8s
+        state: present
+
+    - name: Ensure group "docker" exists
+      ansible.builtin.group:
+        name: docker
+        state: present
+
+    - name: Add user to docker and microk8s groups
+      ansible.builtin.user:
+        name: "{{ ansible_user }}"
+        groups: docker, microk8s
+
+    - name: Create .kube directory
+      ansible.builtin.file:
+        path: /home/{{ ansible_user }}/.kube
+        state: directory
+        owner: "{{ ansible_user }}"
+        group: "{{ ansible_user }}"
+        mode: "0644"
+
+    - name: Create .kube/config file
+      ansible.builtin.file:
+        path: /home/{{ ansible_user }}/.kube/config
+        state: touch
+        owner: "{{ ansible_user }}"
+        group: "{{ ansible_user }}"
+        mode: "0644"
+
+    - name: Get microk8s config
+      ansible.builtin.command: sudo microk8s config
+      register: microk8s_config
+      changed_when: microk8s_config.rc != 0
+
+    - name: Keep microk8s config
+      ansible.builtin.copy:
+        remote_src: true
+        content: "{{ microk8s_config.stdout }}"
+        dest: /home/{{ ansible_user }}/.kube/config
+        mode: "0644"
+      register: out
+
+    - name: Add IP address to microk8s certificate template
+      ansible.builtin.lineinfile:
+        dest: /var/snap/microk8s/current/certs/csr.conf.template
+        search_string: "#MOREIPS"
+        line: IP.3 = {{ ansible_host }}
+        state: present
+
+    - name: Start MicroK8s
+      ansible.builtin.command: microk8s start
+      register: out
+      changed_when: out.rc != 0
+
+- name: Forming MicroK8s cluster
+  hosts: all
+  become: true
+  remote_user: "{{ ansible_user }}"
+  serial: 1
+
+  tasks:
+    - name: MicroK8s add-node
+      ansible.builtin.shell: |
+        microk8s add-node \
+        | grep -m 1 "{{ hostvars[item].ansible_host }}"
+      register: master_output
+      delegate_to: "{{ item }}"
+      with_items: "{{ groups.all }}"
+      when: (microk8s_master is not defined) and (hostvars[item].microk8s_master is defined)
+
+    - name: Join command
+      ansible.builtin.set_fact:
+        join_command: "{{ (dict(master_output).results | selectattr('changed', 'true') | first).stdout }} --skip-verify"
+      when: microk8s_master is not defined
+
+    - name: MicroK8s join-node
+      ansible.builtin.command: "{{ join_command }}"
+      register: out
+      changed_when: out.rc != 0
+      when: microk8s_master is not defined
+
+- name: Enabling MicroK8s
+  hosts: all
+  become: true
+  remote_user: "{{ ansible_user }}"
+
+  tasks:
+    - name: MicroK8s refresh certificate
+      ansible.builtin.command: microk8s refresh-certs -e ca.crt
+      register: out
+      changed_when: out.rc != 0
+      when: microk8s_master is not defined
+
+    - name: MicroK8s refresh config file
+      ansible.builtin.command: microk8s config > /home/$USER/.kube/config
+      register: out
+      changed_when: out.rc != 0
+      when: microk8s_master is not defined
+
+    - name: Enable add-ons
+      ansible.builtin.command: microk8s.enable {{ item }}
+      loop:
+        - community
+        - dns
+        - helm3
+        - hostpath-storage
+        - ingress
+        - registry
+        - prometheus
+        - metrics-server
+        - linkerd
+      register: out
+      changed_when: out.rc != 0
+      when: microk8s_master is defined
+
+    - name: Create kubectl helm3 alias
+      community.general.snap_alias:
+        name: microk8s.helm3
+        alias: helm3
+      when: microk8s_master is defined
+
+    - name: Create kubectl linkerd alias
+      community.general.snap_alias:
+        name: microk8s.linkerd
+        alias: linkerd
+      when: microk8s_master is defined
+
+    - name: Ensure MicroK8s started
+      ansible.builtin.command: microk8s start
+      register: out
+      changed_when: out.rc != 0
+
+- name: Cloning TFS
+  hosts: all
+  remote_user: "{{ ansible_user }}"
+
+  tasks:
+    - name: Clone repository and switch to desired branch
+      ansible.builtin.git:
+        repo: 'https://labs.etsi.org/rep/tfs/controller.git'
+        dest: ./tfs-ctrl
+        version: "{{ tfs_branch | default('master') }}"
+      when: microk8s_master is defined