diff --git a/inventory/sample/group_vars/k8s_cluster/addons.yml b/inventory/sample/group_vars/k8s_cluster/addons.yml
index 2e077dd805e86ac66b65071bd45ea92a903e0eb8..013f30bf6d580d290081cd07f6d71a11483dd061 100644
--- a/inventory/sample/group_vars/k8s_cluster/addons.yml
+++ b/inventory/sample/group_vars/k8s_cluster/addons.yml
@@ -180,6 +180,19 @@ metallb_speaker_enabled: true
 #     peer_asn: 64513
 #     my_asn: 4200000000
 
+
+argocd_enabled: false
+# argocd_version: v2.1.6
+# argocd_namespace: argocd
+# Default password:
+#   - https://argoproj.github.io/argo-cd/getting_started/#4-login-using-the-cli
+#   ---
+#   The initial password is autogenerated to be the pod name of the Argo CD API server. This can be retrieved with the command:
+#   kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
+#   ---
+# Use the following var to set admin password
+# argocd_admin_password: "password"
+
 # The plugin manager for kubectl
 krew_enabled: false
 krew_root_dir: "/usr/local/krew"
diff --git a/roles/kubernetes-apps/argocd/defaults/main.yml b/roles/kubernetes-apps/argocd/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..39014108bcd4b1abdbf40ea207d94f3bf9ef149c
--- /dev/null
+++ b/roles/kubernetes-apps/argocd/defaults/main.yml
@@ -0,0 +1,5 @@
+---
+argocd_enabled: false
+argocd_version: v2.1.6
+argocd_namespace: argocd
+# argocd_admin_password:
diff --git a/roles/kubernetes-apps/argocd/tasks/main.yml b/roles/kubernetes-apps/argocd/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e80e63e694a84c7c6a850d7abf50744d41258b26
--- /dev/null
+++ b/roles/kubernetes-apps/argocd/tasks/main.yml
@@ -0,0 +1,77 @@
+---
+- name: Kubernetes Apps | Install yq
+  become: yes
+  get_url:
+    url: "https://github.com/mikefarah/yq/releases/download/v4.11.2/yq_linux_amd64"
+    dest: "{{ bin_dir }}/yq"
+    mode: '0755'
+
+- name: Kubernetes Apps | Set ArgoCD template list
+  set_fact:
+    argocd_templates:
+      - name: namespace
+        file: argocd-namespace.yml
+      - name: install
+        file: argocd-install.yml
+        namespace: "{{ argocd_namespace }}"
+        url: "https://raw.githubusercontent.com/argoproj/argo-cd/{{argocd_version}}/manifests/install.yaml"
+  when:
+    - "inventory_hostname == groups['kube_control_plane'][0]"
+
+- name: Kubernetes Apps | Download ArgoCD remote manifests
+  become: yes
+  get_url:
+    url: "{{ item.url }}"
+    dest: "{{ kube_config_dir }}/{{ item.file }}"
+  with_items: "{{ argocd_templates | selectattr('url', 'defined') | list }}"
+  loop_control:
+    label: "{{ item.file }}"
+  when:
+    - "inventory_hostname == groups['kube_control_plane'][0]"
+
+- name: Kubernetes Apps | Set ArgoCD namespace for remote manifests
+  become: yes
+  command: |
+    {{ bin_dir }}/yq eval-all -i '.metadata.namespace="{{argocd_namespace}}"' {{ kube_config_dir }}/{{ item.file }}
+  with_items: "{{ argocd_templates | selectattr('url', 'defined') | list }}"
+  loop_control:
+    label: "{{ item.file }}"
+  when:
+    - "inventory_hostname == groups['kube_control_plane'][0]"
+
+- name: Kubernetes Apps | Create ArgoCD manifests from templates
+  become: yes
+  template:
+    src: "{{ item.file }}.j2"
+    dest: "{{ kube_config_dir }}/{{ item.file }}"
+  with_items: "{{ argocd_templates | selectattr('url', 'undefined') | list }}"
+  loop_control:
+    label: "{{ item.file }}"
+  when:
+    - "inventory_hostname == groups['kube_control_plane'][0]"
+
+- name: Kubernetes Apps | Install ArgoCD
+  become: yes
+  kube:
+    name: ArgoCD
+    kubectl: "{{ bin_dir }}/kubectl"
+    filename: "{{ kube_config_dir }}/{{ item.file }}"
+    state: latest
+  with_items: "{{ argocd_templates }}"
+  when:
+    - "inventory_hostname == groups['kube_control_plane'][0]"
+
+# https://github.com/argoproj/argo-cd/blob/master/docs/faq.md#i-forgot-the-admin-password-how-do-i-reset-it
+- name: Kubernetes Apps | Set ArgoCD custom admin password
+  become: yes
+  shell: |
+    {{ bin_dir }}/kubectl --kubeconfig /etc/kubernetes/admin.conf -n {{argocd_namespace}} patch secret argocd-secret -p \
+      '{
+        "stringData": {
+          "admin.password": "{{argocd_admin_password|password_hash('bcrypt')}}",
+          "admin.passwordMtime": "'$(date +%FT%T%Z)'"
+        }
+      }'
+  when:
+    - argocd_admin_password is defined
+    - "inventory_hostname == groups['kube_control_plane'][0]"
diff --git a/roles/kubernetes-apps/argocd/templates/argocd-namespace.yml.j2 b/roles/kubernetes-apps/argocd/templates/argocd-namespace.yml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..99962f13f251d6645ac8936af034b1022455eee4
--- /dev/null
+++ b/roles/kubernetes-apps/argocd/templates/argocd-namespace.yml.j2
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{argocd_namespace}}
+  labels:
+    app: argocd
diff --git a/roles/kubernetes-apps/meta/main.yml b/roles/kubernetes-apps/meta/main.yml
index 8ed80387d53ad6bd210c6a774b088187af19774e..4650b38c162c0d034e87af527a266c3500bcd150 100644
--- a/roles/kubernetes-apps/meta/main.yml
+++ b/roles/kubernetes-apps/meta/main.yml
@@ -110,3 +110,10 @@ dependencies:
       - inventory_hostname == groups['kube_control_plane'][0]
     tags:
       - metallb
+
+  - role: kubernetes-apps/argocd
+    when:
+      - argocd_enabled
+      - inventory_hostname == groups['kube_control_plane'][0]
+    tags:
+      - argocd
diff --git a/roles/kubespray-defaults/defaults/main.yaml b/roles/kubespray-defaults/defaults/main.yaml
index 282cc6507aa9144f02227b07a20b281f2aad7b83..ef14e9b3297439b10f426927e43d72a19e60226c 100644
--- a/roles/kubespray-defaults/defaults/main.yaml
+++ b/roles/kubespray-defaults/defaults/main.yaml
@@ -377,6 +377,7 @@ ingress_alb_enabled: false
 cert_manager_enabled: false
 expand_persistent_volumes: false
 metallb_enabled: false
+argocd_enabled: false
 
 # containerd official CLI tool
 nerdctl_enabled: false