diff --git a/chart/crds/identity.openstack.org_endpoints.yaml b/chart/crds/identity.openstack.org_endpoints.yaml
new file mode 100644
index 00000000..38a18494
--- /dev/null
+++ b/chart/crds/identity.openstack.org_endpoints.yaml
@@ -0,0 +1,24 @@
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: endpoints.identity.openstack.org
+spec:
+  group: identity.openstack.org
+  names:
+    kind: Endpoint
+    listKind: EndpointList
+    plural: endpoints
+    singular: endpoint
+  scope: Cluster
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/chart/templates/clusterrole.yaml b/chart/templates/clusterrole.yaml
index 89da1440..d6f6f904 100644
--- a/chart/templates/clusterrole.yaml
+++ b/chart/templates/clusterrole.yaml
@@ -114,6 +114,7 @@ rules:
   resources:
   - services
   - keystones
+  - endpoints
   verbs:
   - create
   - delete
@@ -127,6 +128,7 @@ rules:
   resources:
   - services/status
   - keystones/status
+  - endpoints/status
   verbs:
   - get
   - patch
diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml
index 2c2e9ad9..17a5e473 100644
--- a/chart/templates/deployment.yaml
+++ b/chart/templates/deployment.yaml
@@ -37,6 +37,8 @@ spec:
         - -m
         - openstack_operator.memcached
         - -m
+        - openstack_operator.openstack.identity.endpoints
+        - -m
         - openstack_operator.openstack.identity.services
         - -m
         - openstack_operator.rabbitmq
diff --git a/config/samples/identity_v1alpha1_endpoint.yaml b/config/samples/identity_v1alpha1_endpoint.yaml
new file mode 100644
index 00000000..9d522740
--- /dev/null
+++ b/config/samples/identity_v1alpha1_endpoint.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: identity.openstack.org/v1alpha1
+kind: Endpoint
+metadata:
+  name: heat-public
+spec:
+  service: orchestration
+  interface: public
+  url: https://orchestration.sjc1.vexxhost.net
diff --git a/devstack/override-defaults b/devstack/override-defaults
index 78b9f491..c93d4870 100644
--- a/devstack/override-defaults
+++ b/devstack/override-defaults
@@ -135,3 +135,19 @@ function bootstrap_keystone {
 		--bootstrap-public-url "$KEYSTONE_SERVICE_URI"
 }
 export -f bootstrap_keystone
+# Create an endpoint with a specific interface
+# Usage: _get_or_create_endpoint_with_interface <service> <interface> <url> <region>
+function _get_or_create_endpoint_with_interface {
+	cat <<EOF | kubectl apply -f-
+---
+apiVersion: identity.openstack.org/v1alpha1
+kind: Endpoint
+metadata:
+  name: ${1//_/-}-$2
+spec:
+  service: $1
+  interface: $2
+  url: $3
+EOF
+}
+export -f _get_or_create_endpoint_with_interface
diff --git a/openstack_operator/openstack/identity/endpoints.py b/openstack_operator/openstack/identity/endpoints.py
new file mode 100644
index 00000000..2b4bdc2f
--- /dev/null
+++ b/openstack_operator/openstack/identity/endpoints.py
@@ -0,0 +1,94 @@
+# Copyright 2020 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.
+
+"""Endpoints Operator
+
+This operator helps manage the creation and removal of endpoints inside
+Keystone using custom resources.
+"""
+
+import kopf
+from openstack_operator import utils
+
+
+def _get_service_by_type(conn, service_type):
+    """Get a service from Keystone based on service type."""
+
+    services = conn.search_services(filters={"type": service_type})
+
+    if len(services) > 1:
+        raise RuntimeError("Multiple services with type: %s" % service_type)
+    if len(services) == 0:
+        raise RuntimeError("Unable to find service: %s" % service_type)
+    return services[0]
+
+
+def _get_endpoint(conn, service_type, interface):
+    """Get an endpoint from Keystone
+
+    This method will retrieve the endpoint from Keystone, raise an error if it
+    found more than one or return None if it couldn't find it
+    """
+
+    service = _get_service_by_type(conn, service_type)
+
+    filters = {
+        "service_id": service.id,
+        "interface": interface,
+        "region": conn.config.get_region_name(),
+    }
+    endpoints = conn.search_endpoints(filters=filters)
+
+    if len(endpoints) > 1:
+        raise RuntimeError("Found multiple endpoints with interface & region")
+    if len(endpoints) == 0:
+        return service, None
+    return service, endpoints[0]
+
+
+@kopf.on.resume('identity.openstack.org', 'v1alpha1', 'endpoints')
+@kopf.on.create('identity.openstack.org', 'v1alpha1', 'endpoints')
+def create_or_resume(spec, **_):
+    """Create or resume controller
+
+    This function runs when a new resource is created or when the controller
+    is first started.  It creates or updates the appropriate endpoint.
+    """
+
+    conn = utils.get_openstack_connection()
+    service, endpoint = _get_endpoint(conn, spec["service"], spec["interface"])
+    if endpoint:
+        conn.update_endpoint(endpoint.id, url=spec["url"])
+        return
+
+    conn.create_endpoint(service_name_or_id=service.id, url=spec["url"],
+                         interface=spec["interface"],
+                         region=conn.config.get_region_name())
+
+
+@kopf.on.delete('identity.openstack.org', 'v1alpha1', 'endpoints')
+def delete(spec, **_):
+    """Delete an endpoint
+
+    This function runs when the endpoint CR is deleted and removes the record
+    from Keystone.
+    """
+
+    conn = utils.get_openstack_connection()
+    endpoint = _get_endpoint(conn, spec["service"], spec["interface"])
+
+    if not endpoint:
+        return
+
+    conn.delete_endpoint(endpoint)
diff --git a/openstack_operator/openstack/identity/services.py b/openstack_operator/openstack/identity/services.py
index a1f4a741..a4f8c99a 100644
--- a/openstack_operator/openstack/identity/services.py
+++ b/openstack_operator/openstack/identity/services.py
@@ -51,7 +51,6 @@ def create_or_resume(name, spec, **_):
 
     conn = utils.get_openstack_connection()
     service = _get_service(conn, name, spec["type"])
-
     if service:
         service = conn.update_service(service.id, name=name,
                                       type=spec["type"],
diff --git a/playbooks/functional/post.yaml b/playbooks/functional/post.yaml
index c534f2b0..e376b7aa 100755
--- a/playbooks/functional/post.yaml
+++ b/playbooks/functional/post.yaml
@@ -16,5 +16,6 @@
 
 - hosts: all
   roles:
-    - collect-container-logs
-    - collect-kubernetes-logs
+  - collect-container-logs
+  - collect-kubernetes-logs
+  
diff --git a/playbooks/functional/run.yaml b/playbooks/functional/run.yaml
index 96820e42..6beca174 100755
--- a/playbooks/functional/run.yaml
+++ b/playbooks/functional/run.yaml
@@ -32,6 +32,7 @@
           OS_USER_DOMAIN_ID: default
           OS_USERNAME: admin
           OS_PASSWORD: secretadmin
+          OS_REGION_NAME: RegionOne
         EOF
   roles:
     - role: helm-template