diff --git a/README.md b/README.md
index 3c0601ac24f3012e311a425ea5eea80408ea3306..d0cced502746a3a42e09e32c4973b20fb224bd25 100644
--- a/README.md
+++ b/README.md
@@ -24,12 +24,27 @@ spec:
     alwaysOnline: true
     minify:
       css: true
-  dnsRecords:
-    - type: "A"
-      name: "domainname.io"
-      content: "1.1.1.1"
-      proxied: true
-      ttl: 3600
+---
+apiVersion: crds.kubeflare.io/v1alpha1
+kind: DNSRecord
+metadata:
+  name: www.domainname.io
+spec:
+  zone: domainname.io
+  record:
+    type: "A"
+    name: "www"
+    content: "1.1.1.1"
+    proxied: true
+    ttl: 3600
+---
+apiVersion: crds.kubeflare.io/v1alpha1
+kind: DNSRecord
+metadata:
+  name: mx-records
+spec:
+  zone: domainname.io
+  records:
     - type: "MX"
       name: "domainname.io"
       content: "aspmx.l.google.com"
diff --git a/config/crds/v1/crds.kubeflare.io_dnsrecords.yaml b/config/crds/v1/crds.kubeflare.io_dnsrecords.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..810d81ac77d7925c1215a39682254f0ec49495d3
--- /dev/null
+++ b/config/crds/v1/crds.kubeflare.io_dnsrecords.yaml
@@ -0,0 +1,97 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.2.8
+  creationTimestamp: null
+  name: dnsrecords.crds.kubeflare.io
+spec:
+  group: crds.kubeflare.io
+  names:
+    kind: DNSRecord
+    listKind: DNSRecordList
+    plural: dnsrecords
+    singular: dnsrecord
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: DNSRecord is the Schema for the dnsrecords API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: DNSRecordSpec defines the desired state of DNSRecord
+            properties:
+              record:
+                properties:
+                  content:
+                    type: string
+                  name:
+                    type: string
+                  priority:
+                    type: integer
+                  proxied:
+                    type: boolean
+                  ttl:
+                    type: integer
+                  type:
+                    type: string
+                required:
+                - content
+                - name
+                - type
+                type: object
+              records:
+                items:
+                  properties:
+                    content:
+                      type: string
+                    name:
+                      type: string
+                    priority:
+                      type: integer
+                    proxied:
+                      type: boolean
+                    ttl:
+                      type: integer
+                    type:
+                      type: string
+                  required:
+                  - content
+                  - name
+                  - type
+                  type: object
+                type: array
+              zone:
+                type: string
+            required:
+            - zone
+            type: object
+          status:
+            description: DNSRecordStatus defines the observed state of DNSRecord
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/config/crds/v1/crds.kubeflare.io_zones.yaml b/config/crds/v1/crds.kubeflare.io_zones.yaml
index 0333b8ab07f6e477891fced93f4c600e3d350b68..57debde07db00e5b0b06eff16a3076dd636be83c 100644
--- a/config/crds/v1/crds.kubeflare.io_zones.yaml
+++ b/config/crds/v1/crds.kubeflare.io_zones.yaml
@@ -38,27 +38,6 @@ spec:
             properties:
               apiToken:
                 type: string
-              dnsRecords:
-                items:
-                  properties:
-                    content:
-                      type: string
-                    name:
-                      type: string
-                    priority:
-                      type: integer
-                    proxied:
-                      type: boolean
-                    ttl:
-                      type: integer
-                    type:
-                      type: string
-                  required:
-                  - content
-                  - name
-                  - type
-                  type: object
-                type: array
               settings:
                 properties:
                   advancedDDOS:
diff --git a/config/crds/v1beta1/crds.kubeflare.io_dnsrecords.yaml b/config/crds/v1beta1/crds.kubeflare.io_dnsrecords.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d9f8dd821a80e2bc53ba4deba8cbaa97cb2596c6
--- /dev/null
+++ b/config/crds/v1beta1/crds.kubeflare.io_dnsrecords.yaml
@@ -0,0 +1,98 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.2.8
+  creationTimestamp: null
+  name: dnsrecords.crds.kubeflare.io
+spec:
+  group: crds.kubeflare.io
+  names:
+    kind: DNSRecord
+    listKind: DNSRecordList
+    plural: dnsrecords
+    singular: dnsrecord
+  scope: Namespaced
+  subresources:
+    status: {}
+  validation:
+    openAPIV3Schema:
+      description: DNSRecord is the Schema for the dnsrecords API
+      properties:
+        apiVersion:
+          description: 'APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+          type: string
+        kind:
+          description: 'Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+          type: string
+        metadata:
+          type: object
+        spec:
+          description: DNSRecordSpec defines the desired state of DNSRecord
+          properties:
+            record:
+              properties:
+                content:
+                  type: string
+                name:
+                  type: string
+                priority:
+                  type: integer
+                proxied:
+                  type: boolean
+                ttl:
+                  type: integer
+                type:
+                  type: string
+              required:
+              - content
+              - name
+              - type
+              type: object
+            records:
+              items:
+                properties:
+                  content:
+                    type: string
+                  name:
+                    type: string
+                  priority:
+                    type: integer
+                  proxied:
+                    type: boolean
+                  ttl:
+                    type: integer
+                  type:
+                    type: string
+                required:
+                - content
+                - name
+                - type
+                type: object
+              type: array
+            zone:
+              type: string
+          required:
+          - zone
+          type: object
+        status:
+          description: DNSRecordStatus defines the observed state of DNSRecord
+          type: object
+      type: object
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/config/crds/v1beta1/crds.kubeflare.io_zones.yaml b/config/crds/v1beta1/crds.kubeflare.io_zones.yaml
index d2488013e61ba581287eebb62fa9d9159e0058e6..22c00f4e1b388a6f80c4fe1cf6566abbfd545713 100644
--- a/config/crds/v1beta1/crds.kubeflare.io_zones.yaml
+++ b/config/crds/v1beta1/crds.kubeflare.io_zones.yaml
@@ -38,27 +38,6 @@ spec:
           properties:
             apiToken:
               type: string
-            dnsRecords:
-              items:
-                properties:
-                  content:
-                    type: string
-                  name:
-                    type: string
-                  priority:
-                    type: integer
-                  proxied:
-                    type: boolean
-                  ttl:
-                    type: integer
-                  type:
-                    type: string
-                required:
-                - content
-                - name
-                - type
-                type: object
-              type: array
             settings:
               properties:
                 advancedDDOS:
diff --git a/pkg/apis/crds/v1alpha1/dnsrecord_types.go b/pkg/apis/crds/v1alpha1/dnsrecord_types.go
new file mode 100644
index 0000000000000000000000000000000000000000..3afba86f2485b90bd5d6d03728090d1fe4c664f5
--- /dev/null
+++ b/pkg/apis/crds/v1alpha1/dnsrecord_types.go
@@ -0,0 +1,68 @@
+/*
+Copyright 2019 Replicated, 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.
+*/
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type Record struct {
+	Type     string `json:"type"`
+	Name     string `json:"name"`
+	Content  string `json:"content"`
+	TTL      *int   `json:"ttl,omitempty"`
+	Priority *int   `json:"priority,omitempty"`
+	Proxied  *bool  `json:"proxied,omitempty"`
+}
+
+// DNSRecordSpec defines the desired state of DNSRecord
+type DNSRecordSpec struct {
+	Zone    string    `json:"zone"`
+	Record  *Record   `json:"record,omitempty"`
+	Records []*Record `json:"records,omitempty"`
+}
+
+// DNSRecordStatus defines the observed state of DNSRecord
+type DNSRecordStatus struct {
+}
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// DNSRecord is the Schema for the dnsrecords API
+// +k8s:openapi-gen=true
+// +kubebuilder:subresource:status
+type DNSRecord struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   DNSRecordSpec   `json:"spec,omitempty"`
+	Status DNSRecordStatus `json:"status,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// DNSRecordList contains a list of DNSRecord
+type DNSRecordList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []DNSRecord `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&DNSRecord{}, &DNSRecordList{})
+}
diff --git a/pkg/apis/crds/v1alpha1/zone_types.go b/pkg/apis/crds/v1alpha1/zone_types.go
index af9170b1a5f40a68f9e5361cb672926c744f22cb..fb878ec08e66a88e49d758fdc2c8bb73b9f7b4c5 100644
--- a/pkg/apis/crds/v1alpha1/zone_types.go
+++ b/pkg/apis/crds/v1alpha1/zone_types.go
@@ -20,15 +20,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-type DNSRecord struct {
-	Type     string `json:"type"`
-	Name     string `json:"name"`
-	Content  string `json:"content"`
-	TTL      *int   `json:"ttl,omitempty"`
-	Priority *int   `json:"priority,omitempty"`
-	Proxied  *bool  `json:"proxied,omitempty"`
-}
-
 type MobileRedirect struct {
 	Status          *bool   `json:"status,omi2tempty"`
 	MobileSubdomain *string `json:"mobileSubdomain,omitempty"`
@@ -72,9 +63,8 @@ type ZoneSettings struct {
 
 // ZoneSpec defines the desired state of Zone
 type ZoneSpec struct {
-	APIToken   string        `json:"apiToken"`
-	Settings   *ZoneSettings `json:"settings,omitempty"`
-	DNSRecords []*DNSRecord  `json:"dnsRecords,omitempty"`
+	APIToken string        `json:"apiToken"`
+	Settings *ZoneSettings `json:"settings,omitempty"`
 }
 
 // ZoneStatus defines the observed state of Zone
diff --git a/pkg/apis/crds/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/crds/v1alpha1/zz_generated.deepcopy.go
index aa16c5470aa44a1a13b5c568b20c0db35de680ed..de3145b85348bb26dccce2a2b7e40ee4714394a7 100644
--- a/pkg/apis/crds/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/crds/v1alpha1/zz_generated.deepcopy.go
@@ -122,21 +122,10 @@ func (in *APITokenStatus) DeepCopy() *APITokenStatus {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *DNSRecord) DeepCopyInto(out *DNSRecord) {
 	*out = *in
-	if in.TTL != nil {
-		in, out := &in.TTL, &out.TTL
-		*out = new(int)
-		**out = **in
-	}
-	if in.Priority != nil {
-		in, out := &in.Priority, &out.Priority
-		*out = new(int)
-		**out = **in
-	}
-	if in.Proxied != nil {
-		in, out := &in.Proxied, &out.Proxied
-		*out = new(bool)
-		**out = **in
-	}
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecord.
@@ -149,6 +138,92 @@ func (in *DNSRecord) DeepCopy() *DNSRecord {
 	return out
 }
 
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *DNSRecord) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DNSRecordList) DeepCopyInto(out *DNSRecordList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]DNSRecord, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordList.
+func (in *DNSRecordList) DeepCopy() *DNSRecordList {
+	if in == nil {
+		return nil
+	}
+	out := new(DNSRecordList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *DNSRecordList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DNSRecordSpec) DeepCopyInto(out *DNSRecordSpec) {
+	*out = *in
+	if in.Record != nil {
+		in, out := &in.Record, &out.Record
+		*out = new(Record)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Records != nil {
+		in, out := &in.Records, &out.Records
+		*out = make([]*Record, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Record)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSpec.
+func (in *DNSRecordSpec) DeepCopy() *DNSRecordSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(DNSRecordSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DNSRecordStatus) DeepCopyInto(out *DNSRecordStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordStatus.
+func (in *DNSRecordStatus) DeepCopy() *DNSRecordStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(DNSRecordStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *MinifySetting) DeepCopyInto(out *MinifySetting) {
 	*out = *in
@@ -209,6 +284,36 @@ func (in *MobileRedirect) DeepCopy() *MobileRedirect {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Record) DeepCopyInto(out *Record) {
+	*out = *in
+	if in.TTL != nil {
+		in, out := &in.TTL, &out.TTL
+		*out = new(int)
+		**out = **in
+	}
+	if in.Priority != nil {
+		in, out := &in.Priority, &out.Priority
+		*out = new(int)
+		**out = **in
+	}
+	if in.Proxied != nil {
+		in, out := &in.Proxied, &out.Proxied
+		*out = new(bool)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Record.
+func (in *Record) DeepCopy() *Record {
+	if in == nil {
+		return nil
+	}
+	out := new(Record)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ValueFrom) DeepCopyInto(out *ValueFrom) {
 	*out = *in
@@ -441,17 +546,6 @@ func (in *ZoneSpec) DeepCopyInto(out *ZoneSpec) {
 		*out = new(ZoneSettings)
 		(*in).DeepCopyInto(*out)
 	}
-	if in.DNSRecords != nil {
-		in, out := &in.DNSRecords, &out.DNSRecords
-		*out = make([]*DNSRecord, len(*in))
-		for i := range *in {
-			if (*in)[i] != nil {
-				in, out := &(*in)[i], &(*out)[i]
-				*out = new(DNSRecord)
-				(*in).DeepCopyInto(*out)
-			}
-		}
-	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneSpec.
diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/crds_client.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/crds_client.go
index 07c2a6eb201600be239a57dcfb9214e79d7aa00f..ddacc9020fb61252d5030b31413136a841c95346 100644
--- a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/crds_client.go
+++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/crds_client.go
@@ -27,6 +27,7 @@ import (
 type CrdsV1alpha1Interface interface {
 	RESTClient() rest.Interface
 	APITokensGetter
+	DNSRecordsGetter
 	ZonesGetter
 }
 
@@ -39,6 +40,10 @@ func (c *CrdsV1alpha1Client) APITokens(namespace string) APITokenInterface {
 	return newAPITokens(c, namespace)
 }
 
+func (c *CrdsV1alpha1Client) DNSRecords(namespace string) DNSRecordInterface {
+	return newDNSRecords(c, namespace)
+}
+
 func (c *CrdsV1alpha1Client) Zones(namespace string) ZoneInterface {
 	return newZones(c, namespace)
 }
diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/dnsrecord.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/dnsrecord.go
new file mode 100644
index 0000000000000000000000000000000000000000..0f642aead3dfa3e03d3507cb16b72bef4bb3235e
--- /dev/null
+++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/dnsrecord.go
@@ -0,0 +1,195 @@
+/*
+Copyright 2020 Replicated, 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	"time"
+
+	v1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1"
+	scheme "github.com/replicatedhq/kubeflare/pkg/client/kubeflareclientset/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// DNSRecordsGetter has a method to return a DNSRecordInterface.
+// A group's client should implement this interface.
+type DNSRecordsGetter interface {
+	DNSRecords(namespace string) DNSRecordInterface
+}
+
+// DNSRecordInterface has methods to work with DNSRecord resources.
+type DNSRecordInterface interface {
+	Create(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.CreateOptions) (*v1alpha1.DNSRecord, error)
+	Update(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.UpdateOptions) (*v1alpha1.DNSRecord, error)
+	UpdateStatus(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.UpdateOptions) (*v1alpha1.DNSRecord, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.DNSRecord, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.DNSRecordList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.DNSRecord, err error)
+	DNSRecordExpansion
+}
+
+// dNSRecords implements DNSRecordInterface
+type dNSRecords struct {
+	client rest.Interface
+	ns     string
+}
+
+// newDNSRecords returns a DNSRecords
+func newDNSRecords(c *CrdsV1alpha1Client, namespace string) *dNSRecords {
+	return &dNSRecords{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the dNSRecord, and returns the corresponding dNSRecord object, and an error if there is any.
+func (c *dNSRecords) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.DNSRecord, err error) {
+	result = &v1alpha1.DNSRecord{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of DNSRecords that match those selectors.
+func (c *dNSRecords) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.DNSRecordList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.DNSRecordList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested dNSRecords.
+func (c *dNSRecords) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a dNSRecord and creates it.  Returns the server's representation of the dNSRecord, and an error, if there is any.
+func (c *dNSRecords) Create(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.CreateOptions) (result *v1alpha1.DNSRecord, err error) {
+	result = &v1alpha1.DNSRecord{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(dNSRecord).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a dNSRecord and updates it. Returns the server's representation of the dNSRecord, and an error, if there is any.
+func (c *dNSRecords) Update(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.UpdateOptions) (result *v1alpha1.DNSRecord, err error) {
+	result = &v1alpha1.DNSRecord{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		Name(dNSRecord.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(dNSRecord).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *dNSRecords) UpdateStatus(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.UpdateOptions) (result *v1alpha1.DNSRecord, err error) {
+	result = &v1alpha1.DNSRecord{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		Name(dNSRecord.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(dNSRecord).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the dNSRecord and deletes it. Returns an error if one occurs.
+func (c *dNSRecords) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *dNSRecords) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched dNSRecord.
+func (c *dNSRecords) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.DNSRecord, err error) {
+	result = &v1alpha1.DNSRecord{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("dnsrecords").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_crds_client.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_crds_client.go
index 5bb4b4a5079796f6146b4fe86ee79f01786797f0..5360f0839761ad68496867c0b2a3def7d319beae 100644
--- a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_crds_client.go
+++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_crds_client.go
@@ -32,6 +32,10 @@ func (c *FakeCrdsV1alpha1) APITokens(namespace string) v1alpha1.APITokenInterfac
 	return &FakeAPITokens{c, namespace}
 }
 
+func (c *FakeCrdsV1alpha1) DNSRecords(namespace string) v1alpha1.DNSRecordInterface {
+	return &FakeDNSRecords{c, namespace}
+}
+
 func (c *FakeCrdsV1alpha1) Zones(namespace string) v1alpha1.ZoneInterface {
 	return &FakeZones{c, namespace}
 }
diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_dnsrecord.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_dnsrecord.go
new file mode 100644
index 0000000000000000000000000000000000000000..a743e92fca7defe4fb54d0ea4ca466868ca08941
--- /dev/null
+++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/fake/fake_dnsrecord.go
@@ -0,0 +1,142 @@
+/*
+Copyright 2020 Replicated, 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.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeDNSRecords implements DNSRecordInterface
+type FakeDNSRecords struct {
+	Fake *FakeCrdsV1alpha1
+	ns   string
+}
+
+var dnsrecordsResource = schema.GroupVersionResource{Group: "crds.kubeflare.io", Version: "v1alpha1", Resource: "dnsrecords"}
+
+var dnsrecordsKind = schema.GroupVersionKind{Group: "crds.kubeflare.io", Version: "v1alpha1", Kind: "DNSRecord"}
+
+// Get takes name of the dNSRecord, and returns the corresponding dNSRecord object, and an error if there is any.
+func (c *FakeDNSRecords) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.DNSRecord, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(dnsrecordsResource, c.ns, name), &v1alpha1.DNSRecord{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.DNSRecord), err
+}
+
+// List takes label and field selectors, and returns the list of DNSRecords that match those selectors.
+func (c *FakeDNSRecords) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.DNSRecordList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(dnsrecordsResource, dnsrecordsKind, c.ns, opts), &v1alpha1.DNSRecordList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.DNSRecordList{ListMeta: obj.(*v1alpha1.DNSRecordList).ListMeta}
+	for _, item := range obj.(*v1alpha1.DNSRecordList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested dNSRecords.
+func (c *FakeDNSRecords) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(dnsrecordsResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a dNSRecord and creates it.  Returns the server's representation of the dNSRecord, and an error, if there is any.
+func (c *FakeDNSRecords) Create(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.CreateOptions) (result *v1alpha1.DNSRecord, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(dnsrecordsResource, c.ns, dNSRecord), &v1alpha1.DNSRecord{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.DNSRecord), err
+}
+
+// Update takes the representation of a dNSRecord and updates it. Returns the server's representation of the dNSRecord, and an error, if there is any.
+func (c *FakeDNSRecords) Update(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.UpdateOptions) (result *v1alpha1.DNSRecord, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(dnsrecordsResource, c.ns, dNSRecord), &v1alpha1.DNSRecord{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.DNSRecord), err
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *FakeDNSRecords) UpdateStatus(ctx context.Context, dNSRecord *v1alpha1.DNSRecord, opts v1.UpdateOptions) (*v1alpha1.DNSRecord, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(dnsrecordsResource, "status", c.ns, dNSRecord), &v1alpha1.DNSRecord{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.DNSRecord), err
+}
+
+// Delete takes name of the dNSRecord and deletes it. Returns an error if one occurs.
+func (c *FakeDNSRecords) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(dnsrecordsResource, c.ns, name), &v1alpha1.DNSRecord{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeDNSRecords) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(dnsrecordsResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.DNSRecordList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched dNSRecord.
+func (c *FakeDNSRecords) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.DNSRecord, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(dnsrecordsResource, c.ns, name, pt, data, subresources...), &v1alpha1.DNSRecord{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.DNSRecord), err
+}
diff --git a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/generated_expansion.go b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/generated_expansion.go
index 22a3a39f5bd0f66130a657e5c8f019435fe1d378..8e703fb86304ac24f845129fbdd4396ec7ad646d 100644
--- a/pkg/client/kubeflareclientset/typed/crds/v1alpha1/generated_expansion.go
+++ b/pkg/client/kubeflareclientset/typed/crds/v1alpha1/generated_expansion.go
@@ -20,4 +20,6 @@ package v1alpha1
 
 type APITokenExpansion interface{}
 
+type DNSRecordExpansion interface{}
+
 type ZoneExpansion interface{}
diff --git a/pkg/controller/zone/dns_records.go b/pkg/controller/dnsrecord/dns_records.go
similarity index 79%
rename from pkg/controller/zone/dns_records.go
rename to pkg/controller/dnsrecord/dns_records.go
index 6cb5eb191cd2444ce5c5244d371550c707b01cc7..a1352c90a08423d1d1c3bd706cc1234d70e44c6f 100644
--- a/pkg/controller/zone/dns_records.go
+++ b/pkg/controller/dnsrecord/dns_records.go
@@ -1,4 +1,4 @@
-package zone
+package dnsrecord
 
 import (
 	"context"
@@ -7,34 +7,36 @@ import (
 	"github.com/pkg/errors"
 	crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1"
 	"github.com/replicatedhq/kubeflare/pkg/logger"
-	"go.uber.org/zap"
 )
 
-func (r *ReconcileZone) reconcileDNSRecords(ctx context.Context, instance crdsv1alpha1.Zone) error {
-	logger.Debug("reconcileDNSRecords for zone", zap.String("zoneName", instance.Name))
+func ReconcileDNSRecords(ctx context.Context, instance crdsv1alpha1.DNSRecord, zone *crdsv1alpha1.Zone, cf *cloudflare.API) error {
+	logger.Debug("reconcileDNSRecords for zone")
 
-	api, err := r.getCloudflareAPI(ctx, instance)
-	if err != nil {
-		return errors.Wrap(err, "failed to get cloudflare api")
-	}
-
-	zoneID, err := api.ZoneIDByName(instance.Name)
+	zoneID, err := cf.ZoneIDByName(zone.Name)
 	if err != nil {
 		return errors.Wrap(err, "failed to get zone id")
 	}
 
-	existingRecords, err := api.DNSRecords(zoneID, cloudflare.DNSRecord{})
+	existingRecords, err := cf.DNSRecords(zoneID, cloudflare.DNSRecord{})
 	if err != nil {
 		return errors.Wrap(err, "failed to list dns records")
 	}
 
+	desiredRecords := []*crdsv1alpha1.Record{}
+	if instance.Spec.Record != nil {
+		desiredRecords = append(desiredRecords, instance.Spec.Record)
+	}
+	if instance.Spec.Records != nil {
+		desiredRecords = append(desiredRecords, instance.Spec.Records...)
+	}
+
 	recordsToCreate := []cloudflare.DNSRecord{}
 	recordsToUpdate := []cloudflare.DNSRecord{}
 	// recordsToDelete := []cloudflare.DNSRecord{}
 
 	for _, existingRecord := range existingRecords {
 		found := false
-		for _, desiredRecord := range instance.Spec.DNSRecords {
+		for _, desiredRecord := range desiredRecords {
 			if desiredRecord.Name == existingRecord.Name && desiredRecord.Type == existingRecord.Type {
 				found = true
 				isChanged := false
@@ -75,7 +77,7 @@ func (r *ReconcileZone) reconcileDNSRecords(ctx context.Context, instance crdsv1
 		}
 	}
 
-	for _, desiredRecord := range instance.Spec.DNSRecords {
+	for _, desiredRecord := range desiredRecords {
 		found := false
 		for _, existingRecord := range existingRecords {
 			if existingRecord.Type == desiredRecord.Type && existingRecord.Name == desiredRecord.Name {
@@ -107,7 +109,7 @@ func (r *ReconcileZone) reconcileDNSRecords(ctx context.Context, instance crdsv1
 	}
 
 	for _, recordToCreate := range recordsToCreate {
-		response, err := api.CreateDNSRecord(zoneID, recordToCreate)
+		response, err := cf.CreateDNSRecord(zoneID, recordToCreate)
 		if err != nil {
 			return errors.Wrap(err, "failed to create dns record")
 		}
@@ -126,7 +128,7 @@ func (r *ReconcileZone) reconcileDNSRecords(ctx context.Context, instance crdsv1
 			Proxied: recordToUpdate.Proxied,
 		}
 
-		err := api.UpdateDNSRecord(zoneID, recordToUpdate.ID, rr)
+		err := cf.UpdateDNSRecord(zoneID, recordToUpdate.ID, rr)
 		if err != nil {
 			return errors.Wrap(err, "failed to update dns record")
 		}
diff --git a/pkg/controller/dnsrecord/dnsrecord_controller.go b/pkg/controller/dnsrecord/dnsrecord_controller.go
new file mode 100644
index 0000000000000000000000000000000000000000..9e56ececa2156590256efa2c80b515b469240931
--- /dev/null
+++ b/pkg/controller/dnsrecord/dnsrecord_controller.go
@@ -0,0 +1,123 @@
+/*
+Copyright 2019 Replicated, 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.
+*/
+
+package dnsrecord
+
+import (
+	"context"
+	"time"
+
+	"github.com/pkg/errors"
+	crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1"
+	"github.com/replicatedhq/kubeflare/pkg/controller/shared"
+	"github.com/replicatedhq/kubeflare/pkg/logger"
+	"k8s.io/apimachinery/pkg/runtime"
+	kubeinformers "k8s.io/client-go/informers"
+	"k8s.io/client-go/kubernetes"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/source"
+)
+
+// Add creates a new DNSRecord Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller
+// and Start it when the Manager is Started.
+func Add(mgr manager.Manager) error {
+	return add(mgr, newReconciler(mgr))
+}
+
+// newReconciler returns a new reconcile.Reconciler
+func newReconciler(mgr manager.Manager) reconcile.Reconciler {
+	return &ReconcileDNSRecord{
+		Client: mgr.GetClient(),
+		scheme: mgr.GetScheme(),
+	}
+}
+
+// add adds a new Controller to mgr with r as the reconcile.Reconciler
+func add(mgr manager.Manager, r reconcile.Reconciler) error {
+	// Create a new controller
+	c, err := controller.New("dnsrecord-controller", mgr, controller.Options{Reconciler: r})
+	if err != nil {
+		return err
+	}
+
+	// Watch for changes to DNSRecord
+	err = c.Watch(&source.Kind{
+		Type: &crdsv1alpha1.DNSRecord{},
+	}, &handler.EnqueueRequestForObject{})
+	if err != nil {
+		return errors.Wrap(err, "failed to start watch on dnsrecords")
+	}
+
+	generatedClient := kubernetes.NewForConfigOrDie(mgr.GetConfig())
+	generatedInformers := kubeinformers.NewSharedInformerFactory(generatedClient, time.Minute)
+	err = mgr.Add(manager.RunnableFunc(func(s <-chan struct{}) error {
+		generatedInformers.Start(s)
+		<-s
+		return nil
+	}))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+var _ reconcile.Reconciler = &ReconcileDNSRecord{}
+
+// ReconcileDNSRecord reconciles a DNSRecord object
+type ReconcileDNSRecord struct {
+	client.Client
+	scheme *runtime.Scheme
+}
+
+// Reconcile reads that state of the cluster for a ReconcileDNSRecord object and makes changes based on the state read
+// and what is in the Zone.Spec
+// +kubebuilder:rbac:groups=crds.kubeflare.io,resources=dnsrecords,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=crds.kubeflare.io,resources=dnsrecords/status,verbs=get;update;patch
+func (r *ReconcileDNSRecord) Reconcile(request reconcile.Request) (reconcile.Result, error) {
+	// This reconcile loop will be called for all ReconcileDNSRecord objects
+	// because of the informer that we have set up
+	ctx := context.Background()
+	instance := crdsv1alpha1.DNSRecord{}
+	err := r.Get(ctx, request.NamespacedName, &instance)
+	if err != nil {
+		logger.Error(err)
+		return reconcile.Result{}, err
+	}
+
+	zone, err := shared.GetZone(ctx, instance.Namespace, instance.Spec.Zone)
+	if err != nil {
+		logger.Error(err)
+		return reconcile.Result{}, err
+	}
+
+	cf, err := shared.GetCloudflareAPI(ctx, instance.Namespace, zone.Spec.APIToken)
+	if err != nil {
+		logger.Error(err)
+		return reconcile.Result{}, err
+	}
+
+	if err := ReconcileDNSRecords(ctx, instance, zone, cf); err != nil {
+		logger.Error(err)
+		return reconcile.Result{}, err
+	}
+
+	return reconcile.Result{}, nil
+}
diff --git a/pkg/controller/zone/api.go b/pkg/controller/shared/cloudflare.go
similarity index 78%
rename from pkg/controller/zone/api.go
rename to pkg/controller/shared/cloudflare.go
index 6cc98ec917d2e4004f0cbe03222bea06df99e3d2..7ab7a3bf634d9d6f40c2d0972b63010710f92b08 100644
--- a/pkg/controller/zone/api.go
+++ b/pkg/controller/shared/cloudflare.go
@@ -1,11 +1,10 @@
-package zone
+package shared
 
 import (
 	"context"
 
 	"github.com/cloudflare/cloudflare-go"
 	"github.com/pkg/errors"
-	crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1"
 	crdsclientv1alpha1 "github.com/replicatedhq/kubeflare/pkg/client/kubeflareclientset/typed/crds/v1alpha1"
 	"github.com/replicatedhq/kubeflare/pkg/logger"
 	"go.uber.org/zap"
@@ -13,7 +12,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
 )
 
-func (r *ReconcileZone) getCloudflareAPI(ctx context.Context, instance crdsv1alpha1.Zone) (*cloudflare.API, error) {
+func GetCloudflareAPI(ctx context.Context, namespace string, apiTokenName string) (*cloudflare.API, error) {
 	cfg, err := config.GetConfig()
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get config")
@@ -24,7 +23,7 @@ func (r *ReconcileZone) getCloudflareAPI(ctx context.Context, instance crdsv1alp
 		return nil, errors.Wrap(err, "failed to create crds client")
 	}
 
-	apiToken, err := crdsClient.APITokens(instance.Namespace).Get(ctx, instance.Spec.APIToken, metav1.GetOptions{})
+	apiToken, err := crdsClient.APITokens(namespace).Get(ctx, apiTokenName, metav1.GetOptions{})
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get api token")
 	}
diff --git a/pkg/controller/shared/zone.go b/pkg/controller/shared/zone.go
new file mode 100644
index 0000000000000000000000000000000000000000..68b0f733a98201da3e8d7dc527c55bea13bb0417
--- /dev/null
+++ b/pkg/controller/shared/zone.go
@@ -0,0 +1,30 @@
+package shared
+
+import (
+	"context"
+
+	"github.com/pkg/errors"
+	crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1"
+	crdsclientv1alpha1 "github.com/replicatedhq/kubeflare/pkg/client/kubeflareclientset/typed/crds/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+)
+
+func GetZone(ctx context.Context, namespace string, zoneName string) (*crdsv1alpha1.Zone, error) {
+	cfg, err := config.GetConfig()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get config")
+	}
+
+	crdsClient, err := crdsclientv1alpha1.NewForConfig(cfg)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to create crds client")
+	}
+
+	zone, err := crdsClient.Zones(namespace).Get(ctx, zoneName, metav1.GetOptions{})
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get zone")
+	}
+
+	return zone, nil
+}
diff --git a/pkg/controller/zone/zone_controller.go b/pkg/controller/zone/zone_controller.go
index 68b749dc3faf81549258ca5aa086a459341febad..54088afd1c6ec4c325f9eec103397f83d60c3c70 100644
--- a/pkg/controller/zone/zone_controller.go
+++ b/pkg/controller/zone/zone_controller.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/pkg/errors"
 	crdsv1alpha1 "github.com/replicatedhq/kubeflare/pkg/apis/crds/v1alpha1"
+	"github.com/replicatedhq/kubeflare/pkg/controller/shared"
 	"github.com/replicatedhq/kubeflare/pkg/logger"
 	"k8s.io/apimachinery/pkg/runtime"
 	kubeinformers "k8s.io/client-go/informers"
@@ -101,7 +102,7 @@ func (r *ReconcileZone) Reconcile(request reconcile.Request) (reconcile.Result,
 		return reconcile.Result{}, err
 	}
 
-	cf, err := r.getCloudflareAPI(ctx, instance)
+	cf, err := shared.GetCloudflareAPI(ctx, instance.Namespace, instance.Spec.APIToken)
 	if err != nil {
 		logger.Error(err)
 		return reconcile.Result{}, err
@@ -112,10 +113,5 @@ func (r *ReconcileZone) Reconcile(request reconcile.Request) (reconcile.Result,
 		return reconcile.Result{}, err
 	}
 
-	if err := r.reconcileDNSRecords(ctx, instance); err != nil {
-		logger.Error(err)
-		return reconcile.Result{}, err
-	}
-
 	return reconcile.Result{}, nil
 }