chore: move memcached out of operator
diff --git a/atmosphere/flows.py b/atmosphere/flows.py
index c72474f..42877cc 100644
--- a/atmosphere/flows.py
+++ b/atmosphere/flows.py
@@ -5,7 +5,7 @@
 from atmosphere.operator.api import objects, types
 from atmosphere.tasks import constants
 from atmosphere.tasks.composite import openstack_helm
-from atmosphere.tasks.kubernetes import cert_manager, v1
+from atmosphere.tasks.kubernetes import cert_manager
 
 
 def get_engine(config):
@@ -141,41 +141,4 @@
         *cert_manager.issuer_tasks_from_config(config.issuer),
     )
 
-    if config.memcached.enabled:
-        flow.add(
-            openstack_helm.ApplyReleaseSecretTask(
-                config=config,
-                namespace=config.memcached.namespace,
-                chart="memcached",
-            ),
-            openstack_helm.ApplyHelmReleaseTask(
-                namespace=config.memcached.namespace,
-                repository=constants.HELM_REPOSITORY_OPENSTACK_HELM_INFRA,
-                name="memcached",
-                version="0.1.12",
-            ),
-            v1.ApplyServiceTask(
-                namespace=config.memcached.namespace,
-                name="memcached-metrics",
-                labels={
-                    "application": "memcached",
-                    "component": "server",
-                },
-                spec={
-                    "selector": {
-                        "application": "memcached",
-                        "component": "server",
-                    },
-                    "ports": [
-                        {
-                            "name": "metrics",
-                            "protocol": "TCP",
-                            "port": 9150,
-                            "targetPort": 9150,
-                        }
-                    ],
-                },
-            ),
-        )
-
     return flow
diff --git a/atmosphere/logger.py b/atmosphere/logger.py
deleted file mode 100644
index b44a8d1..0000000
--- a/atmosphere/logger.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import structlog
-
-
-def get_logger():
-    return structlog.get_logger()
diff --git a/atmosphere/models/config.py b/atmosphere/models/config.py
index bb68d49..b861299 100644
--- a/atmosphere/models/config.py
+++ b/atmosphere/models/config.py
@@ -4,7 +4,6 @@
 from schematics import types
 from schematics.exceptions import ValidationError
 
-from atmosphere import utils
 from atmosphere.models import base
 
 CONFIG_FILE = os.environ.get("ATMOSPHERE_CONFIG", "/etc/atmosphere/config.toml")
@@ -88,23 +87,6 @@
     namespace = types.StringType(default="monitoring", required=True)
 
 
-class MemcachedImagesConfig(base.Model):
-    memcached = types.StringType(
-        default=utils.get_image_ref_using_legacy_image_repository("memcached").string()
-    )
-    exporter = types.StringType(
-        default=utils.get_image_ref_using_legacy_image_repository(
-            "prometheus_memcached_exporter"
-        ).string()
-    )
-
-
-class MemcachedChartConfig(ChartConfig):
-    namespace = types.StringType(default="openstack", required=True)
-    secret_key = types.StringType(required=True)
-    images = types.ModelType(MemcachedImagesConfig, default=MemcachedImagesConfig())
-
-
 class IngressNginxChartConfig(ChartConfig):
     namespace = types.StringType(default="openstack", required=True)
 
@@ -133,9 +115,6 @@
     ingress_nginx = types.ModelType(
         IngressNginxChartConfig, default=IngressNginxChartConfig()
     )
-    memcached = types.ModelType(
-        MemcachedChartConfig, default=MemcachedChartConfig(), required=True
-    )
     issuer = types.PolyModelType(
         [AcmeIssuerConfig, CaIssuerConfig, SelfSignedIssuerConfig],
         default=AcmeIssuerConfig(),
diff --git a/atmosphere/models/openstack_helm/endpoints.py b/atmosphere/models/openstack_helm/endpoints.py
deleted file mode 100644
index f86980e..0000000
--- a/atmosphere/models/openstack_helm/endpoints.py
+++ /dev/null
@@ -1,79 +0,0 @@
-from schematics import types
-
-from atmosphere.models import base
-
-
-class Endpoint(base.Model):
-    pass
-
-
-class EndpointHosts(base.Model):
-    default = types.StringType()
-
-
-class OsloCacheEndpointAuth(base.Model):
-    memcache_secret_key = types.StringType(required=True)
-
-
-class OsloCacheEndpoint(Endpoint):
-    auth = types.ModelType(OsloCacheEndpointAuth)
-
-    @classmethod
-    def for_chart(cls, chart, config):
-        return cls(
-            {
-                "auth": OsloCacheEndpointAuth(
-                    {
-                        "memcache_secret_key": config.memcached.secret_key,
-                    }
-                )
-            }
-        )
-
-
-class OsloDbEndpointHosts(EndpointHosts):
-    default = types.StringType(default="percona-xtradb-haproxy")
-
-
-class OsloDbEndpointAuthUser(base.Model):
-    username = types.StringType()
-    password = types.StringType()
-
-
-class OsloDbEndpointAuth(base.Model):
-    keystone = types.ModelType(OsloDbEndpointAuthUser)
-
-
-class OsloDbEndpoint(Endpoint):
-    auth = types.ModelType(OsloDbEndpointAuth)
-    hosts = types.ModelType(OsloDbEndpointHosts, default=OsloDbEndpointHosts())
-
-    @classmethod
-    def for_chart(cls, chart, config):
-        pass
-
-
-class Endpoints(base.Model):
-    oslo_cache = types.ModelType(OsloCacheEndpoint)
-    oslo_db = types.ModelType(OsloDbEndpoint)
-
-    MAPPINGS = {
-        "oslo_cache": OsloCacheEndpoint,
-        "oslo_db": OsloDbEndpoint,
-    }
-
-    ENDPOINTS = {
-        "memcached": ["oslo_cache"],
-    }
-
-    @classmethod
-    def for_chart(cls, chart, config):
-        endpoint = cls()
-
-        for endpoint_name in cls.ENDPOINTS[chart]:
-            endpoint[endpoint_name] = cls.MAPPINGS[endpoint_name].for_chart(
-                chart, config
-            )
-        endpoint.validate()
-
-        return endpoint
diff --git a/atmosphere/models/openstack_helm/images.py b/atmosphere/models/openstack_helm/images.py
deleted file mode 100644
index 7ed5283..0000000
--- a/atmosphere/models/openstack_helm/images.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from schematics import types
-
-from atmosphere.models import base
-
-
-class ImagesTags(base.Model):
-    pass
-
-
-class MemcachedImagesTags(ImagesTags):
-    memcached = types.StringType(required=True)
-    prometheus_memcached_exporter = types.StringType(required=True)
-
-    @classmethod
-    def for_chart(cls, chart, config):
-        return cls(
-            {
-                "memcached": config.memcached.images.memcached,
-                "prometheus_memcached_exporter": config.memcached.images.exporter,
-            }
-        )
-
-
-class Images(base.Model):
-    pull_policy = types.StringType(default="Always")
-    tags = types.ModelType(ImagesTags)
-
-    MAPPINGS = {
-        "memcached": MemcachedImagesTags,
-    }
-
-    @classmethod
-    def for_chart(cls, chart, config):
-        return cls(
-            {
-                "tags": cls.MAPPINGS[chart].for_chart(chart, config),
-            }
-        )
diff --git a/atmosphere/models/openstack_helm/monitoring.py b/atmosphere/models/openstack_helm/monitoring.py
deleted file mode 100644
index c508f3c..0000000
--- a/atmosphere/models/openstack_helm/monitoring.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from schematics import types
-
-from atmosphere.models import base
-
-
-class PrometheusMonitoring(base.Model):
-    enabled = types.BooleanType()
-
-
-class Monitoring(base.Model):
-    prometheus = types.ModelType(PrometheusMonitoring)
-
-    @classmethod
-    def for_chart(cls, chart, config):
-        if chart == "memcached":
-            return Monitoring(
-                {
-                    "prometheus": PrometheusMonitoring(
-                        {
-                            "enabled": True,
-                        }
-                    )
-                }
-            )
diff --git a/atmosphere/models/openstack_helm/values.py b/atmosphere/models/openstack_helm/values.py
deleted file mode 100644
index b203239..0000000
--- a/atmosphere/models/openstack_helm/values.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from schematics import types
-from schematics.transforms import blacklist
-
-from atmosphere.models import base
-from atmosphere.models.openstack_helm import endpoints, images, monitoring
-
-
-class Values(base.Model):
-    chart = types.StringType(required=True)
-
-    endpoints = types.ModelType(endpoints.Endpoints)
-    images = types.ModelType(images.Images)
-    monitoring = types.ModelType(monitoring.Monitoring)
-
-    class Options:
-        roles = {"default": blacklist("chart")}
-
-    @classmethod
-    def for_chart(cls, chart, config):
-        return cls(
-            {
-                "chart": chart,
-                "endpoints": endpoints.Endpoints.for_chart(chart, config),
-                "images": images.Images.for_chart(chart, config),
-                "monitoring": monitoring.Monitoring.for_chart(chart, config),
-            }
-        )
diff --git a/atmosphere/tasks/composite/openstack_helm.py b/atmosphere/tasks/composite/openstack_helm.py
index e47c583..4b5926b 100644
--- a/atmosphere/tasks/composite/openstack_helm.py
+++ b/atmosphere/tasks/composite/openstack_helm.py
@@ -1,51 +1,6 @@
 import textwrap
 
-import mergedeep
-import yaml
-
 from atmosphere.models import config
-from atmosphere.models.openstack_helm import values
-from atmosphere.tasks.kubernetes import flux, v1
-
-
-class ApplyReleaseSecretTask(v1.ApplySecretTask):
-    def __init__(self, config: config.Config, namespace: str, chart: str):
-        vals = mergedeep.merge(
-            {},
-            values.Values.for_chart(chart, config).to_native(),
-            getattr(config, chart).overrides,
-        )
-        values_yaml = yaml.dump(vals, default_flow_style=False)
-
-        super().__init__(
-            namespace=namespace,
-            name=f"atmosphere-{chart}",
-            data={"values.yaml": values_yaml},
-        )
-
-
-class ApplyHelmReleaseTask(flux.ApplyHelmReleaseTask):
-    def __init__(
-        self,
-        namespace: str,
-        name: str,
-        repository: str,
-        version: str,
-    ):
-        super().__init__(
-            namespace=namespace,
-            name=name,
-            repository=repository,
-            chart=name,
-            version=version,
-            values_from=[
-                {
-                    "kind": "Secret",
-                    "name": f"atmosphere-{name}",
-                }
-            ],
-            requires=set([f"secret-{namespace}-atmosphere-{name}"]),
-        )
 
 
 def generate_alertmanager_config_for_opsgenie(
diff --git a/atmosphere/tasks/kubernetes/base.py b/atmosphere/tasks/kubernetes/base.py
index b8f5bae..838b6ad 100644
--- a/atmosphere/tasks/kubernetes/base.py
+++ b/atmosphere/tasks/kubernetes/base.py
@@ -2,13 +2,14 @@
 import re
 
 import pykube
+import structlog
 from taskflow import task
 from tenacity import retry, stop_after_delay, wait_fixed
 
-from atmosphere import clients, logger
+from atmosphere import clients
 
 CAMEL_CASE_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
-LOG = logger.get_logger()
+LOG = structlog.get_logger()
 
 
 class ApplyKubernetesObjectTask(task.Task):
diff --git a/atmosphere/tasks/kubernetes/flux.py b/atmosphere/tasks/kubernetes/flux.py
deleted file mode 100644
index 9b4c3af..0000000
--- a/atmosphere/tasks/kubernetes/flux.py
+++ /dev/null
@@ -1,99 +0,0 @@
-import pykube
-from oslo_utils import strutils
-from tenacity import retry, retry_if_result, stop_after_delay, wait_fixed
-
-from atmosphere import logger
-from atmosphere.tasks.kubernetes import base
-
-LOG = logger.get_logger()
-
-
-class HelmRelease(pykube.objects.NamespacedAPIObject):
-    version = "helm.toolkit.fluxcd.io/v2beta1"
-    endpoint = "helmreleases"
-    kind = "HelmRelease"
-
-
-class ApplyHelmReleaseTask(base.ApplyKubernetesObjectTask):
-    def __init__(
-        self,
-        namespace: str,
-        name: str,
-        repository: str,
-        chart: str,
-        version: str,
-        values: dict = {},
-        values_from: list = [],
-        *args,
-        **kwargs,
-    ):
-        self._repository = repository
-        self._chart = chart
-        self._version = version
-        self._values = values
-        self._values_from = values_from
-
-        super().__init__(
-            kind=HelmRelease,
-            namespace=namespace,
-            name=name,
-            *args,
-            **kwargs,
-        )
-
-    def generate_object(self) -> HelmRelease:
-        return HelmRelease(
-            self.api,
-            {
-                "apiVersion": self._obj_kind.version,
-                "kind": self._obj_kind.kind,
-                "metadata": {
-                    "name": self._obj_name,
-                    "namespace": self._obj_namespace,
-                },
-                "spec": {
-                    "interval": "60s",
-                    "chart": {
-                        "spec": {
-                            "chart": self._chart,
-                            "version": self._version,
-                            "sourceRef": {
-                                "kind": "HelmRepository",
-                                "name": self._repository,
-                            },
-                        }
-                    },
-                    "install": {
-                        "crds": "CreateReplace",
-                        "disableWait": True,
-                        "remediation": {
-                            "retries": 3,
-                        },
-                    },
-                    "upgrade": {
-                        "crds": "CreateReplace",
-                        "disableWait": True,
-                        "remediation": {
-                            "retries": 3,
-                        },
-                    },
-                    "values": self._values,
-                    "valuesFrom": self._values_from,
-                },
-            },
-        )
-
-    @retry(
-        retry=retry_if_result(lambda f: f is False),
-        stop=stop_after_delay(300),
-        wait=wait_fixed(1),
-    )
-    def wait_for_resource(self, resource: HelmRelease, *args, **kwargs) -> bool:
-        resource.reload()
-
-        conditions = {
-            condition["type"]: strutils.bool_from_string(condition["status"])
-            for condition in resource.obj["status"].get("conditions", [])
-        }
-
-        return conditions.get("Ready", False) and conditions.get("Released", False)
diff --git a/atmosphere/tasks/kubernetes/v1.py b/atmosphere/tasks/kubernetes/v1.py
index fc1a881..46b917f 100644
--- a/atmosphere/tasks/kubernetes/v1.py
+++ b/atmosphere/tasks/kubernetes/v1.py
@@ -1,36 +1,9 @@
 import pykube
+import structlog
 
-from atmosphere import logger
 from atmosphere.tasks.kubernetes import base
 
-LOG = logger.get_logger()
-
-
-class ApplyServiceTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, namespace: str, name: str, labels: dict, spec: dict):
-        self._labels = labels
-        self._spec = spec
-
-        super().__init__(
-            kind=pykube.Service,
-            namespace=namespace,
-            name=name,
-        )
-
-    def generate_object(self) -> pykube.Service:
-        return pykube.Service(
-            self.api,
-            {
-                "apiVersion": self._obj_kind.version,
-                "kind": self._obj_kind.kind,
-                "metadata": {
-                    "name": self._obj_name,
-                    "namespace": self._obj_namespace,
-                    "labels": self._labels,
-                },
-                "spec": self._spec,
-            },
-        )
+LOG = structlog.get_logger()
 
 
 class ApplySecretTask(base.ApplyKubernetesObjectTask):
diff --git a/atmosphere/tests/unit/models/openstack_helm/test_endpoints.py b/atmosphere/tests/unit/models/openstack_helm/test_endpoints.py
deleted file mode 100644
index 3f5cfb5..0000000
--- a/atmosphere/tests/unit/models/openstack_helm/test_endpoints.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import pytest
-import requests
-import yaml
-
-from atmosphere.models import config
-from atmosphere.models.openstack_helm import endpoints as osh_endpoints
-
-# XXX(mnaser): Once we have the details of our charts codified, we can get rid
-#              of this.
-CHART_LOOKUP_TABLE = {
-    "memcached": "openstack-helm-infra",
-}
-
-
-@pytest.mark.parametrize(
-    "project,chart",
-    [(CHART_LOOKUP_TABLE[c], c) for c in osh_endpoints.Endpoints.ENDPOINTS],
-)
-def test_openstack_helm_endpoint_keys(project, chart):
-    ignored_keys = [
-        "cluster_domain_suffix",
-        "local_image_registry",
-        "oci_image_registry",
-        "kube_dns",
-    ]
-
-    raw_data = requests.get(
-        f"https://opendev.org/openstack/{project}/raw/branch/master/{chart}/values.yaml"
-    ).text
-    data = yaml.safe_load(raw_data)
-
-    chart_keys = data["endpoints"].keys() - ignored_keys
-
-    cfg = config.Config.get_mock_object()
-    atmosphere_keys = osh_endpoints.Endpoints.for_chart(chart, cfg).to_native().keys()
-
-    assert chart_keys == atmosphere_keys
-
-
-def test_endpoint_for_chart_memcached():
-    cfg = config.Config.get_mock_object()
-    endpoints = osh_endpoints.Endpoints.for_chart("memcached", cfg)
-
-    assert {
-        "oslo_cache": {
-            "auth": {
-                "memcache_secret_key": cfg.memcached.secret_key,
-            }
-        }
-    } == endpoints.to_primitive()
diff --git a/atmosphere/tests/unit/models/openstack_helm/test_images.py b/atmosphere/tests/unit/models/openstack_helm/test_images.py
deleted file mode 100644
index a724b34..0000000
--- a/atmosphere/tests/unit/models/openstack_helm/test_images.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from atmosphere.models import config
-from atmosphere.models.openstack_helm import images as osh_images
-
-
-def test_images_for_chart_memcached():
-    cfg = config.Config.get_mock_object()
-    assert {
-        "pull_policy": "Always",
-        "tags": {
-            "memcached": cfg.memcached.images.memcached,
-            "prometheus_memcached_exporter": cfg.memcached.images.exporter,
-        },
-    } == osh_images.Images.for_chart("memcached", cfg).to_primitive()
diff --git a/atmosphere/tests/unit/models/openstack_helm/test_values.py b/atmosphere/tests/unit/models/openstack_helm/test_values.py
deleted file mode 100644
index 8ed2dfa..0000000
--- a/atmosphere/tests/unit/models/openstack_helm/test_values.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from atmosphere.models import config
-from atmosphere.models.openstack_helm import values as osh_values
-
-
-class TestMemcachedValues:
-    def test_values_for_chart(self):
-        cfg = config.Config.get_mock_object()
-        values = osh_values.Values.for_chart("memcached", cfg)
-
-        assert {
-            "endpoints": {
-                "oslo_cache": {
-                    "auth": {"memcache_secret_key": cfg.memcached.secret_key}
-                }
-            },
-            "images": {
-                "pull_policy": "Always",
-                "tags": {
-                    "memcached": cfg.memcached.images.memcached,
-                    "prometheus_memcached_exporter": cfg.memcached.images.exporter,
-                },
-            },
-            "monitoring": {
-                "prometheus": {
-                    "enabled": True,
-                }
-            },
-        } == values.to_primitive()
diff --git a/playbooks/openstack.yml b/playbooks/openstack.yml
index 07b4fb5..e0ad7dd 100644
--- a/playbooks/openstack.yml
+++ b/playbooks/openstack.yml
@@ -76,6 +76,10 @@
       tags:
         - keepalived
 
+    - role: memcached
+      tags:
+        - memcached
+
     - role: openstack_helm_keystone
       tags:
         - openstack-helm-keystone
diff --git a/roles/atmosphere/defaults/main.yml b/roles/atmosphere/defaults/main.yml
index 372b2c1..3d09da7 100644
--- a/roles/atmosphere/defaults/main.yml
+++ b/roles/atmosphere/defaults/main.yml
@@ -4,9 +4,6 @@
   image_repository: "{{ atmosphere_image_repository | default('') }}"
   kube_prometheus_stack:
     overrides: "{{ kube_prometheus_stack_values | default({}) }}"
-  memcached:
-    secret_key: "{{ openstack_helm_endpoints_memcached_secret_key }}"
-    overrides: "{{ openstack_helm_infra_memcached_values | default({}) }}"
   issuer: "{{ atmosphere_issuer_config }}"
   opsgenie: "{{ atmosphere_opsgenie_config | default({}) }}"
 
diff --git a/roles/memcached/README.md b/roles/memcached/README.md
new file mode 100644
index 0000000..4d71aba
--- /dev/null
+++ b/roles/memcached/README.md
@@ -0,0 +1 @@
+# `memcached`
diff --git a/roles/memcached/defaults/main.yml b/roles/memcached/defaults/main.yml
new file mode 100644
index 0000000..d816167
--- /dev/null
+++ b/roles/memcached/defaults/main.yml
@@ -0,0 +1,20 @@
+# Copyright (c) 2023 VEXXHOST, Inc.
+#
+# 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
+#
+#      http://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.
+
+memcached_helm_release_name: memcached
+memcached_helm_chart_path: "{{ role_path }}/../../charts/memcached/"
+memcached_helm_chart_ref: /usr/local/src/memcached
+
+memcached_helm_release_namespace: openstack
+memcached_helm_values: {}
diff --git a/roles/memcached/meta/main.yml b/roles/memcached/meta/main.yml
new file mode 100644
index 0000000..ec7babd
--- /dev/null
+++ b/roles/memcached/meta/main.yml
@@ -0,0 +1,34 @@
+# Copyright (c) 2022 VEXXHOST, Inc.
+#
+# 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
+#
+#      http://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.
+
+galaxy_info:
+  author: VEXXHOST, Inc.
+  description: Ansible role for Memcached
+  license: Apache-2.0
+  min_ansible_version: 5.5.0
+  standalone: false
+  platforms:
+    - name: Ubuntu
+      versions:
+        - focal
+
+dependencies:
+  - role: defaults
+  - role: openstack_helm_endpoints
+    vars:
+      openstack_helm_endpoints_chart: memcached
+  - role: upload_helm_chart
+    vars:
+      upload_helm_chart_src: "{{ memcached_helm_chart_path }}"
+      upload_helm_chart_dest: "{{ memcached_helm_chart_ref }}"
diff --git a/roles/memcached/tasks/main.yml b/roles/memcached/tasks/main.yml
new file mode 100644
index 0000000..d941a98
--- /dev/null
+++ b/roles/memcached/tasks/main.yml
@@ -0,0 +1,68 @@
+# Copyright (c) 2022 VEXXHOST, Inc.
+#
+# 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
+#
+#      http://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: Uninstall the legacy HelmRelease
+  run_once: true
+  block:
+    - name: Suspend the existing HelmRelease
+      kubernetes.core.k8s:
+        state: patched
+        api_version: helm.toolkit.fluxcd.io/v2beta1
+        kind: HelmRelease
+        name: "{{ memcached_helm_release_name }}"
+        namespace: "{{ memcached_helm_release_namespace }}"
+        definition:
+          spec:
+            suspend: true
+
+    - name: Remove the existing HelmRelease
+      kubernetes.core.k8s:
+        state: absent
+        api_version: helm.toolkit.fluxcd.io/v2beta1
+        kind: HelmRelease
+        name: "{{ memcached_helm_release_name }}"
+        namespace: "{{ memcached_helm_release_namespace }}"
+
+- name: Deploy Helm chart
+  run_once: true
+  kubernetes.core.helm:
+    name: "{{ memcached_helm_release_name }}"
+    chart_ref: "{{ memcached_helm_chart_ref }}"
+    release_namespace: "{{ memcached_helm_release_namespace }}"
+    create_namespace: true
+    kubeconfig: /etc/kubernetes/admin.conf
+    values: "{{ _memcached_helm_values | combine(memcached_helm_values, recursive=True) }}"
+
+- name: Apply manifests for monitoring
+  run_once: true
+  kubernetes.core.k8s:
+    state: present
+    definition:
+      - apiVersion: v1
+        kind: Service
+        metadata:
+          name: "{{ memcached_helm_release_name }}-metrics"
+          namespace: "{{ memcached_helm_release_namespace }}"
+          labels:
+            application: memcached
+            component: server
+        spec:
+          ports:
+            - name: metrics
+              protocol: TCP
+              port: 9150
+              targetPort: 9150
+          selector:
+            application: memcached
+            component: server
diff --git a/roles/memcached/vars/main.yml b/roles/memcached/vars/main.yml
new file mode 100644
index 0000000..d7fcaa7
--- /dev/null
+++ b/roles/memcached/vars/main.yml
@@ -0,0 +1,21 @@
+# Copyright (c) 2022 VEXXHOST, Inc.
+#
+# 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
+#
+#      http://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.
+
+_memcached_helm_values:
+  endpoints: "{{ openstack_helm_endpoints }}"
+  images:
+    tags: "{{ atmosphere_images | vexxhost.atmosphere.openstack_helm_image_tags('memcached') }}"
+  monitoring:
+    prometheus:
+      enabled: true