Repository: weaveworks/flagger
Pull request #584 (2020-05-13 14:46:51)

Implement AppMesh v1beta2 router
This PR adds support for AppMesh v1beta2. Since v1beta2 is not backwards compatible, users must set the provider to `appmesh:v1beta2` on new cluster in order to use Flagger with the new AppMesh manager.

Changes:
- add AppMesh v1beta2 router (provider `appmesh:v1beta2`)
- maintain compatibility with AppMesh v1beta1 (provider `appmesh`)
- register VirtualNodes before primary deployment initialisation 
- add provider list to API
- refactor observer factory
- add Envoy metric templates to crossover docs
- fix service annotation diff

TODO (followup PRs):
- add  v1beta2 VirtualNodes to loadtester helm chart
- rewrite the Flagger AppMesh Gateway for v1beta2
- create docs for AppMesh v1beta2 setup on EKS
- rewrite AppMesh tutorial when v1beta2 is released by AWS

Ref: #577


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ab63153..4dde4592 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,7 +22,7 @@ The project uses Slack: To join the conversation, simply join the
 ## Getting Started
 
 - Fork the repository on GitHub
-- If you want to contribute as a developer, read [Flagger Development Guide](https://docs.flagger.app/dev/dev-guide)
+- If you want to contribute as a developer, read [Flagger Development Guide](https://docs.flagger.app/dev-guide)
 - If you have questions, concerns, get stuck or need a hand, let us know
   on the Slack channel. We are happy to help and look forward to having
   you part of the team. No matter in which capacity.
diff --git a/artifacts/flagger/account.yaml b/artifacts/flagger/account.yaml
index 311c9165..22c30447 100644
--- a/artifacts/flagger/account.yaml
+++ b/artifacts/flagger/account.yaml
@@ -110,6 +110,8 @@ rules:
     resources:
       - virtualnodes
       - virtualnodes/finalizers
+      - virtualrouters
+      - virtualrouters/finalizers
       - virtualservices
       - virtualservices/finalizers
     verbs:
diff --git a/charts/flagger/templates/rbac.yaml b/charts/flagger/templates/rbac.yaml
index c2ffb5a2..506c1783 100644
--- a/charts/flagger/templates/rbac.yaml
+++ b/charts/flagger/templates/rbac.yaml
@@ -106,6 +106,8 @@ rules:
     resources:
       - virtualnodes
       - virtualnodes/finalizers
+      - virtualrouters
+      - virtualrouters/finalizers
       - virtualservices
       - virtualservices/finalizers
     verbs:
diff --git a/docs/gitbook/README.md b/docs/gitbook/README.md
index e0361dd0..16995256 100644
--- a/docs/gitbook/README.md
+++ b/docs/gitbook/README.md
@@ -44,4 +44,4 @@ After install Flagger, you can follow one of the tutorials:
 
 * [Istio](https://github.com/stefanprodan/gitops-istio)
 * [Linkerd](https://helm.workshop.flagger.dev)
-* [AWS App Mesh](https://eks.handson.flagger.dev)
+* [AWS App Mesh](https://eks.hands-on.flagger.dev)
diff --git a/docs/gitbook/tutorials/crossover-progressive-delivery.md b/docs/gitbook/tutorials/crossover-progressive-delivery.md
index 8f92bfa0..970c2807 100644
--- a/docs/gitbook/tutorials/crossover-progressive-delivery.md
+++ b/docs/gitbook/tutorials/crossover-progressive-delivery.md
@@ -51,17 +51,6 @@ helm upgrade -i flagger flagger/flagger \
 --set meshProvider=smi:crossover
 ```
 
-Optionally you can enable Slack notifications:
-
-```bash
-helm upgrade -i flagger flagger/flagger \
---reuse-values \
---namespace test \
---set slack.url=https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \
---set slack.channel=general \
---set slack.user=flagger
-```
-
 ## Bootstrap
 
 Flagger takes a Kubernetes deployment and optionally a horizontal pod autoscaler (HPA),
@@ -82,6 +71,59 @@ helm upgrade -i flagger-loadtester flagger/loadtester \
 --namespace=test
 ```
 
+Create a metric template to measure the HTTP requests error rate:
+
+```yaml
+apiVersion: flagger.app/v1beta1
+kind: MetricTemplate
+metadata:
+  name: error-rate
+  namespace: test
+spec:
+  provider:
+    address: http://flagger-prometheus:9090
+    type: prometheus
+  query: |
+    100 - rate(
+      envoy_cluster_upstream_rq{
+        kubernetes_namespace="{{ namespace }}",
+        envoy_cluster_name="{{ target }}-canary",
+        envoy_response_code!~"5.*"
+      }[{{ interval }}]) 
+    / 
+    rate(
+      envoy_cluster_upstream_rq{
+        kubernetes_namespace="{{ namespace }}",
+        envoy_cluster_name="{{ target }}-canary"
+      }[{{ interval }}]
+    ) * 100
+```
+
+Create a metric template to measure the HTTP requests average duration:
+
+```yaml
+apiVersion: flagger.app/v1beta1
+kind: MetricTemplate
+metadata:
+  name: latency
+  namespace: test
+spec:
+  provider:
+    address: http://flagger-prometheus:9090
+    type: prometheus
+  query: |
+    histogram_quantile(0.99,
+      sum(
+        rate(
+          envoy_cluster_upstream_rq_time_bucket{
+            kubernetes_namespace="{{ namespace }}",
+            envoy_cluster_name="{{ target }}-canary"
+          }[{{ interval }}]
+        )
+      ) by (le)
+    )
+```
+
 Create a canary custom resource:
 
 ```yaml
@@ -91,15 +133,12 @@ metadata:
   name: podinfo
   namespace: test
 spec:
-  # specify mesh provider if it isn't the default one
-  # provider: "smi:crossover"
+  provider: "smi:crossover"
   # deployment reference
   targetRef:
     apiVersion: apps/v1
     kind: Deployment
     name: podinfo
-  # the maximum time in seconds for the canary deployment
-  # to make progress before it is rollback (default 600s)
   progressDeadlineSeconds: 60
   # HPA reference (optional)
   autoscalerRef:
@@ -107,10 +146,7 @@ spec:
     kind: HorizontalPodAutoscaler
     name: podinfo
   service:
-    # ClusterIP port number
     port: 9898
-    # container port number or name (optional)
-    targetPort: 9898
   # define the canary analysis timing and KPIs
   analysis:
     # schedule interval (default 60s)
@@ -123,21 +159,19 @@ spec:
     # canary increment step
     # percentage (0-100)
     stepWeight: 5
-    # App Mesh Prometheus checks
     metrics:
-    - name: request-success-rate
-      # minimum req success rate (non 5xx responses)
-      # percentage (0-100)
+    - name: error-rate
+      templateRef:
+        name: error-rate
       thresholdRange:
-        min: 99
-      interval: 1m
-    - name: request-duration
-      # maximum req duration P99
-      # milliseconds
+        max: 1
+      interval: 30s
+    - name: latency
+      templateRef:
+        name: latency
       thresholdRange:
-        max: 500
+        max: 0.5
       interval: 30s
-    # testing (optional)
     webhooks:
     - name: acceptance-test
       type: pre-rollout
diff --git a/docs/gitbook/tutorials/prometheus-operator.md b/docs/gitbook/tutorials/prometheus-operator.md
index ceb4bf88..89066e3f 100644
--- a/docs/gitbook/tutorials/prometheus-operator.md
+++ b/docs/gitbook/tutorials/prometheus-operator.md
@@ -118,7 +118,7 @@ spec:
     ) * 100
 ```
 
-Amd a metric template to measure the HTTP requests average duration:
+Create a metric template to measure the HTTP requests average duration:
 
 ```yaml
 apiVersion: flagger.app/v1beta1
diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh
index cd609cd5..eaed5c60 100755
--- a/hack/update-codegen.sh
+++ b/hack/update-codegen.sh
@@ -30,7 +30,7 @@ chmod +x ${CODEGEN_PKG}/generate-groups.sh
 
 ${CODEGEN_PKG}/generate-groups.sh all \
     github.com/weaveworks/flagger/pkg/client github.com/weaveworks/flagger/pkg/apis \
-    "flagger:v1beta1 appmesh:v1beta1 istio:v1alpha3 smi:v1alpha1 smi:v1alpha2 gloo:v1 projectcontour:v1" \
+    "flagger:v1beta1 appmesh:v1beta2 appmesh:v1beta1 istio:v1alpha3 smi:v1alpha1 smi:v1alpha2 gloo:v1 projectcontour:v1" \
     --output-base "${TEMP_DIR}" \
     --go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt
 
diff --git a/kustomize/base/flagger/rbac.yaml b/kustomize/base/flagger/rbac.yaml
index 5c9d102a..e2a6b48d 100644
--- a/kustomize/base/flagger/rbac.yaml
+++ b/kustomize/base/flagger/rbac.yaml
@@ -100,6 +100,8 @@ rules:
     resources:
       - virtualnodes
       - virtualnodes/finalizers
+      - virtualrouters
+      - virtualrouters/finalizers
       - virtualservices
       - virtualservices/finalizers
     verbs:
diff --git a/pkg/apis/appmesh/v1beta2/doc.go b/pkg/apis/appmesh/v1beta2/doc.go
new file mode 100644
index 00000000..bc912d34
--- /dev/null
+++ b/pkg/apis/appmesh/v1beta2/doc.go
@@ -0,0 +1,5 @@
+// +k8s:deepcopy-gen=package
+
+// Package v1beta2 is the v1beta2 version of the API.
+// +groupName=appmesh.k8s.aws
+package v1beta2
diff --git a/pkg/apis/appmesh/v1beta2/register.go b/pkg/apis/appmesh/v1beta2/register.go
new file mode 100644
index 00000000..5cb0f231
--- /dev/null
+++ b/pkg/apis/appmesh/v1beta2/register.go
@@ -0,0 +1,41 @@
+package v1beta2
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/weaveworks/flagger/pkg/apis/appmesh"
+)
+
+// SchemeGroupVersion is group version used to register these objects
+var SchemeGroupVersion = schema.GroupVersion{Group: appmesh.GroupName, Version: "v1beta2"}
+
+// Kind takes an unqualified kind and returns back a Group qualified GroupKind
+func Kind(kind string) schema.GroupKind {
+	return SchemeGroupVersion.WithKind(kind).GroupKind()
+}
+
+// Resource takes an unqualified resource and returns a Group qualified GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+var (
+	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+	AddToScheme   = SchemeBuilder.AddToScheme
+)
+
+// Adds the list of known types to api.Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&VirtualService{},
+		&VirtualServiceList{},
+		&VirtualRouter{},
+		&VirtualRouterList{},
+		&VirtualNode{},
+		&VirtualNodeList{},
+	)
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/pkg/apis/appmesh/v1beta2/types.go b/pkg/apis/appmesh/v1beta2/types.go
new file mode 100644
index 00000000..324ea351
--- /dev/null
+++ b/pkg/apis/appmesh/v1beta2/types.go
@@ -0,0 +1,91 @@
+package v1beta2
+
+import "k8s.io/apimachinery/pkg/types"
+
+// +kubebuilder:validation:Enum=s;ms
+type DurationUnit string
+
+const (
+	DurationUnitS  DurationUnit = "s"
+	DurationUnitMS DurationUnit = "ms"
+)
+
+type Duration struct {
+	// A unit of time.
+	Unit DurationUnit `json:"unit"`
+	// A number of time units.
+	// +kubebuilder:validation:Minimum=0
+	Value int64 `json:"value"`
+}
+
+// +kubebuilder:validation:Minimum=1
+// +kubebuilder:validation:Maximum=65535
+type PortNumber int64
+
+// +kubebuilder:validation:Enum=grpc;http;http2;tcp
+type PortProtocol string
+
+const (
+	PortProtocolGRPC  PortProtocol = "grpc"
+	PortProtocolHTTP  PortProtocol = "http"
+	PortProtocolHTTP2 PortProtocol = "http2"
+	PortProtocolTCP   PortProtocol = "tcp"
+)
+
+// PortMapping refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_PortMapping.html
+type PortMapping struct {
+	// The port used for the port mapping.
+	Port PortNumber `json:"port"`
+	// The protocol used for the port mapping.
+	Protocol PortProtocol `json:"protocol"`
+}
+
+// VirtualNodeReference holds a reference to VirtualNode.appmesh.k8s.aws
+type VirtualNodeReference struct {
+	// Namespace is the namespace of VirtualNode CR.
+	// If unspecified, defaults to the referencing object's namespace
+	// +optional
+	Namespace *string `json:"namespace,omitempty"`
+	// Name is the name of VirtualNode CR
+	Name string `json:"name"`
+}
+
+// VirtualServiceReference holds a reference to VirtualService.appmesh.k8s.aws
+type VirtualServiceReference struct {
+	// Namespace is the namespace of VirtualService CR.
+	// If unspecified, defaults to the referencing object's namespace
+	// +optional
+	Namespace *string `json:"namespace,omitempty"`
+	// Name is the name of VirtualService CR
+	Name string `json:"name"`
+}
+
+// VirtualRouterReference holds a reference to VirtualRouter.appmesh.k8s.aws
+type VirtualRouterReference struct {
+	// Namespace is the namespace of VirtualRouter CR.
+	// If unspecified, defaults to the referencing object's namespace
+	// +optional
+	Namespace *string `json:"namespace,omitempty"`
+	// Name is the name of VirtualRouter CR
+	Name string `json:"name"`
+}
+
+// MeshReference holds a reference to Mesh.appmesh.k8s.aws
+type MeshReference struct {
+	// Name is the name of Mesh CR
+	Name string `json:"name"`
+	// UID is the UID of Mesh CR
+	UID types.UID `json:"uid"`
+}
+
+// VirtualGatewayReference holds a reference to VirtualGateway.appmesh.k8s.aws
+type VirtualGatewayReference struct {
+	// Namespace is the namespace of VirtualGateway CR.
+	// If unspecified, defaults to the referencing object's namespace
+	// +optional
+	Namespace *string `json:"namespace,omitempty"`
+	// Name is the name of VirtualGateway CR
+	Name string `json:"name"`
+	// UID is the UID of VirtualGateway CR
+	UID types.UID `json:"uid"`
+}
diff --git a/pkg/apis/appmesh/v1beta2/virtualnode_types.go b/pkg/apis/appmesh/v1beta2/virtualnode_types.go
new file mode 100644
index 00000000..48b29f48
--- /dev/null
+++ b/pkg/apis/appmesh/v1beta2/virtualnode_types.go
@@ -0,0 +1,347 @@
+package v1beta2
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// TLSValidationContextACMTrust refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_TlsValidationContextAcmTrust.html
+type TLSValidationContextACMTrust struct {
+	// One or more ACM Amazon Resource Name (ARN)s.
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=3
+	CertificateAuthorityARNs []string `json:"certificateAuthorityARNs"`
+}
+
+// TLSValidationContextFileTrust refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_TlsValidationContextFileTrust.html
+type TLSValidationContextFileTrust struct {
+	// The certificate trust chain for a certificate stored on the file system of the virtual node that the proxy is running on.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	CertificateChain string `json:"certificateChain"`
+}
+
+// TLSValidationContextTrust refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_TlsValidationContextTrust.html
+type TLSValidationContextTrust struct {
+	// A reference to an object that represents a TLS validation context trust for an AWS Certicate Manager (ACM) certificate.
+	// +optional
+	ACM *TLSValidationContextACMTrust `json:"acm,omitempty"`
+	// An object that represents a TLS validation context trust for a local file.
+	// +optional
+	File *TLSValidationContextFileTrust `json:"file,omitempty"`
+}
+
+// TLSValidationContext refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_TlsValidationContext.html
+type TLSValidationContext struct {
+	// A reference to an object that represents a TLS validation context trust
+	Trust TLSValidationContextTrust `json:"trust"`
+}
+
+// ClientPolicyTLS refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_ClientPolicyTls.html
+type ClientPolicyTLS struct {
+	// Whether the policy is enforced.
+	// If unspecified, default settings from AWS API will be applied. Refer to AWS Docs for default settings.
+	// +optional
+	Enforce *bool `json:"enforce,omitempty"`
+	// The range of ports that the policy is enforced for.
+	// +optional
+	Ports []PortNumber `json:"ports,omitempty"`
+	// A reference to an object that represents a TLS validation context.
+	Validation TLSValidationContext `json:"validation"`
+}
+
+// ClientPolicy refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_ClientPolicy.html
+type ClientPolicy struct {
+	// A reference to an object that represents a Transport Layer Security (TLS) client policy.
+	// +optional
+	TLS *ClientPolicyTLS `json:"tls,omitempty"`
+}
+
+// VirtualServiceBackend refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_VirtualServiceBackend.html
+type VirtualServiceBackend struct {
+	// The VirtualService that is acting as a virtual node backend.
+	VirtualServiceRef VirtualServiceReference `json:"virtualServiceRef"`
+	// A reference to an object that represents the client policy for a backend.
+	// +optional
+	ClientPolicy *ClientPolicy `json:"clientPolicy,omitempty"`
+}
+
+// Backend refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_Backend.html
+type Backend struct {
+	// Specifies a virtual service to use as a backend for a virtual node.
+	VirtualService VirtualServiceBackend `json:"virtualService"`
+}
+
+// BackendDefaults refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_BackendDefaults.html
+type BackendDefaults struct {
+	// A reference to an object that represents a client policy.
+	// +optional
+	ClientPolicy *ClientPolicy `json:"clientPolicy,omitempty"`
+}
+
+// HealthCheckPolicy refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HealthCheckPolicy.html
+type HealthCheckPolicy struct {
+	// The number of consecutive successful health checks that must occur before declaring listener healthy.
+	// If unspecified, defaults to be 10
+	// +kubebuilder:validation:Minimum=2
+	// +kubebuilder:validation:Maximum=10
+	// +optional
+	HealthyThreshold *int64 `json:"healthyThreshold,omitempty"`
+	// The time period in milliseconds between each health check execution.
+	// If unspecified, defaults to be 30000
+	// +kubebuilder:validation:Minimum=5000
+	// +kubebuilder:validation:Maximum=300000
+	// +optional
+	IntervalMillis *int64 `json:"intervalMillis,omitempty"`
+	// The destination path for the health check request.
+	// This value is only used if the specified protocol is http or http2. For any other protocol, this value is ignored.
+	// +optional
+	Path *string `json:"path,omitempty"`
+	// The destination port for the health check request.
+	// If unspecified, defaults to be same as port defined in the PortMapping for the listener.
+	// +optional
+	Port *PortNumber `json:"port,omitempty"`
+	// The protocol for the health check request
+	// If unspecified, defaults to be same as protocol defined in the PortMapping for the listener.
+	// +optional
+	Protocol *PortProtocol `json:"protocol,omitempty"`
+	// The amount of time to wait when receiving a response from the health check, in milliseconds.
+	// If unspecified, defaults to be 5000
+	// +kubebuilder:validation:Minimum=2000
+	// +kubebuilder:validation:Maximum=60000
+	// +optional
+	TimeoutMillis *int64 `json:"timeoutMillis,omitempty"`
+	// The number of consecutive failed health checks that must occur before declaring a virtual node unhealthy.
+	// If unspecified, defaults to be 2
+	// +kubebuilder:validation:Minimum=2
+	// +kubebuilder:validation:Maximum=10
+	// +optional
+	UnhealthyThreshold *int64 `json:"unhealthyThreshold,omitempty"`
+}
+
+// ListenerTLSACMCertificate refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_ListenerTlsAcmCertificate.html
+type ListenerTLSACMCertificate struct {
+	// The Amazon Resource Name (ARN) for the certificate.
+	CertificateARN string `json:"certificateARN"`
+}
+
+// ListenerTLSFileCertificate refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_ListenerTlsFileCertificate.html
+type ListenerTLSFileCertificate struct {
+	// The certificate chain for the certificate.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	CertificateChain string `json:"certificateChain"`
+	// The private key for a certificate stored on the file system of the virtual node that the proxy is running on.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	PrivateKey string `json:"privateKey"`
+}
+
+// ListenerTLSCertificate refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_ListenerTlsCertificate.html
+type ListenerTLSCertificate struct {
+	// A reference to an object that represents an AWS Certificate Manager (ACM) certificate.
+	// +optional
+	ACM *ListenerTLSACMCertificate `json:"acm,omitempty"`
+	// A reference to an object that represents a local file certificate.
+	// +optional
+	File *ListenerTLSFileCertificate `json:"file,omitempty"`
+}
+
+const (
+	ListenerTLSModeDisabled   ListenerTLSMode = "DISABLED"
+	ListenerTLSModePermissive ListenerTLSMode = "PERMISSIVE"
+	ListenerTLSModeStrict     ListenerTLSMode = "STRICT"
+)
+
+// +kubebuilder:validation:Enum=DISABLED;PERMISSIVE;STRICT
+type ListenerTLSMode string
+
+// ListenerTLS refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_ListenerTls.html
+type ListenerTLS struct {
+	// A reference to an object that represents a listener's TLS certificate.
+	Certificate ListenerTLSCertificate `json:"certificate"`
+	// ListenerTLS mode
+	Mode ListenerTLSMode `json:"mode"`
+}
+
+// Listener refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_Listener.html
+type Listener struct {
+	// The port mapping information for the listener.
+	PortMapping PortMapping `json:"portMapping"`
+	// The health check information for the listener.
+	// +optional
+	HealthCheck *HealthCheckPolicy `json:"healthCheck,omitempty"`
+	// A reference to an object that represents the Transport Layer Security (TLS) properties for a listener.
+	// +optional
+	TLS *ListenerTLS `json:"tls,omitempty"`
+}
+
+// AWSCloudMapInstanceAttribute refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_AwsCloudMapInstanceAttribute.html
+type AWSCloudMapInstanceAttribute struct {
+	// The name of an AWS Cloud Map service instance attribute key.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	Key string `json:"key"`
+	// The value of an AWS Cloud Map service instance attribute key.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=1024
+	Value string `json:"value"`
+}
+
+// AWSCloudMapServiceDiscovery refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_AwsCloudMapServiceDiscovery.html
+type AWSCloudMapServiceDiscovery struct {
+	// The name of the AWS Cloud Map namespace to use.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=1024
+	NamespaceName string `json:"namespaceName"`
+	// The name of the AWS Cloud Map service to use.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=1024
+	ServiceName string `json:"serviceName"`
+	// A string map that contains attributes with values that you can use to filter instances by any custom attribute that you specified when you registered the instance
+	// +optional
+	Attributes []AWSCloudMapInstanceAttribute `json:"attributes,omitempty"`
+}
+
+// DNSServiceDiscovery refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_DnsServiceDiscovery.html
+type DNSServiceDiscovery struct {
+	// Specifies the DNS service discovery hostname for the virtual node.
+	Hostname string `json:"hostname"`
+}
+
+// ServiceDiscovery refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_ServiceDiscovery.html
+type ServiceDiscovery struct {
+	// Specifies any AWS Cloud Map information for the virtual node.
+	// +optional
+	AWSCloudMap *AWSCloudMapServiceDiscovery `json:"awsCloudMap,omitempty"`
+	// Specifies the DNS information for the virtual node.
+	// +optional
+	DNS *DNSServiceDiscovery `json:"dns,omitempty"`
+}
+
+// FileAccessLog refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_FileAccessLog.html
+type FileAccessLog struct {
+	// The file path to write access logs to.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	Path string `json:"path"`
+}
+
+// AccessLog refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_AccessLog.html
+type AccessLog struct {
+	// The file object to send virtual node access logs to.
+	// +optional
+	File *FileAccessLog `json:"file,omitempty"`
+}
+
+// Logging refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_Logging.html
+type Logging struct {
+	// The access log configuration for a virtual node.
+	// +optional
+	AccessLog *AccessLog `json:"accessLog,omitempty"`
+}
+
+type VirtualNodeConditionType string
+
+const (
+	// VirtualNodeActive is True when the AppMesh VirtualNode has been created or found via the API
+	VirtualNodeActive VirtualNodeConditionType = "VirtualNodeActive"
+)
+
+type VirtualNodeCondition struct {
+	// Type of VirtualNode condition.
+	Type VirtualNodeConditionType `json:"type"`
+	// Status of the condition, one of True, False, Unknown.
+	Status corev1.ConditionStatus `json:"status"`
+	// Last time the condition transitioned from one status to another.
+	// +optional
+	LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"`
+	// The reason for the condition's last transition.
+	// +optional
+	Reason *string `json:"reason,omitempty"`
+	// A human readable message indicating details about the transition.
+	// +optional
+	Message *string `json:"message,omitempty"`
+}
+
+// AWSCloudMapServiceStatus is AWS CloudMap Service object's info
+type AWSCloudMapServiceStatus struct {
+	// NamespaceID is AWS CloudMap Service object's namespace Id
+	// +optional
+	NamespaceID *string `json:"namespaceID,omitempty"`
+	// ServiceID is AWS CloudMap Service object's Id
+	// +optional
+	ServiceID *string `json:"serviceID,omitempty"`
+}
+
+// VirtualNodeSpec defines the desired state of VirtualNode
+// refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_VirtualServiceSpec.html
+type VirtualNodeSpec struct {
+	// AWSName is the AppMesh VirtualNode object's name.
+	// If unspecified or empty, it defaults to be "${name}_${namespace}" of k8s VirtualNode
+	// +optional
+	AWSName *string `json:"awsName,omitempty"`
+	// PodSelector selects Pods using labels to designate VirtualNode membership.
+	// if unspecified or empty, it selects no pods.
+	// +optional
+	PodSelector *metav1.LabelSelector `json:"podSelector,omitempty"`
+	// The listener that the virtual node is expected to receive inbound traffic from
+	// +kubebuilder:validation:MinItems=0
+	// +kubebuilder:validation:MaxItems=1
+	// +optional
+	Listeners []Listener `json:"listeners,omitempty"`
+	// The service discovery information for the virtual node.
+	// +optional
+	ServiceDiscovery *ServiceDiscovery `json:"serviceDiscovery,omitempty"`
+	// The backends that the virtual node is expected to send outbound traffic to.
+	// +optional
+	Backends []Backend `json:"backends,omitempty"`
+	// A reference to an object that represents the defaults for backends.
+	// +optional
+	BackendDefaults *BackendDefaults `json:"backendDefaults,omitempty"`
+	// The inbound and outbound access logging information for the virtual node.
+	// +optional
+	Logging *Logging `json:"logging,omitempty"`
+
+	// A reference to k8s Mesh CR that this VirtualNode belongs to.
+	// The admission controller populates it using Meshes's selector, and prevents users from setting this field.
+	//
+	// Populated by the system.
+	// Read-only.
+	// +optional
+	MeshRef *MeshReference `json:"meshRef,omitempty"`
+}
+
+// VirtualNodeStatus defines the observed state of VirtualNode
+type VirtualNodeStatus struct {
+	// VirtualNodeARN is the AppMesh VirtualNode object's Amazon Resource Name
+	// +optional
+	VirtualNodeARN *string `json:"virtualNodeARN,omitempty"`
+	// The current VirtualNode status.
+	// +optional
+	Conditions []VirtualNodeCondition `json:"conditions,omitempty"`
+	// AWSCloudMapServiceStatus is AWS CloudMap Service object's info
+	// +optional
+	AWSCloudMapServiceStatus *AWSCloudMapServiceStatus `json:"awsCloudMapServiceStatus,omitempty"`
+}
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// VirtualNode is the Schema for the virtualnodes API
+type VirtualNode struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   VirtualNodeSpec   `json:"spec,omitempty"`
+	Status VirtualNodeStatus `json:"status,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// VirtualNodeList contains a list of VirtualNode
+type VirtualNodeList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []VirtualNode `json:"items"`
+}
diff --git a/pkg/apis/appmesh/v1beta2/virtualrouter_types.go b/pkg/apis/appmesh/v1beta2/virtualrouter_types.go
new file mode 100644
index 00000000..e4b3674a
--- /dev/null
+++ b/pkg/apis/appmesh/v1beta2/virtualrouter_types.go
@@ -0,0 +1,369 @@
+/*
+
+
+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 v1beta2
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// VirtualRouterListener refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_VirtualRouterListener.html
+type VirtualRouterListener struct {
+	// The port mapping information for the listener.
+	PortMapping PortMapping `json:"portMapping"`
+}
+
+// WeightedTarget refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_WeightedTarget.html
+type WeightedTarget struct {
+	// The virtual node to associate with the weighted target.
+	VirtualNodeRef VirtualNodeReference `json:"virtualNodeRef"`
+	// The relative weight of the weighted target.
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:validation:Maximum=100
+	Weight int64 `json:"weight"`
+}
+
+type MatchRange struct {
+	// The start of the range.
+	// +optional
+	Start int64 `json:"start"`
+	// The end of the range.
+	// +optional
+	End int64 `json:"end"`
+}
+
+// HeaderMatchMethod refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HeaderMatchMethod.html
+type HeaderMatchMethod struct {
+	// The value sent by the client must match the specified value exactly.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	// +optional
+	Exact *string `json:"exact,omitempty"`
+	// The value sent by the client must begin with the specified characters.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	// +optional
+	Prefix *string `json:"prefix,omitempty"`
+	// An object that represents the range of values to match on.
+	// +optional
+	Range *MatchRange `json:"range,omitempty"`
+	// The value sent by the client must include the specified characters.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	// +optional
+	Regex *string `json:"regex,omitempty"`
+	// The value sent by the client must end with the specified characters.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	// +optional
+	Suffix *string `json:"suffix,omitempty"`
+}
+
+// HTTPRouteHeader refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HttpRouteHeader.html
+type HTTPRouteHeader struct {
+	// A name for the HTTP header in the client request that will be matched on.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=50
+	Name string `json:"name"`
+	// The HeaderMatchMethod object.
+	// +optional
+	Match *HeaderMatchMethod `json:"match,omitempty"`
+	// Specify True to match anything except the match criteria. The default value is False.
+	// +optional
+	Invert *bool `json:"invert,omitempty"`
+}
+
+// HTTPRouteMatch refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HttpRouteMatch.html
+type HTTPRouteMatch struct {
+	// An object that represents the client request headers to match on.
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=10
+	// +optional
+	Headers []HTTPRouteHeader `json:"headers,omitempty"`
+	// The client request method to match on.
+	// +kubebuilder:validation:Enum=CONNECT;DELETE;GET;HEAD;OPTIONS;PATCH;POST;PUT;TRACE
+	// +optional
+	Method *string `json:"method,omitempty"`
+	// Specifies the path to match requests with
+	Prefix string `json:"prefix"`
+	// The client request scheme to match on
+	// +kubebuilder:validation:Enum=http;https
+	// +optional
+	Scheme *string `json:"scheme,omitempty"`
+}
+
+// HTTPRouteAction refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HttpRouteAction.html
+type HTTPRouteAction struct {
+	// An object that represents the targets that traffic is routed to when a request matches the route.
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=10
+	WeightedTargets []WeightedTarget `json:"weightedTargets"`
+}
+
+// +kubebuilder:validation:Enum=server-error;gateway-error;client-error;stream-error
+type HTTPRetryPolicyEvent string
+
+// +kubebuilder:validation:Enum=connection-error
+type TCPRetryPolicyEvent string
+
+// +kubebuilder:validation:Enum=cancelled;deadline-exceeded;internal;resource-exhausted;unavailable
+type GRPCRetryPolicyEvent string
+
+// HTTPRetryPolicy refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HttpRetryPolicy.html
+type HTTPRetryPolicy struct {
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=25
+	// +optional
+	HTTPRetryEvents []HTTPRetryPolicyEvent `json:"httpRetryEvents,omitempty"`
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=1
+	// +optional
+	TCPRetryEvents []TCPRetryPolicyEvent `json:"tcpRetryEvents,omitempty"`
+	// The maximum number of retry attempts.
+	// +kubebuilder:validation:Minimum=0
+	MaxRetries int64 `json:"maxRetries"`
+	// An object that represents a duration of time
+	PerRetryTimeout Duration `json:"perRetryTimeout"`
+}
+
+// HTTPRoute refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HttpRoute.html
+type HTTPRoute struct {
+	// An object that represents the criteria for determining a request match.
+	Match HTTPRouteMatch `json:"match"`
+	// An object that represents the action to take if a match is determined.
+	Action HTTPRouteAction `json:"action"`
+	// An object that represents a retry policy.
+	// +optional
+	RetryPolicy *HTTPRetryPolicy `json:"retryPolicy,omitempty"`
+}
+
+// TCPRouteAction refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_TcpRouteAction.html
+type TCPRouteAction struct {
+	// An object that represents the targets that traffic is routed to when a request matches the route.
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=10
+	WeightedTargets []WeightedTarget `json:"weightedTargets"`
+}
+
+// TCPRoute refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_TcpRoute.html
+type TCPRoute struct {
+	// The action to take if a match is determined.
+	Action TCPRouteAction `json:"action"`
+}
+
+// GRPCRouteMetadataMatchMethod refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_GrpcRouteMetadataMatchMethod.html
+type GRPCRouteMetadataMatchMethod struct {
+	// The value sent by the client must match the specified value exactly.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	// +optional
+	Exact *string `json:"exact,omitempty"`
+	// The value sent by the client must begin with the specified characters.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	// +optional
+	Prefix *string `json:"prefix,omitempty"`
+	// An object that represents the range of values to match on
+	// +optional
+	Range *MatchRange `json:"range,omitempty"`
+	// The value sent by the client must include the specified characters.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	// +optional
+	Regex *string `json:"regex,omitempty"`
+	// The value sent by the client must end with the specified characters.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=255
+	// +optional
+	Suffix *string `json:"suffix,omitempty"`
+}
+
+// GRPCRouteMetadata refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_GrpcRouteMetadata.html
+type GRPCRouteMetadata struct {
+	// The name of the route.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=50
+	Name string `json:"name"`
+	// An object that represents the data to match from the request.
+	// +optional
+	Match *GRPCRouteMetadataMatchMethod `json:"match,omitempty"`
+	// Specify True to match anything except the match criteria. The default value is False.
+	// +optional
+	Invert *bool `json:"invert,omitempty"`
+}
+
+// GRPCRouteMatch refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_GrpcRouteMatch.html
+type GRPCRouteMatch struct {
+	// The method name to match from the request. If you specify a name, you must also specify a serviceName.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=50
+	// +optional
+	MethodName *string `json:"methodName,omitempty"`
+	// The fully qualified domain name for the service to match from the request.
+	// +optional
+	ServiceName *string `json:"serviceName,omitempty"`
+	// An object that represents the data to match from the request.
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=10
+	// +optional
+	Metadata []GRPCRouteMetadata `json:"metadata,omitempty"`
+}
+
+// GRPCRouteAction refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_GrpcRouteAction.html
+type GRPCRouteAction struct {
+	// An object that represents the targets that traffic is routed to when a request matches the route.
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=10
+	WeightedTargets []WeightedTarget `json:"weightedTargets"`
+}
+
+// GRPCRetryPolicy refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_GrpcRetryPolicy.html
+type GRPCRetryPolicy struct {
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=5
+	// +optional
+	GRPCRetryEvents []GRPCRetryPolicyEvent `json:"grpcRetryEvents,omitempty"`
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=25
+	// +optional
+	HTTPRetryEvents []HTTPRetryPolicyEvent `json:"httpRetryEvents,omitempty"`
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=1
+	// +optional
+	TCPRetryEvents []TCPRetryPolicyEvent `json:"tcpRetryEvents,omitempty"`
+	// The maximum number of retry attempts.
+	// +kubebuilder:validation:Minimum=0
+	MaxRetries int64 `json:"maxRetries"`
+	// An object that represents a duration of time.
+	PerRetryTimeout Duration `json:"perRetryTimeout"`
+}
+
+// GRPCRoute refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_GrpcRoute.html
+type GRPCRoute struct {
+	// An object that represents the criteria for determining a request match.
+	Match GRPCRouteMatch `json:"match"`
+	// An object that represents the action to take if a match is determined.
+	Action GRPCRouteAction `json:"action"`
+	// An object that represents a retry policy.
+	// +optional
+	RetryPolicy *GRPCRetryPolicy `json:"retryPolicy,omitempty"`
+}
+
+// Route refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_RouteSpec.html
+type Route struct {
+	// Route's name
+	Name string `json:"name"`
+	// An object that represents the specification of a gRPC route.
+	// +optional
+	GRPCRoute *GRPCRoute `json:"grpcRoute,omitempty"`
+	// An object that represents the specification of an HTTP route.
+	// +optional
+	HTTPRoute *HTTPRoute `json:"httpRoute,omitempty"`
+	// An object that represents the specification of an HTTP/2 route.
+	// +optional
+	HTTP2Route *HTTPRoute `json:"http2Route,omitempty"`
+	// An object that represents the specification of a TCP route.
+	// +optional
+	TCPRoute *TCPRoute `json:"tcpRoute,omitempty"`
+	// The priority for the route.
+	// +kubebuilder:validation:Minimum=0
+	// +kubebuilder:validation:Maximum=1000
+	// +optional
+	Priority *int64 `json:"priority,omitempty"`
+}
+
+type VirtualRouterConditionType string
+
+const (
+	// VirtualRouterActive is True when the AppMesh VirtualRouter has been created or found via the API
+	VirtualRouterActive VirtualRouterConditionType = "VirtualRouterActive"
+)
+
+type VirtualRouterCondition struct {
+	// Type of VirtualRouter condition.
+	Type VirtualRouterConditionType `json:"type"`
+	// Status of the condition, one of True, False, Unknown.
+	Status corev1.ConditionStatus `json:"status"`
+	// Last time the condition transitioned from one status to another.
+	// +optional
+	LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"`
+	// The reason for the condition's last transition.
+	// +optional
+	Reason *string `json:"reason,omitempty"`
+	// A human readable message indicating details about the transition.
+	// +optional
+	Message *string `json:"message,omitempty"`
+}
+
+// VirtualRouterSpec defines the desired state of VirtualRouter
+// refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_VirtualRouterSpec.html
+type VirtualRouterSpec struct {
+	// AWSName is the AppMesh VirtualRouter object's name.
+	// If unspecified or empty, it defaults to be "${name}_${namespace}" of k8s VirtualRouter
+	// +optional
+	AWSName *string `json:"awsName,omitempty"`
+	// The listeners that the virtual router is expected to receive inbound traffic from
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=1
+	Listeners []VirtualRouterListener `json:"listeners,omitempty"`
+
+	// The routes associated with VirtualRouter
+	// +optional
+	Routes []Route `json:"routes,omitempty"`
+
+	// A reference to k8s Mesh CR that this VirtualRouter belongs to.
+	// The admission controller populates it using Meshes's selector, and prevents users from setting this field.
+	//
+	// Populated by the system.
+	// Read-only.
+	// +optional
+	MeshRef *MeshReference `json:"meshRef,omitempty"`
+}
+
+// VirtualRouterStatus defines the observed state of VirtualRouter
+type VirtualRouterStatus struct {
+	// VirtualRouterARN is the AppMesh VirtualRouter object's Amazon Resource Name.
+	// +optional
+	VirtualRouterARN *string `json:"virtualRouterARN,omitempty"`
+	// RouteARNs is a map of AppMesh Route objects' Amazon Resource Names, indexed by route name.
+	// +optional
+	RouteARNs map[string]string `json:"routeARNs,omitempty"`
+	// The current VirtualRouter status.
+	// +optional
+	Conditions []VirtualRouterCondition `json:"conditions,omitempty"`
+}
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// VirtualRouter is the Schema for the virtualrouters API
+type VirtualRouter struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   VirtualRouterSpec   `json:"spec,omitempty"`
+	Status VirtualRouterStatus `json:"status,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// VirtualRouterList contains a list of VirtualRouter
+type VirtualRouterList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []VirtualRouter `json:"items"`
+}
diff --git a/pkg/apis/appmesh/v1beta2/virtualservice_types.go b/pkg/apis/appmesh/v1beta2/virtualservice_types.go
new file mode 100644
index 00000000..2c7d9d51
--- /dev/null
+++ b/pkg/apis/appmesh/v1beta2/virtualservice_types.go
@@ -0,0 +1,118 @@
+/*
+
+
+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 v1beta2
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// VirtualNodeServiceProvider refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_VirtualNodeServiceProvider.html
+type VirtualNodeServiceProvider struct {
+	// The virtual node that is acting as a service provider.
+	VirtualNodeRef VirtualNodeReference `json:"virtualNodeRef"`
+}
+
+// VirtualRouterServiceProvider refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_VirtualRouterServiceProvider.html
+type VirtualRouterServiceProvider struct {
+	// The virtual router that is acting as a service provider.
+	VirtualRouterRef VirtualRouterReference `json:"virtualRouterRef"`
+}
+
+// VirtualServiceProvider refers to https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_VirtualServiceProvider.html
+type VirtualServiceProvider struct {
+	// The virtual node associated with a virtual service.
+	// +optional
+	VirtualNode *VirtualNodeServiceProvider `json:"virtualNode,omitempty"`
+	// The virtual router associated with a virtual service.
+	// +optional
+	VirtualRouter *VirtualRouterServiceProvider `json:"virtualRouter,omitempty"`
+}
+
+type VirtualServiceConditionType string
+
+const (
+	// VirtualServiceActive is True when the AppMesh VirtualService has been created or found via the API
+	VirtualServiceActive VirtualServiceConditionType = "VirtualServiceActive"
+)
+
+type VirtualServiceCondition struct {
+	// Type of VirtualService condition.
+	Type VirtualServiceConditionType `json:"type"`
+	// Status of the condition, one of True, False, Unknown.
+	Status corev1.ConditionStatus `json:"status"`
+	// Last time the condition transitioned from one status to another.
+	// +optional
+	LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"`
+	// The reason for the condition's last transition.
+	// +optional
+	Reason *string `json:"reason,omitempty"`
+	// A human readable message indicating details about the transition.
+	// +optional
+	Message *string `json:"message,omitempty"`
+}
+
+// VirtualServiceSpec defines the desired state of VirtualService
+type VirtualServiceSpec struct {
+	// AWSName is the AppMesh VirtualService object's name.
+	// If unspecified or empty, it defaults to be "${name}.${namespace}" of k8s VirtualService
+	// +optional
+	AWSName *string `json:"awsName,omitempty"`
+
+	// The provider for virtual services. You can specify a single virtual node or virtual router.
+	// +optional
+	Provider *VirtualServiceProvider `json:"provider,omitempty"`
+
+	// A reference to k8s Mesh CR that this VirtualService belongs to.
+	// The admission controller populates it using Meshes's selector, and prevents users from setting this field.
+	//
+	// Populated by the system.
+	// Read-only.
+	// +optional
+	MeshRef *MeshReference `json:"meshRef,omitempty"`
+}
+
+// VirtualServiceStatus defines the observed state of VirtualService
+type VirtualServiceStatus struct {
+	// VirtualServiceARN is the AppMesh VirtualService object's Amazon Resource Name.
+	// +optional
+	VirtualServiceARN *string `json:"virtualServiceARN,omitempty"`
+	// The current VirtualService status.
+	// +optional
+	Conditions []VirtualServiceCondition `json:"conditions,omitempty"`
+}
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// VirtualService is the Schema for the virtualservices API
+type VirtualService struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   VirtualServiceSpec   `json:"spec,omitempty"`
+	Status VirtualServiceStatus `json:"status,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// VirtualServiceList contains a list of VirtualService
+type VirtualServiceList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []VirtualService `json:"items"`
+}
diff --git a/pkg/apis/appmesh/v1beta2/zz_generated.deepcopy.go b/pkg/apis/appmesh/v1beta2/zz_generated.deepcopy.go
new file mode 100644
index 00000000..9e5fe7dc
--- /dev/null
+++ b/pkg/apis/appmesh/v1beta2/zz_generated.deepcopy.go
@@ -0,0 +1,1702 @@
+// +build !ignore_autogenerated
+
+/*
+Copyright The Flagger Authors.
+
+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 deepcopy-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AWSCloudMapInstanceAttribute) DeepCopyInto(out *AWSCloudMapInstanceAttribute) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSCloudMapInstanceAttribute.
+func (in *AWSCloudMapInstanceAttribute) DeepCopy() *AWSCloudMapInstanceAttribute {
+	if in == nil {
+		return nil
+	}
+	out := new(AWSCloudMapInstanceAttribute)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AWSCloudMapServiceDiscovery) DeepCopyInto(out *AWSCloudMapServiceDiscovery) {
+	*out = *in
+	if in.Attributes != nil {
+		in, out := &in.Attributes, &out.Attributes
+		*out = make([]AWSCloudMapInstanceAttribute, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSCloudMapServiceDiscovery.
+func (in *AWSCloudMapServiceDiscovery) DeepCopy() *AWSCloudMapServiceDiscovery {
+	if in == nil {
+		return nil
+	}
+	out := new(AWSCloudMapServiceDiscovery)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AWSCloudMapServiceStatus) DeepCopyInto(out *AWSCloudMapServiceStatus) {
+	*out = *in
+	if in.NamespaceID != nil {
+		in, out := &in.NamespaceID, &out.NamespaceID
+		*out = new(string)
+		**out = **in
+	}
+	if in.ServiceID != nil {
+		in, out := &in.ServiceID, &out.ServiceID
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSCloudMapServiceStatus.
+func (in *AWSCloudMapServiceStatus) DeepCopy() *AWSCloudMapServiceStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(AWSCloudMapServiceStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AccessLog) DeepCopyInto(out *AccessLog) {
+	*out = *in
+	if in.File != nil {
+		in, out := &in.File, &out.File
+		*out = new(FileAccessLog)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessLog.
+func (in *AccessLog) DeepCopy() *AccessLog {
+	if in == nil {
+		return nil
+	}
+	out := new(AccessLog)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Backend) DeepCopyInto(out *Backend) {
+	*out = *in
+	in.VirtualService.DeepCopyInto(&out.VirtualService)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backend.
+func (in *Backend) DeepCopy() *Backend {
+	if in == nil {
+		return nil
+	}
+	out := new(Backend)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BackendDefaults) DeepCopyInto(out *BackendDefaults) {
+	*out = *in
+	if in.ClientPolicy != nil {
+		in, out := &in.ClientPolicy, &out.ClientPolicy
+		*out = new(ClientPolicy)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendDefaults.
+func (in *BackendDefaults) DeepCopy() *BackendDefaults {
+	if in == nil {
+		return nil
+	}
+	out := new(BackendDefaults)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClientPolicy) DeepCopyInto(out *ClientPolicy) {
+	*out = *in
+	if in.TLS != nil {
+		in, out := &in.TLS, &out.TLS
+		*out = new(ClientPolicyTLS)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientPolicy.
+func (in *ClientPolicy) DeepCopy() *ClientPolicy {
+	if in == nil {
+		return nil
+	}
+	out := new(ClientPolicy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClientPolicyTLS) DeepCopyInto(out *ClientPolicyTLS) {
+	*out = *in
+	if in.Enforce != nil {
+		in, out := &in.Enforce, &out.Enforce
+		*out = new(bool)
+		**out = **in
+	}
+	if in.Ports != nil {
+		in, out := &in.Ports, &out.Ports
+		*out = make([]PortNumber, len(*in))
+		copy(*out, *in)
+	}
+	in.Validation.DeepCopyInto(&out.Validation)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientPolicyTLS.
+func (in *ClientPolicyTLS) DeepCopy() *ClientPolicyTLS {
+	if in == nil {
+		return nil
+	}
+	out := new(ClientPolicyTLS)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DNSServiceDiscovery) DeepCopyInto(out *DNSServiceDiscovery) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSServiceDiscovery.
+func (in *DNSServiceDiscovery) DeepCopy() *DNSServiceDiscovery {
+	if in == nil {
+		return nil
+	}
+	out := new(DNSServiceDiscovery)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Duration) DeepCopyInto(out *Duration) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Duration.
+func (in *Duration) DeepCopy() *Duration {
+	if in == nil {
+		return nil
+	}
+	out := new(Duration)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FileAccessLog) DeepCopyInto(out *FileAccessLog) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileAccessLog.
+func (in *FileAccessLog) DeepCopy() *FileAccessLog {
+	if in == nil {
+		return nil
+	}
+	out := new(FileAccessLog)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GRPCRetryPolicy) DeepCopyInto(out *GRPCRetryPolicy) {
+	*out = *in
+	if in.GRPCRetryEvents != nil {
+		in, out := &in.GRPCRetryEvents, &out.GRPCRetryEvents
+		*out = make([]GRPCRetryPolicyEvent, len(*in))
+		copy(*out, *in)
+	}
+	if in.HTTPRetryEvents != nil {
+		in, out := &in.HTTPRetryEvents, &out.HTTPRetryEvents
+		*out = make([]HTTPRetryPolicyEvent, len(*in))
+		copy(*out, *in)
+	}
+	if in.TCPRetryEvents != nil {
+		in, out := &in.TCPRetryEvents, &out.TCPRetryEvents
+		*out = make([]TCPRetryPolicyEvent, len(*in))
+		copy(*out, *in)
+	}
+	out.PerRetryTimeout = in.PerRetryTimeout
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCRetryPolicy.
+func (in *GRPCRetryPolicy) DeepCopy() *GRPCRetryPolicy {
+	if in == nil {
+		return nil
+	}
+	out := new(GRPCRetryPolicy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GRPCRoute) DeepCopyInto(out *GRPCRoute) {
+	*out = *in
+	in.Match.DeepCopyInto(&out.Match)
+	in.Action.DeepCopyInto(&out.Action)
+	if in.RetryPolicy != nil {
+		in, out := &in.RetryPolicy, &out.RetryPolicy
+		*out = new(GRPCRetryPolicy)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCRoute.
+func (in *GRPCRoute) DeepCopy() *GRPCRoute {
+	if in == nil {
+		return nil
+	}
+	out := new(GRPCRoute)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GRPCRouteAction) DeepCopyInto(out *GRPCRouteAction) {
+	*out = *in
+	if in.WeightedTargets != nil {
+		in, out := &in.WeightedTargets, &out.WeightedTargets
+		*out = make([]WeightedTarget, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCRouteAction.
+func (in *GRPCRouteAction) DeepCopy() *GRPCRouteAction {
+	if in == nil {
+		return nil
+	}
+	out := new(GRPCRouteAction)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GRPCRouteMatch) DeepCopyInto(out *GRPCRouteMatch) {
+	*out = *in
+	if in.MethodName != nil {
+		in, out := &in.MethodName, &out.MethodName
+		*out = new(string)
+		**out = **in
+	}
+	if in.ServiceName != nil {
+		in, out := &in.ServiceName, &out.ServiceName
+		*out = new(string)
+		**out = **in
+	}
+	if in.Metadata != nil {
+		in, out := &in.Metadata, &out.Metadata
+		*out = make([]GRPCRouteMetadata, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCRouteMatch.
+func (in *GRPCRouteMatch) DeepCopy() *GRPCRouteMatch {
+	if in == nil {
+		return nil
+	}
+	out := new(GRPCRouteMatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GRPCRouteMetadata) DeepCopyInto(out *GRPCRouteMetadata) {
+	*out = *in
+	if in.Match != nil {
+		in, out := &in.Match, &out.Match
+		*out = new(GRPCRouteMetadataMatchMethod)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Invert != nil {
+		in, out := &in.Invert, &out.Invert
+		*out = new(bool)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCRouteMetadata.
+func (in *GRPCRouteMetadata) DeepCopy() *GRPCRouteMetadata {
+	if in == nil {
+		return nil
+	}
+	out := new(GRPCRouteMetadata)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GRPCRouteMetadataMatchMethod) DeepCopyInto(out *GRPCRouteMetadataMatchMethod) {
+	*out = *in
+	if in.Exact != nil {
+		in, out := &in.Exact, &out.Exact
+		*out = new(string)
+		**out = **in
+	}
+	if in.Prefix != nil {
+		in, out := &in.Prefix, &out.Prefix
+		*out = new(string)
+		**out = **in
+	}
+	if in.Range != nil {
+		in, out := &in.Range, &out.Range
+		*out = new(MatchRange)
+		**out = **in
+	}
+	if in.Regex != nil {
+		in, out := &in.Regex, &out.Regex
+		*out = new(string)
+		**out = **in
+	}
+	if in.Suffix != nil {
+		in, out := &in.Suffix, &out.Suffix
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GRPCRouteMetadataMatchMethod.
+func (in *GRPCRouteMetadataMatchMethod) DeepCopy() *GRPCRouteMetadataMatchMethod {
+	if in == nil {
+		return nil
+	}
+	out := new(GRPCRouteMetadataMatchMethod)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTPRetryPolicy) DeepCopyInto(out *HTTPRetryPolicy) {
+	*out = *in
+	if in.HTTPRetryEvents != nil {
+		in, out := &in.HTTPRetryEvents, &out.HTTPRetryEvents
+		*out = make([]HTTPRetryPolicyEvent, len(*in))
+		copy(*out, *in)
+	}
+	if in.TCPRetryEvents != nil {
+		in, out := &in.TCPRetryEvents, &out.TCPRetryEvents
+		*out = make([]TCPRetryPolicyEvent, len(*in))
+		copy(*out, *in)
+	}
+	out.PerRetryTimeout = in.PerRetryTimeout
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRetryPolicy.
+func (in *HTTPRetryPolicy) DeepCopy() *HTTPRetryPolicy {
+	if in == nil {
+		return nil
+	}
+	out := new(HTTPRetryPolicy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) {
+	*out = *in
+	in.Match.DeepCopyInto(&out.Match)
+	in.Action.DeepCopyInto(&out.Action)
+	if in.RetryPolicy != nil {
+		in, out := &in.RetryPolicy, &out.RetryPolicy
+		*out = new(HTTPRetryPolicy)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoute.
+func (in *HTTPRoute) DeepCopy() *HTTPRoute {
+	if in == nil {
+		return nil
+	}
+	out := new(HTTPRoute)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTPRouteAction) DeepCopyInto(out *HTTPRouteAction) {
+	*out = *in
+	if in.WeightedTargets != nil {
+		in, out := &in.WeightedTargets, &out.WeightedTargets
+		*out = make([]WeightedTarget, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteAction.
+func (in *HTTPRouteAction) DeepCopy() *HTTPRouteAction {
+	if in == nil {
+		return nil
+	}
+	out := new(HTTPRouteAction)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTPRouteHeader) DeepCopyInto(out *HTTPRouteHeader) {
+	*out = *in
+	if in.Match != nil {
+		in, out := &in.Match, &out.Match
+		*out = new(HeaderMatchMethod)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Invert != nil {
+		in, out := &in.Invert, &out.Invert
+		*out = new(bool)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteHeader.
+func (in *HTTPRouteHeader) DeepCopy() *HTTPRouteHeader {
+	if in == nil {
+		return nil
+	}
+	out := new(HTTPRouteHeader)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HTTPRouteMatch) DeepCopyInto(out *HTTPRouteMatch) {
+	*out = *in
+	if in.Headers != nil {
+		in, out := &in.Headers, &out.Headers
+		*out = make([]HTTPRouteHeader, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.Method != nil {
+		in, out := &in.Method, &out.Method
+		*out = new(string)
+		**out = **in
+	}
+	if in.Scheme != nil {
+		in, out := &in.Scheme, &out.Scheme
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteMatch.
+func (in *HTTPRouteMatch) DeepCopy() *HTTPRouteMatch {
+	if in == nil {
+		return nil
+	}
+	out := new(HTTPRouteMatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HeaderMatchMethod) DeepCopyInto(out *HeaderMatchMethod) {
+	*out = *in
+	if in.Exact != nil {
+		in, out := &in.Exact, &out.Exact
+		*out = new(string)
+		**out = **in
+	}
+	if in.Prefix != nil {
+		in, out := &in.Prefix, &out.Prefix
+		*out = new(string)
+		**out = **in
+	}
+	if in.Range != nil {
+		in, out := &in.Range, &out.Range
+		*out = new(MatchRange)
+		**out = **in
+	}
+	if in.Regex != nil {
+		in, out := &in.Regex, &out.Regex
+		*out = new(string)
+		**out = **in
+	}
+	if in.Suffix != nil {
+		in, out := &in.Suffix, &out.Suffix
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderMatchMethod.
+func (in *HeaderMatchMethod) DeepCopy() *HeaderMatchMethod {
+	if in == nil {
+		return nil
+	}
+	out := new(HeaderMatchMethod)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HealthCheckPolicy) DeepCopyInto(out *HealthCheckPolicy) {
+	*out = *in
+	if in.HealthyThreshold != nil {
+		in, out := &in.HealthyThreshold, &out.HealthyThreshold
+		*out = new(int64)
+		**out = **in
+	}
+	if in.IntervalMillis != nil {
+		in, out := &in.IntervalMillis, &out.IntervalMillis
+		*out = new(int64)
+		**out = **in
+	}
+	if in.Path != nil {
+		in, out := &in.Path, &out.Path
+		*out = new(string)
+		**out = **in
+	}
+	if in.Port != nil {
+		in, out := &in.Port, &out.Port
+		*out = new(PortNumber)
+		**out = **in
+	}
+	if in.Protocol != nil {
+		in, out := &in.Protocol, &out.Protocol
+		*out = new(PortProtocol)
+		**out = **in
+	}
+	if in.TimeoutMillis != nil {
+		in, out := &in.TimeoutMillis, &out.TimeoutMillis
+		*out = new(int64)
+		**out = **in
+	}
+	if in.UnhealthyThreshold != nil {
+		in, out := &in.UnhealthyThreshold, &out.UnhealthyThreshold
+		*out = new(int64)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckPolicy.
+func (in *HealthCheckPolicy) DeepCopy() *HealthCheckPolicy {
+	if in == nil {
+		return nil
+	}
+	out := new(HealthCheckPolicy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Listener) DeepCopyInto(out *Listener) {
+	*out = *in
+	out.PortMapping = in.PortMapping
+	if in.HealthCheck != nil {
+		in, out := &in.HealthCheck, &out.HealthCheck
+		*out = new(HealthCheckPolicy)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.TLS != nil {
+		in, out := &in.TLS, &out.TLS
+		*out = new(ListenerTLS)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Listener.
+func (in *Listener) DeepCopy() *Listener {
+	if in == nil {
+		return nil
+	}
+	out := new(Listener)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ListenerTLS) DeepCopyInto(out *ListenerTLS) {
+	*out = *in
+	in.Certificate.DeepCopyInto(&out.Certificate)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ListenerTLS.
+func (in *ListenerTLS) DeepCopy() *ListenerTLS {
+	if in == nil {
+		return nil
+	}
+	out := new(ListenerTLS)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ListenerTLSACMCertificate) DeepCopyInto(out *ListenerTLSACMCertificate) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ListenerTLSACMCertificate.
+func (in *ListenerTLSACMCertificate) DeepCopy() *ListenerTLSACMCertificate {
+	if in == nil {
+		return nil
+	}
+	out := new(ListenerTLSACMCertificate)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ListenerTLSCertificate) DeepCopyInto(out *ListenerTLSCertificate) {
+	*out = *in
+	if in.ACM != nil {
+		in, out := &in.ACM, &out.ACM
+		*out = new(ListenerTLSACMCertificate)
+		**out = **in
+	}
+	if in.File != nil {
+		in, out := &in.File, &out.File
+		*out = new(ListenerTLSFileCertificate)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ListenerTLSCertificate.
+func (in *ListenerTLSCertificate) DeepCopy() *ListenerTLSCertificate {
+	if in == nil {
+		return nil
+	}
+	out := new(ListenerTLSCertificate)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ListenerTLSFileCertificate) DeepCopyInto(out *ListenerTLSFileCertificate) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ListenerTLSFileCertificate.
+func (in *ListenerTLSFileCertificate) DeepCopy() *ListenerTLSFileCertificate {
+	if in == nil {
+		return nil
+	}
+	out := new(ListenerTLSFileCertificate)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Logging) DeepCopyInto(out *Logging) {
+	*out = *in
+	if in.AccessLog != nil {
+		in, out := &in.AccessLog, &out.AccessLog
+		*out = new(AccessLog)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Logging.
+func (in *Logging) DeepCopy() *Logging {
+	if in == nil {
+		return nil
+	}
+	out := new(Logging)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MatchRange) DeepCopyInto(out *MatchRange) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchRange.
+func (in *MatchRange) DeepCopy() *MatchRange {
+	if in == nil {
+		return nil
+	}
+	out := new(MatchRange)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MeshReference) DeepCopyInto(out *MeshReference) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MeshReference.
+func (in *MeshReference) DeepCopy() *MeshReference {
+	if in == nil {
+		return nil
+	}
+	out := new(MeshReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PortMapping) DeepCopyInto(out *PortMapping) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PortMapping.
+func (in *PortMapping) DeepCopy() *PortMapping {
+	if in == nil {
+		return nil
+	}
+	out := new(PortMapping)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Route) DeepCopyInto(out *Route) {
+	*out = *in
+	if in.GRPCRoute != nil {
+		in, out := &in.GRPCRoute, &out.GRPCRoute
+		*out = new(GRPCRoute)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.HTTPRoute != nil {
+		in, out := &in.HTTPRoute, &out.HTTPRoute
+		*out = new(HTTPRoute)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.HTTP2Route != nil {
+		in, out := &in.HTTP2Route, &out.HTTP2Route
+		*out = new(HTTPRoute)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.TCPRoute != nil {
+		in, out := &in.TCPRoute, &out.TCPRoute
+		*out = new(TCPRoute)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Priority != nil {
+		in, out := &in.Priority, &out.Priority
+		*out = new(int64)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route.
+func (in *Route) DeepCopy() *Route {
+	if in == nil {
+		return nil
+	}
+	out := new(Route)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServiceDiscovery) DeepCopyInto(out *ServiceDiscovery) {
+	*out = *in
+	if in.AWSCloudMap != nil {
+		in, out := &in.AWSCloudMap, &out.AWSCloudMap
+		*out = new(AWSCloudMapServiceDiscovery)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.DNS != nil {
+		in, out := &in.DNS, &out.DNS
+		*out = new(DNSServiceDiscovery)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceDiscovery.
+func (in *ServiceDiscovery) DeepCopy() *ServiceDiscovery {
+	if in == nil {
+		return nil
+	}
+	out := new(ServiceDiscovery)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TCPRoute) DeepCopyInto(out *TCPRoute) {
+	*out = *in
+	in.Action.DeepCopyInto(&out.Action)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPRoute.
+func (in *TCPRoute) DeepCopy() *TCPRoute {
+	if in == nil {
+		return nil
+	}
+	out := new(TCPRoute)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TCPRouteAction) DeepCopyInto(out *TCPRouteAction) {
+	*out = *in
+	if in.WeightedTargets != nil {
+		in, out := &in.WeightedTargets, &out.WeightedTargets
+		*out = make([]WeightedTarget, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPRouteAction.
+func (in *TCPRouteAction) DeepCopy() *TCPRouteAction {
+	if in == nil {
+		return nil
+	}
+	out := new(TCPRouteAction)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TLSValidationContext) DeepCopyInto(out *TLSValidationContext) {
+	*out = *in
+	in.Trust.DeepCopyInto(&out.Trust)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSValidationContext.
+func (in *TLSValidationContext) DeepCopy() *TLSValidationContext {
+	if in == nil {
+		return nil
+	}
+	out := new(TLSValidationContext)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TLSValidationContextACMTrust) DeepCopyInto(out *TLSValidationContextACMTrust) {
+	*out = *in
+	if in.CertificateAuthorityARNs != nil {
+		in, out := &in.CertificateAuthorityARNs, &out.CertificateAuthorityARNs
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSValidationContextACMTrust.
+func (in *TLSValidationContextACMTrust) DeepCopy() *TLSValidationContextACMTrust {
+	if in == nil {
+		return nil
+	}
+	out := new(TLSValidationContextACMTrust)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TLSValidationContextFileTrust) DeepCopyInto(out *TLSValidationContextFileTrust) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSValidationContextFileTrust.
+func (in *TLSValidationContextFileTrust) DeepCopy() *TLSValidationContextFileTrust {
+	if in == nil {
+		return nil
+	}
+	out := new(TLSValidationContextFileTrust)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TLSValidationContextTrust) DeepCopyInto(out *TLSValidationContextTrust) {
+	*out = *in
+	if in.ACM != nil {
+		in, out := &in.ACM, &out.ACM
+		*out = new(TLSValidationContextACMTrust)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.File != nil {
+		in, out := &in.File, &out.File
+		*out = new(TLSValidationContextFileTrust)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSValidationContextTrust.
+func (in *TLSValidationContextTrust) DeepCopy() *TLSValidationContextTrust {
+	if in == nil {
+		return nil
+	}
+	out := new(TLSValidationContextTrust)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualGatewayReference) DeepCopyInto(out *VirtualGatewayReference) {
+	*out = *in
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualGatewayReference.
+func (in *VirtualGatewayReference) DeepCopy() *VirtualGatewayReference {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualGatewayReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualNode) DeepCopyInto(out *VirtualNode) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualNode.
+func (in *VirtualNode) DeepCopy() *VirtualNode {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualNode)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VirtualNode) 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 *VirtualNodeCondition) DeepCopyInto(out *VirtualNodeCondition) {
+	*out = *in
+	if in.LastTransitionTime != nil {
+		in, out := &in.LastTransitionTime, &out.LastTransitionTime
+		*out = (*in).DeepCopy()
+	}
+	if in.Reason != nil {
+		in, out := &in.Reason, &out.Reason
+		*out = new(string)
+		**out = **in
+	}
+	if in.Message != nil {
+		in, out := &in.Message, &out.Message
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualNodeCondition.
+func (in *VirtualNodeCondition) DeepCopy() *VirtualNodeCondition {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualNodeCondition)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualNodeList) DeepCopyInto(out *VirtualNodeList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]VirtualNode, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualNodeList.
+func (in *VirtualNodeList) DeepCopy() *VirtualNodeList {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualNodeList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VirtualNodeList) 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 *VirtualNodeReference) DeepCopyInto(out *VirtualNodeReference) {
+	*out = *in
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualNodeReference.
+func (in *VirtualNodeReference) DeepCopy() *VirtualNodeReference {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualNodeReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualNodeServiceProvider) DeepCopyInto(out *VirtualNodeServiceProvider) {
+	*out = *in
+	in.VirtualNodeRef.DeepCopyInto(&out.VirtualNodeRef)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualNodeServiceProvider.
+func (in *VirtualNodeServiceProvider) DeepCopy() *VirtualNodeServiceProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualNodeServiceProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualNodeSpec) DeepCopyInto(out *VirtualNodeSpec) {
+	*out = *in
+	if in.AWSName != nil {
+		in, out := &in.AWSName, &out.AWSName
+		*out = new(string)
+		**out = **in
+	}
+	if in.PodSelector != nil {
+		in, out := &in.PodSelector, &out.PodSelector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Listeners != nil {
+		in, out := &in.Listeners, &out.Listeners
+		*out = make([]Listener, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.ServiceDiscovery != nil {
+		in, out := &in.ServiceDiscovery, &out.ServiceDiscovery
+		*out = new(ServiceDiscovery)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Backends != nil {
+		in, out := &in.Backends, &out.Backends
+		*out = make([]Backend, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.BackendDefaults != nil {
+		in, out := &in.BackendDefaults, &out.BackendDefaults
+		*out = new(BackendDefaults)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Logging != nil {
+		in, out := &in.Logging, &out.Logging
+		*out = new(Logging)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.MeshRef != nil {
+		in, out := &in.MeshRef, &out.MeshRef
+		*out = new(MeshReference)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualNodeSpec.
+func (in *VirtualNodeSpec) DeepCopy() *VirtualNodeSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualNodeSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualNodeStatus) DeepCopyInto(out *VirtualNodeStatus) {
+	*out = *in
+	if in.VirtualNodeARN != nil {
+		in, out := &in.VirtualNodeARN, &out.VirtualNodeARN
+		*out = new(string)
+		**out = **in
+	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]VirtualNodeCondition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.AWSCloudMapServiceStatus != nil {
+		in, out := &in.AWSCloudMapServiceStatus, &out.AWSCloudMapServiceStatus
+		*out = new(AWSCloudMapServiceStatus)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualNodeStatus.
+func (in *VirtualNodeStatus) DeepCopy() *VirtualNodeStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualNodeStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualRouter) DeepCopyInto(out *VirtualRouter) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualRouter.
+func (in *VirtualRouter) DeepCopy() *VirtualRouter {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualRouter)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VirtualRouter) 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 *VirtualRouterCondition) DeepCopyInto(out *VirtualRouterCondition) {
+	*out = *in
+	if in.LastTransitionTime != nil {
+		in, out := &in.LastTransitionTime, &out.LastTransitionTime
+		*out = (*in).DeepCopy()
+	}
+	if in.Reason != nil {
+		in, out := &in.Reason, &out.Reason
+		*out = new(string)
+		**out = **in
+	}
+	if in.Message != nil {
+		in, out := &in.Message, &out.Message
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualRouterCondition.
+func (in *VirtualRouterCondition) DeepCopy() *VirtualRouterCondition {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualRouterCondition)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualRouterList) DeepCopyInto(out *VirtualRouterList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]VirtualRouter, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualRouterList.
+func (in *VirtualRouterList) DeepCopy() *VirtualRouterList {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualRouterList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VirtualRouterList) 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 *VirtualRouterListener) DeepCopyInto(out *VirtualRouterListener) {
+	*out = *in
+	out.PortMapping = in.PortMapping
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualRouterListener.
+func (in *VirtualRouterListener) DeepCopy() *VirtualRouterListener {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualRouterListener)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualRouterReference) DeepCopyInto(out *VirtualRouterReference) {
+	*out = *in
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualRouterReference.
+func (in *VirtualRouterReference) DeepCopy() *VirtualRouterReference {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualRouterReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualRouterServiceProvider) DeepCopyInto(out *VirtualRouterServiceProvider) {
+	*out = *in
+	in.VirtualRouterRef.DeepCopyInto(&out.VirtualRouterRef)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualRouterServiceProvider.
+func (in *VirtualRouterServiceProvider) DeepCopy() *VirtualRouterServiceProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualRouterServiceProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualRouterSpec) DeepCopyInto(out *VirtualRouterSpec) {
+	*out = *in
+	if in.AWSName != nil {
+		in, out := &in.AWSName, &out.AWSName
+		*out = new(string)
+		**out = **in
+	}
+	if in.Listeners != nil {
+		in, out := &in.Listeners, &out.Listeners
+		*out = make([]VirtualRouterListener, len(*in))
+		copy(*out, *in)
+	}
+	if in.Routes != nil {
+		in, out := &in.Routes, &out.Routes
+		*out = make([]Route, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.MeshRef != nil {
+		in, out := &in.MeshRef, &out.MeshRef
+		*out = new(MeshReference)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualRouterSpec.
+func (in *VirtualRouterSpec) DeepCopy() *VirtualRouterSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualRouterSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualRouterStatus) DeepCopyInto(out *VirtualRouterStatus) {
+	*out = *in
+	if in.VirtualRouterARN != nil {
+		in, out := &in.VirtualRouterARN, &out.VirtualRouterARN
+		*out = new(string)
+		**out = **in
+	}
+	if in.RouteARNs != nil {
+		in, out := &in.RouteARNs, &out.RouteARNs
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]VirtualRouterCondition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualRouterStatus.
+func (in *VirtualRouterStatus) DeepCopy() *VirtualRouterStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualRouterStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualService) DeepCopyInto(out *VirtualService) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualService.
+func (in *VirtualService) DeepCopy() *VirtualService {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualService)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VirtualService) 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 *VirtualServiceBackend) DeepCopyInto(out *VirtualServiceBackend) {
+	*out = *in
+	in.VirtualServiceRef.DeepCopyInto(&out.VirtualServiceRef)
+	if in.ClientPolicy != nil {
+		in, out := &in.ClientPolicy, &out.ClientPolicy
+		*out = new(ClientPolicy)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceBackend.
+func (in *VirtualServiceBackend) DeepCopy() *VirtualServiceBackend {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualServiceBackend)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualServiceCondition) DeepCopyInto(out *VirtualServiceCondition) {
+	*out = *in
+	if in.LastTransitionTime != nil {
+		in, out := &in.LastTransitionTime, &out.LastTransitionTime
+		*out = (*in).DeepCopy()
+	}
+	if in.Reason != nil {
+		in, out := &in.Reason, &out.Reason
+		*out = new(string)
+		**out = **in
+	}
+	if in.Message != nil {
+		in, out := &in.Message, &out.Message
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceCondition.
+func (in *VirtualServiceCondition) DeepCopy() *VirtualServiceCondition {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualServiceCondition)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualServiceList) DeepCopyInto(out *VirtualServiceList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]VirtualService, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceList.
+func (in *VirtualServiceList) DeepCopy() *VirtualServiceList {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualServiceList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VirtualServiceList) 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 *VirtualServiceProvider) DeepCopyInto(out *VirtualServiceProvider) {
+	*out = *in
+	if in.VirtualNode != nil {
+		in, out := &in.VirtualNode, &out.VirtualNode
+		*out = new(VirtualNodeServiceProvider)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.VirtualRouter != nil {
+		in, out := &in.VirtualRouter, &out.VirtualRouter
+		*out = new(VirtualRouterServiceProvider)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceProvider.
+func (in *VirtualServiceProvider) DeepCopy() *VirtualServiceProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualServiceProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualServiceReference) DeepCopyInto(out *VirtualServiceReference) {
+	*out = *in
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceReference.
+func (in *VirtualServiceReference) DeepCopy() *VirtualServiceReference {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualServiceReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualServiceSpec) DeepCopyInto(out *VirtualServiceSpec) {
+	*out = *in
+	if in.AWSName != nil {
+		in, out := &in.AWSName, &out.AWSName
+		*out = new(string)
+		**out = **in
+	}
+	if in.Provider != nil {
+		in, out := &in.Provider, &out.Provider
+		*out = new(VirtualServiceProvider)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.MeshRef != nil {
+		in, out := &in.MeshRef, &out.MeshRef
+		*out = new(MeshReference)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceSpec.
+func (in *VirtualServiceSpec) DeepCopy() *VirtualServiceSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualServiceSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VirtualServiceStatus) DeepCopyInto(out *VirtualServiceStatus) {
+	*out = *in
+	if in.VirtualServiceARN != nil {
+		in, out := &in.VirtualServiceARN, &out.VirtualServiceARN
+		*out = new(string)
+		**out = **in
+	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]VirtualServiceCondition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceStatus.
+func (in *VirtualServiceStatus) DeepCopy() *VirtualServiceStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(VirtualServiceStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WeightedTarget) DeepCopyInto(out *WeightedTarget) {
+	*out = *in
+	in.VirtualNodeRef.DeepCopyInto(&out.VirtualNodeRef)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightedTarget.
+func (in *WeightedTarget) DeepCopy() *WeightedTarget {
+	if in == nil {
+		return nil
+	}
+	out := new(WeightedTarget)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/pkg/apis/flagger/v1beta1/provider.go b/pkg/apis/flagger/v1beta1/provider.go
new file mode 100644
index 00000000..9dbcf017
--- /dev/null
+++ b/pkg/apis/flagger/v1beta1/provider.go
@@ -0,0 +1,12 @@
+package v1beta1
+
+const (
+	AppMeshProvider    string = "appmesh"
+	LinkerdProvider    string = "linkerd"
+	IstioProvider      string = "istio"
+	SMIProvider        string = "smi"
+	ContourProvider    string = "contour"
+	GlooProvider       string = "gloo"
+	NGINXProvider      string = "nginx"
+	KubernetesProvider string = "kubernetes"
+)
diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go
index b6b73b42..fc0b22c3 100644
--- a/pkg/client/clientset/versioned/clientset.go
+++ b/pkg/client/clientset/versioned/clientset.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 
 	appmeshv1beta1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/appmesh/v1beta1"
+	appmeshv1beta2 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/appmesh/v1beta2"
 	flaggerv1beta1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/flagger/v1beta1"
 	gloov1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/gloo/v1"
 	networkingv1alpha3 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/istio/v1alpha3"
@@ -35,6 +36,7 @@ import (
 
 type Interface interface {
 	Discovery() discovery.DiscoveryInterface
+	AppmeshV1beta2() appmeshv1beta2.AppmeshV1beta2Interface
 	AppmeshV1beta1() appmeshv1beta1.AppmeshV1beta1Interface
 	FlaggerV1beta1() flaggerv1beta1.FlaggerV1beta1Interface
 	GlooV1() gloov1.GlooV1Interface
@@ -48,6 +50,7 @@ type Interface interface {
 // version included in a Clientset.
 type Clientset struct {
 	*discovery.DiscoveryClient
+	appmeshV1beta2     *appmeshv1beta2.AppmeshV1beta2Client
 	appmeshV1beta1     *appmeshv1beta1.AppmeshV1beta1Client
 	flaggerV1beta1     *flaggerv1beta1.FlaggerV1beta1Client
 	glooV1             *gloov1.GlooV1Client
@@ -57,6 +60,11 @@ type Clientset struct {
 	splitV1alpha2      *splitv1alpha2.SplitV1alpha2Client
 }
 
+// AppmeshV1beta2 retrieves the AppmeshV1beta2Client
+func (c *Clientset) AppmeshV1beta2() appmeshv1beta2.AppmeshV1beta2Interface {
+	return c.appmeshV1beta2
+}
+
 // AppmeshV1beta1 retrieves the AppmeshV1beta1Client
 func (c *Clientset) AppmeshV1beta1() appmeshv1beta1.AppmeshV1beta1Interface {
 	return c.appmeshV1beta1
@@ -113,6 +121,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
 	}
 	var cs Clientset
 	var err error
+	cs.appmeshV1beta2, err = appmeshv1beta2.NewForConfig(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
 	cs.appmeshV1beta1, err = appmeshv1beta1.NewForConfig(&configShallowCopy)
 	if err != nil {
 		return nil, err
@@ -153,6 +165,7 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
 // panics if there is an error in the config.
 func NewForConfigOrDie(c *rest.Config) *Clientset {
 	var cs Clientset
+	cs.appmeshV1beta2 = appmeshv1beta2.NewForConfigOrDie(c)
 	cs.appmeshV1beta1 = appmeshv1beta1.NewForConfigOrDie(c)
 	cs.flaggerV1beta1 = flaggerv1beta1.NewForConfigOrDie(c)
 	cs.glooV1 = gloov1.NewForConfigOrDie(c)
@@ -168,6 +181,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset {
 // New creates a new Clientset for the given RESTClient.
 func New(c rest.Interface) *Clientset {
 	var cs Clientset
+	cs.appmeshV1beta2 = appmeshv1beta2.New(c)
 	cs.appmeshV1beta1 = appmeshv1beta1.New(c)
 	cs.flaggerV1beta1 = flaggerv1beta1.New(c)
 	cs.glooV1 = gloov1.New(c)
diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go
index bf760012..d68d1142 100644
--- a/pkg/client/clientset/versioned/fake/clientset_generated.go
+++ b/pkg/client/clientset/versioned/fake/clientset_generated.go
@@ -22,6 +22,8 @@ import (
 	clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
 	appmeshv1beta1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/appmesh/v1beta1"
 	fakeappmeshv1beta1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/appmesh/v1beta1/fake"
+	appmeshv1beta2 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/appmesh/v1beta2"
+	fakeappmeshv1beta2 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake"
 	flaggerv1beta1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/flagger/v1beta1"
 	fakeflaggerv1beta1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake"
 	gloov1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/gloo/v1"
@@ -88,6 +90,11 @@ func (c *Clientset) Tracker() testing.ObjectTracker {
 
 var _ clientset.Interface = &Clientset{}
 
+// AppmeshV1beta2 retrieves the AppmeshV1beta2Client
+func (c *Clientset) AppmeshV1beta2() appmeshv1beta2.AppmeshV1beta2Interface {
+	return &fakeappmeshv1beta2.FakeAppmeshV1beta2{Fake: &c.Fake}
+}
+
 // AppmeshV1beta1 retrieves the AppmeshV1beta1Client
 func (c *Clientset) AppmeshV1beta1() appmeshv1beta1.AppmeshV1beta1Interface {
 	return &fakeappmeshv1beta1.FakeAppmeshV1beta1{Fake: &c.Fake}
diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go
index 07d2d182..e5ab88c5 100644
--- a/pkg/client/clientset/versioned/fake/register.go
+++ b/pkg/client/clientset/versioned/fake/register.go
@@ -20,6 +20,7 @@ package fake
 
 import (
 	appmeshv1beta1 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta1"
+	appmeshv1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
 	flaggerv1beta1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
 	gloov1 "github.com/weaveworks/flagger/pkg/apis/gloo/v1"
 	networkingv1alpha3 "github.com/weaveworks/flagger/pkg/apis/istio/v1alpha3"
@@ -37,6 +38,7 @@ var scheme = runtime.NewScheme()
 var codecs = serializer.NewCodecFactory(scheme)
 var parameterCodec = runtime.NewParameterCodec(scheme)
 var localSchemeBuilder = runtime.SchemeBuilder{
+	appmeshv1beta2.AddToScheme,
 	appmeshv1beta1.AddToScheme,
 	flaggerv1beta1.AddToScheme,
 	gloov1.AddToScheme,
diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go
index d27b9397..19b1677c 100644
--- a/pkg/client/clientset/versioned/scheme/register.go
+++ b/pkg/client/clientset/versioned/scheme/register.go
@@ -20,6 +20,7 @@ package scheme
 
 import (
 	appmeshv1beta1 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta1"
+	appmeshv1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
 	flaggerv1beta1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
 	gloov1 "github.com/weaveworks/flagger/pkg/apis/gloo/v1"
 	networkingv1alpha3 "github.com/weaveworks/flagger/pkg/apis/istio/v1alpha3"
@@ -37,6 +38,7 @@ var Scheme = runtime.NewScheme()
 var Codecs = serializer.NewCodecFactory(Scheme)
 var ParameterCodec = runtime.NewParameterCodec(Scheme)
 var localSchemeBuilder = runtime.SchemeBuilder{
+	appmeshv1beta2.AddToScheme,
 	appmeshv1beta1.AddToScheme,
 	flaggerv1beta1.AddToScheme,
 	gloov1.AddToScheme,
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/appmesh_client.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/appmesh_client.go
new file mode 100644
index 00000000..4af19f9f
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/appmesh_client.go
@@ -0,0 +1,99 @@
+/*
+Copyright The Flagger Authors.
+
+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 v1beta2
+
+import (
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	"github.com/weaveworks/flagger/pkg/client/clientset/versioned/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type AppmeshV1beta2Interface interface {
+	RESTClient() rest.Interface
+	VirtualNodesGetter
+	VirtualRoutersGetter
+	VirtualServicesGetter
+}
+
+// AppmeshV1beta2Client is used to interact with features provided by the appmesh.k8s.aws group.
+type AppmeshV1beta2Client struct {
+	restClient rest.Interface
+}
+
+func (c *AppmeshV1beta2Client) VirtualNodes(namespace string) VirtualNodeInterface {
+	return newVirtualNodes(c, namespace)
+}
+
+func (c *AppmeshV1beta2Client) VirtualRouters(namespace string) VirtualRouterInterface {
+	return newVirtualRouters(c, namespace)
+}
+
+func (c *AppmeshV1beta2Client) VirtualServices(namespace string) VirtualServiceInterface {
+	return newVirtualServices(c, namespace)
+}
+
+// NewForConfig creates a new AppmeshV1beta2Client for the given config.
+func NewForConfig(c *rest.Config) (*AppmeshV1beta2Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return &AppmeshV1beta2Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new AppmeshV1beta2Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *AppmeshV1beta2Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new AppmeshV1beta2Client for the given RESTClient.
+func New(c rest.Interface) *AppmeshV1beta2Client {
+	return &AppmeshV1beta2Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1beta2.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *AppmeshV1beta2Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/doc.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/doc.go
new file mode 100644
index 00000000..a36957cd
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Flagger Authors.
+
+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.
+
+// This package has the automatically generated typed clients.
+package v1beta2
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/doc.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/doc.go
new file mode 100644
index 00000000..7a3b19cb
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Flagger Authors.
+
+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 has the automatically generated clients.
+package fake
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_appmesh_client.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_appmesh_client.go
new file mode 100644
index 00000000..ad03bad8
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_appmesh_client.go
@@ -0,0 +1,48 @@
+/*
+Copyright The Flagger Authors.
+
+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 (
+	v1beta2 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/appmesh/v1beta2"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeAppmeshV1beta2 struct {
+	*testing.Fake
+}
+
+func (c *FakeAppmeshV1beta2) VirtualNodes(namespace string) v1beta2.VirtualNodeInterface {
+	return &FakeVirtualNodes{c, namespace}
+}
+
+func (c *FakeAppmeshV1beta2) VirtualRouters(namespace string) v1beta2.VirtualRouterInterface {
+	return &FakeVirtualRouters{c, namespace}
+}
+
+func (c *FakeAppmeshV1beta2) VirtualServices(namespace string) v1beta2.VirtualServiceInterface {
+	return &FakeVirtualServices{c, namespace}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeAppmeshV1beta2) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualnode.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualnode.go
new file mode 100644
index 00000000..96d63230
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualnode.go
@@ -0,0 +1,142 @@
+/*
+Copyright The Flagger Authors.
+
+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"
+
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	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"
+)
+
+// FakeVirtualNodes implements VirtualNodeInterface
+type FakeVirtualNodes struct {
+	Fake *FakeAppmeshV1beta2
+	ns   string
+}
+
+var virtualnodesResource = schema.GroupVersionResource{Group: "appmesh.k8s.aws", Version: "v1beta2", Resource: "virtualnodes"}
+
+var virtualnodesKind = schema.GroupVersionKind{Group: "appmesh.k8s.aws", Version: "v1beta2", Kind: "VirtualNode"}
+
+// Get takes name of the virtualNode, and returns the corresponding virtualNode object, and an error if there is any.
+func (c *FakeVirtualNodes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.VirtualNode, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(virtualnodesResource, c.ns, name), &v1beta2.VirtualNode{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualNode), err
+}
+
+// List takes label and field selectors, and returns the list of VirtualNodes that match those selectors.
+func (c *FakeVirtualNodes) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.VirtualNodeList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(virtualnodesResource, virtualnodesKind, c.ns, opts), &v1beta2.VirtualNodeList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta2.VirtualNodeList{ListMeta: obj.(*v1beta2.VirtualNodeList).ListMeta}
+	for _, item := range obj.(*v1beta2.VirtualNodeList).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 virtualNodes.
+func (c *FakeVirtualNodes) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(virtualnodesResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a virtualNode and creates it.  Returns the server's representation of the virtualNode, and an error, if there is any.
+func (c *FakeVirtualNodes) Create(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.CreateOptions) (result *v1beta2.VirtualNode, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(virtualnodesResource, c.ns, virtualNode), &v1beta2.VirtualNode{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualNode), err
+}
+
+// Update takes the representation of a virtualNode and updates it. Returns the server's representation of the virtualNode, and an error, if there is any.
+func (c *FakeVirtualNodes) Update(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.UpdateOptions) (result *v1beta2.VirtualNode, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(virtualnodesResource, c.ns, virtualNode), &v1beta2.VirtualNode{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualNode), 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 *FakeVirtualNodes) UpdateStatus(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.UpdateOptions) (*v1beta2.VirtualNode, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(virtualnodesResource, "status", c.ns, virtualNode), &v1beta2.VirtualNode{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualNode), err
+}
+
+// Delete takes name of the virtualNode and deletes it. Returns an error if one occurs.
+func (c *FakeVirtualNodes) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(virtualnodesResource, c.ns, name), &v1beta2.VirtualNode{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeVirtualNodes) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(virtualnodesResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1beta2.VirtualNodeList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched virtualNode.
+func (c *FakeVirtualNodes) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.VirtualNode, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(virtualnodesResource, c.ns, name, pt, data, subresources...), &v1beta2.VirtualNode{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualNode), err
+}
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualrouter.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualrouter.go
new file mode 100644
index 00000000..faf658da
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualrouter.go
@@ -0,0 +1,142 @@
+/*
+Copyright The Flagger Authors.
+
+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"
+
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	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"
+)
+
+// FakeVirtualRouters implements VirtualRouterInterface
+type FakeVirtualRouters struct {
+	Fake *FakeAppmeshV1beta2
+	ns   string
+}
+
+var virtualroutersResource = schema.GroupVersionResource{Group: "appmesh.k8s.aws", Version: "v1beta2", Resource: "virtualrouters"}
+
+var virtualroutersKind = schema.GroupVersionKind{Group: "appmesh.k8s.aws", Version: "v1beta2", Kind: "VirtualRouter"}
+
+// Get takes name of the virtualRouter, and returns the corresponding virtualRouter object, and an error if there is any.
+func (c *FakeVirtualRouters) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.VirtualRouter, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(virtualroutersResource, c.ns, name), &v1beta2.VirtualRouter{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualRouter), err
+}
+
+// List takes label and field selectors, and returns the list of VirtualRouters that match those selectors.
+func (c *FakeVirtualRouters) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.VirtualRouterList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(virtualroutersResource, virtualroutersKind, c.ns, opts), &v1beta2.VirtualRouterList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta2.VirtualRouterList{ListMeta: obj.(*v1beta2.VirtualRouterList).ListMeta}
+	for _, item := range obj.(*v1beta2.VirtualRouterList).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 virtualRouters.
+func (c *FakeVirtualRouters) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(virtualroutersResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a virtualRouter and creates it.  Returns the server's representation of the virtualRouter, and an error, if there is any.
+func (c *FakeVirtualRouters) Create(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.CreateOptions) (result *v1beta2.VirtualRouter, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(virtualroutersResource, c.ns, virtualRouter), &v1beta2.VirtualRouter{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualRouter), err
+}
+
+// Update takes the representation of a virtualRouter and updates it. Returns the server's representation of the virtualRouter, and an error, if there is any.
+func (c *FakeVirtualRouters) Update(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.UpdateOptions) (result *v1beta2.VirtualRouter, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(virtualroutersResource, c.ns, virtualRouter), &v1beta2.VirtualRouter{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualRouter), 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 *FakeVirtualRouters) UpdateStatus(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.UpdateOptions) (*v1beta2.VirtualRouter, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(virtualroutersResource, "status", c.ns, virtualRouter), &v1beta2.VirtualRouter{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualRouter), err
+}
+
+// Delete takes name of the virtualRouter and deletes it. Returns an error if one occurs.
+func (c *FakeVirtualRouters) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(virtualroutersResource, c.ns, name), &v1beta2.VirtualRouter{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeVirtualRouters) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(virtualroutersResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1beta2.VirtualRouterList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched virtualRouter.
+func (c *FakeVirtualRouters) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.VirtualRouter, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(virtualroutersResource, c.ns, name, pt, data, subresources...), &v1beta2.VirtualRouter{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualRouter), err
+}
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualservice.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualservice.go
new file mode 100644
index 00000000..25203962
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/fake/fake_virtualservice.go
@@ -0,0 +1,142 @@
+/*
+Copyright The Flagger Authors.
+
+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"
+
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	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"
+)
+
+// FakeVirtualServices implements VirtualServiceInterface
+type FakeVirtualServices struct {
+	Fake *FakeAppmeshV1beta2
+	ns   string
+}
+
+var virtualservicesResource = schema.GroupVersionResource{Group: "appmesh.k8s.aws", Version: "v1beta2", Resource: "virtualservices"}
+
+var virtualservicesKind = schema.GroupVersionKind{Group: "appmesh.k8s.aws", Version: "v1beta2", Kind: "VirtualService"}
+
+// Get takes name of the virtualService, and returns the corresponding virtualService object, and an error if there is any.
+func (c *FakeVirtualServices) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.VirtualService, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(virtualservicesResource, c.ns, name), &v1beta2.VirtualService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualService), err
+}
+
+// List takes label and field selectors, and returns the list of VirtualServices that match those selectors.
+func (c *FakeVirtualServices) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.VirtualServiceList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(virtualservicesResource, virtualservicesKind, c.ns, opts), &v1beta2.VirtualServiceList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta2.VirtualServiceList{ListMeta: obj.(*v1beta2.VirtualServiceList).ListMeta}
+	for _, item := range obj.(*v1beta2.VirtualServiceList).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 virtualServices.
+func (c *FakeVirtualServices) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(virtualservicesResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a virtualService and creates it.  Returns the server's representation of the virtualService, and an error, if there is any.
+func (c *FakeVirtualServices) Create(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.CreateOptions) (result *v1beta2.VirtualService, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(virtualservicesResource, c.ns, virtualService), &v1beta2.VirtualService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualService), err
+}
+
+// Update takes the representation of a virtualService and updates it. Returns the server's representation of the virtualService, and an error, if there is any.
+func (c *FakeVirtualServices) Update(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.UpdateOptions) (result *v1beta2.VirtualService, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(virtualservicesResource, c.ns, virtualService), &v1beta2.VirtualService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualService), 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 *FakeVirtualServices) UpdateStatus(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.UpdateOptions) (*v1beta2.VirtualService, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(virtualservicesResource, "status", c.ns, virtualService), &v1beta2.VirtualService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualService), err
+}
+
+// Delete takes name of the virtualService and deletes it. Returns an error if one occurs.
+func (c *FakeVirtualServices) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(virtualservicesResource, c.ns, name), &v1beta2.VirtualService{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeVirtualServices) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(virtualservicesResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1beta2.VirtualServiceList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched virtualService.
+func (c *FakeVirtualServices) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.VirtualService, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(virtualservicesResource, c.ns, name, pt, data, subresources...), &v1beta2.VirtualService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.VirtualService), err
+}
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/generated_expansion.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/generated_expansion.go
new file mode 100644
index 00000000..7803347c
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/generated_expansion.go
@@ -0,0 +1,25 @@
+/*
+Copyright The Flagger Authors.
+
+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 v1beta2
+
+type VirtualNodeExpansion interface{}
+
+type VirtualRouterExpansion interface{}
+
+type VirtualServiceExpansion interface{}
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualnode.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualnode.go
new file mode 100644
index 00000000..87fc7b3e
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualnode.go
@@ -0,0 +1,195 @@
+/*
+Copyright The Flagger Authors.
+
+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 v1beta2
+
+import (
+	"context"
+	"time"
+
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	scheme "github.com/weaveworks/flagger/pkg/client/clientset/versioned/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"
+)
+
+// VirtualNodesGetter has a method to return a VirtualNodeInterface.
+// A group's client should implement this interface.
+type VirtualNodesGetter interface {
+	VirtualNodes(namespace string) VirtualNodeInterface
+}
+
+// VirtualNodeInterface has methods to work with VirtualNode resources.
+type VirtualNodeInterface interface {
+	Create(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.CreateOptions) (*v1beta2.VirtualNode, error)
+	Update(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.UpdateOptions) (*v1beta2.VirtualNode, error)
+	UpdateStatus(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.UpdateOptions) (*v1beta2.VirtualNode, 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) (*v1beta2.VirtualNode, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta2.VirtualNodeList, 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 *v1beta2.VirtualNode, err error)
+	VirtualNodeExpansion
+}
+
+// virtualNodes implements VirtualNodeInterface
+type virtualNodes struct {
+	client rest.Interface
+	ns     string
+}
+
+// newVirtualNodes returns a VirtualNodes
+func newVirtualNodes(c *AppmeshV1beta2Client, namespace string) *virtualNodes {
+	return &virtualNodes{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the virtualNode, and returns the corresponding virtualNode object, and an error if there is any.
+func (c *virtualNodes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.VirtualNode, err error) {
+	result = &v1beta2.VirtualNode{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("virtualnodes").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of VirtualNodes that match those selectors.
+func (c *virtualNodes) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.VirtualNodeList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta2.VirtualNodeList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("virtualnodes").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested virtualNodes.
+func (c *virtualNodes) 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("virtualnodes").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a virtualNode and creates it.  Returns the server's representation of the virtualNode, and an error, if there is any.
+func (c *virtualNodes) Create(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.CreateOptions) (result *v1beta2.VirtualNode, err error) {
+	result = &v1beta2.VirtualNode{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("virtualnodes").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualNode).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a virtualNode and updates it. Returns the server's representation of the virtualNode, and an error, if there is any.
+func (c *virtualNodes) Update(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.UpdateOptions) (result *v1beta2.VirtualNode, err error) {
+	result = &v1beta2.VirtualNode{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("virtualnodes").
+		Name(virtualNode.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualNode).
+		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 *virtualNodes) UpdateStatus(ctx context.Context, virtualNode *v1beta2.VirtualNode, opts v1.UpdateOptions) (result *v1beta2.VirtualNode, err error) {
+	result = &v1beta2.VirtualNode{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("virtualnodes").
+		Name(virtualNode.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualNode).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the virtualNode and deletes it. Returns an error if one occurs.
+func (c *virtualNodes) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("virtualnodes").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *virtualNodes) 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("virtualnodes").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched virtualNode.
+func (c *virtualNodes) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.VirtualNode, err error) {
+	result = &v1beta2.VirtualNode{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("virtualnodes").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualrouter.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualrouter.go
new file mode 100644
index 00000000..283c2f5a
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualrouter.go
@@ -0,0 +1,195 @@
+/*
+Copyright The Flagger Authors.
+
+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 v1beta2
+
+import (
+	"context"
+	"time"
+
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	scheme "github.com/weaveworks/flagger/pkg/client/clientset/versioned/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"
+)
+
+// VirtualRoutersGetter has a method to return a VirtualRouterInterface.
+// A group's client should implement this interface.
+type VirtualRoutersGetter interface {
+	VirtualRouters(namespace string) VirtualRouterInterface
+}
+
+// VirtualRouterInterface has methods to work with VirtualRouter resources.
+type VirtualRouterInterface interface {
+	Create(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.CreateOptions) (*v1beta2.VirtualRouter, error)
+	Update(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.UpdateOptions) (*v1beta2.VirtualRouter, error)
+	UpdateStatus(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.UpdateOptions) (*v1beta2.VirtualRouter, 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) (*v1beta2.VirtualRouter, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta2.VirtualRouterList, 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 *v1beta2.VirtualRouter, err error)
+	VirtualRouterExpansion
+}
+
+// virtualRouters implements VirtualRouterInterface
+type virtualRouters struct {
+	client rest.Interface
+	ns     string
+}
+
+// newVirtualRouters returns a VirtualRouters
+func newVirtualRouters(c *AppmeshV1beta2Client, namespace string) *virtualRouters {
+	return &virtualRouters{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the virtualRouter, and returns the corresponding virtualRouter object, and an error if there is any.
+func (c *virtualRouters) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.VirtualRouter, err error) {
+	result = &v1beta2.VirtualRouter{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("virtualrouters").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of VirtualRouters that match those selectors.
+func (c *virtualRouters) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.VirtualRouterList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta2.VirtualRouterList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("virtualrouters").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested virtualRouters.
+func (c *virtualRouters) 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("virtualrouters").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a virtualRouter and creates it.  Returns the server's representation of the virtualRouter, and an error, if there is any.
+func (c *virtualRouters) Create(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.CreateOptions) (result *v1beta2.VirtualRouter, err error) {
+	result = &v1beta2.VirtualRouter{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("virtualrouters").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualRouter).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a virtualRouter and updates it. Returns the server's representation of the virtualRouter, and an error, if there is any.
+func (c *virtualRouters) Update(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.UpdateOptions) (result *v1beta2.VirtualRouter, err error) {
+	result = &v1beta2.VirtualRouter{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("virtualrouters").
+		Name(virtualRouter.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualRouter).
+		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 *virtualRouters) UpdateStatus(ctx context.Context, virtualRouter *v1beta2.VirtualRouter, opts v1.UpdateOptions) (result *v1beta2.VirtualRouter, err error) {
+	result = &v1beta2.VirtualRouter{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("virtualrouters").
+		Name(virtualRouter.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualRouter).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the virtualRouter and deletes it. Returns an error if one occurs.
+func (c *virtualRouters) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("virtualrouters").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *virtualRouters) 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("virtualrouters").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched virtualRouter.
+func (c *virtualRouters) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.VirtualRouter, err error) {
+	result = &v1beta2.VirtualRouter{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("virtualrouters").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualservice.go b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualservice.go
new file mode 100644
index 00000000..c1f7a81d
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/appmesh/v1beta2/virtualservice.go
@@ -0,0 +1,195 @@
+/*
+Copyright The Flagger Authors.
+
+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 v1beta2
+
+import (
+	"context"
+	"time"
+
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	scheme "github.com/weaveworks/flagger/pkg/client/clientset/versioned/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"
+)
+
+// VirtualServicesGetter has a method to return a VirtualServiceInterface.
+// A group's client should implement this interface.
+type VirtualServicesGetter interface {
+	VirtualServices(namespace string) VirtualServiceInterface
+}
+
+// VirtualServiceInterface has methods to work with VirtualService resources.
+type VirtualServiceInterface interface {
+	Create(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.CreateOptions) (*v1beta2.VirtualService, error)
+	Update(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.UpdateOptions) (*v1beta2.VirtualService, error)
+	UpdateStatus(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.UpdateOptions) (*v1beta2.VirtualService, 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) (*v1beta2.VirtualService, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta2.VirtualServiceList, 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 *v1beta2.VirtualService, err error)
+	VirtualServiceExpansion
+}
+
+// virtualServices implements VirtualServiceInterface
+type virtualServices struct {
+	client rest.Interface
+	ns     string
+}
+
+// newVirtualServices returns a VirtualServices
+func newVirtualServices(c *AppmeshV1beta2Client, namespace string) *virtualServices {
+	return &virtualServices{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the virtualService, and returns the corresponding virtualService object, and an error if there is any.
+func (c *virtualServices) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.VirtualService, err error) {
+	result = &v1beta2.VirtualService{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("virtualservices").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of VirtualServices that match those selectors.
+func (c *virtualServices) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.VirtualServiceList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta2.VirtualServiceList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("virtualservices").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested virtualServices.
+func (c *virtualServices) 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("virtualservices").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a virtualService and creates it.  Returns the server's representation of the virtualService, and an error, if there is any.
+func (c *virtualServices) Create(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.CreateOptions) (result *v1beta2.VirtualService, err error) {
+	result = &v1beta2.VirtualService{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("virtualservices").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualService).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a virtualService and updates it. Returns the server's representation of the virtualService, and an error, if there is any.
+func (c *virtualServices) Update(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.UpdateOptions) (result *v1beta2.VirtualService, err error) {
+	result = &v1beta2.VirtualService{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("virtualservices").
+		Name(virtualService.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualService).
+		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 *virtualServices) UpdateStatus(ctx context.Context, virtualService *v1beta2.VirtualService, opts v1.UpdateOptions) (result *v1beta2.VirtualService, err error) {
+	result = &v1beta2.VirtualService{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("virtualservices").
+		Name(virtualService.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(virtualService).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the virtualService and deletes it. Returns an error if one occurs.
+func (c *virtualServices) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("virtualservices").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *virtualServices) 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("virtualservices").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched virtualService.
+func (c *virtualServices) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.VirtualService, err error) {
+	result = &v1beta2.VirtualService{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("virtualservices").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/client/informers/externalversions/appmesh/interface.go b/pkg/client/informers/externalversions/appmesh/interface.go
index 376f4361..5fd4d167 100644
--- a/pkg/client/informers/externalversions/appmesh/interface.go
+++ b/pkg/client/informers/externalversions/appmesh/interface.go
@@ -20,11 +20,14 @@ package appmesh
 
 import (
 	v1beta1 "github.com/weaveworks/flagger/pkg/client/informers/externalversions/appmesh/v1beta1"
+	v1beta2 "github.com/weaveworks/flagger/pkg/client/informers/externalversions/appmesh/v1beta2"
 	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
 )
 
 // Interface provides access to each of this group's versions.
 type Interface interface {
+	// V1beta2 provides access to shared informers for resources in V1beta2.
+	V1beta2() v1beta2.Interface
 	// V1beta1 provides access to shared informers for resources in V1beta1.
 	V1beta1() v1beta1.Interface
 }
@@ -40,6 +43,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
 	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
 }
 
+// V1beta2 returns a new v1beta2.Interface.
+func (g *group) V1beta2() v1beta2.Interface {
+	return v1beta2.New(g.factory, g.namespace, g.tweakListOptions)
+}
+
 // V1beta1 returns a new v1beta1.Interface.
 func (g *group) V1beta1() v1beta1.Interface {
 	return v1beta1.New(g.factory, g.namespace, g.tweakListOptions)
diff --git a/pkg/client/informers/externalversions/appmesh/v1beta2/interface.go b/pkg/client/informers/externalversions/appmesh/v1beta2/interface.go
new file mode 100644
index 00000000..abe451b1
--- /dev/null
+++ b/pkg/client/informers/externalversions/appmesh/v1beta2/interface.go
@@ -0,0 +1,59 @@
+/*
+Copyright The Flagger Authors.
+
+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 informer-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// VirtualNodes returns a VirtualNodeInformer.
+	VirtualNodes() VirtualNodeInformer
+	// VirtualRouters returns a VirtualRouterInformer.
+	VirtualRouters() VirtualRouterInformer
+	// VirtualServices returns a VirtualServiceInformer.
+	VirtualServices() VirtualServiceInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// VirtualNodes returns a VirtualNodeInformer.
+func (v *version) VirtualNodes() VirtualNodeInformer {
+	return &virtualNodeInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
+// VirtualRouters returns a VirtualRouterInformer.
+func (v *version) VirtualRouters() VirtualRouterInformer {
+	return &virtualRouterInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
+// VirtualServices returns a VirtualServiceInformer.
+func (v *version) VirtualServices() VirtualServiceInformer {
+	return &virtualServiceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/pkg/client/informers/externalversions/appmesh/v1beta2/virtualnode.go b/pkg/client/informers/externalversions/appmesh/v1beta2/virtualnode.go
new file mode 100644
index 00000000..d84cf80c
--- /dev/null
+++ b/pkg/client/informers/externalversions/appmesh/v1beta2/virtualnode.go
@@ -0,0 +1,90 @@
+/*
+Copyright The Flagger Authors.
+
+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 informer-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	"context"
+	time "time"
+
+	appmeshv1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	versioned "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
+	v1beta2 "github.com/weaveworks/flagger/pkg/client/listers/appmesh/v1beta2"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// VirtualNodeInformer provides access to a shared informer and lister for
+// VirtualNodes.
+type VirtualNodeInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta2.VirtualNodeLister
+}
+
+type virtualNodeInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewVirtualNodeInformer constructs a new informer for VirtualNode type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewVirtualNodeInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredVirtualNodeInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredVirtualNodeInformer constructs a new informer for VirtualNode type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredVirtualNodeInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AppmeshV1beta2().VirtualNodes(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AppmeshV1beta2().VirtualNodes(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&appmeshv1beta2.VirtualNode{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *virtualNodeInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredVirtualNodeInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *virtualNodeInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&appmeshv1beta2.VirtualNode{}, f.defaultInformer)
+}
+
+func (f *virtualNodeInformer) Lister() v1beta2.VirtualNodeLister {
+	return v1beta2.NewVirtualNodeLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/informers/externalversions/appmesh/v1beta2/virtualrouter.go b/pkg/client/informers/externalversions/appmesh/v1beta2/virtualrouter.go
new file mode 100644
index 00000000..aaf4c6de
--- /dev/null
+++ b/pkg/client/informers/externalversions/appmesh/v1beta2/virtualrouter.go
@@ -0,0 +1,90 @@
+/*
+Copyright The Flagger Authors.
+
+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 informer-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	"context"
+	time "time"
+
+	appmeshv1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	versioned "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
+	v1beta2 "github.com/weaveworks/flagger/pkg/client/listers/appmesh/v1beta2"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// VirtualRouterInformer provides access to a shared informer and lister for
+// VirtualRouters.
+type VirtualRouterInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta2.VirtualRouterLister
+}
+
+type virtualRouterInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewVirtualRouterInformer constructs a new informer for VirtualRouter type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewVirtualRouterInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredVirtualRouterInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredVirtualRouterInformer constructs a new informer for VirtualRouter type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredVirtualRouterInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AppmeshV1beta2().VirtualRouters(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AppmeshV1beta2().VirtualRouters(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&appmeshv1beta2.VirtualRouter{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *virtualRouterInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredVirtualRouterInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *virtualRouterInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&appmeshv1beta2.VirtualRouter{}, f.defaultInformer)
+}
+
+func (f *virtualRouterInformer) Lister() v1beta2.VirtualRouterLister {
+	return v1beta2.NewVirtualRouterLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/informers/externalversions/appmesh/v1beta2/virtualservice.go b/pkg/client/informers/externalversions/appmesh/v1beta2/virtualservice.go
new file mode 100644
index 00000000..70a33bdb
--- /dev/null
+++ b/pkg/client/informers/externalversions/appmesh/v1beta2/virtualservice.go
@@ -0,0 +1,90 @@
+/*
+Copyright The Flagger Authors.
+
+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 informer-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	"context"
+	time "time"
+
+	appmeshv1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	versioned "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
+	v1beta2 "github.com/weaveworks/flagger/pkg/client/listers/appmesh/v1beta2"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// VirtualServiceInformer provides access to a shared informer and lister for
+// VirtualServices.
+type VirtualServiceInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta2.VirtualServiceLister
+}
+
+type virtualServiceInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewVirtualServiceInformer constructs a new informer for VirtualService type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewVirtualServiceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredVirtualServiceInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredVirtualServiceInformer constructs a new informer for VirtualService type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredVirtualServiceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AppmeshV1beta2().VirtualServices(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AppmeshV1beta2().VirtualServices(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&appmeshv1beta2.VirtualService{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *virtualServiceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredVirtualServiceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *virtualServiceInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&appmeshv1beta2.VirtualService{}, f.defaultInformer)
+}
+
+func (f *virtualServiceInformer) Lister() v1beta2.VirtualServiceLister {
+	return v1beta2.NewVirtualServiceLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go
index 43f86822..ceda6adf 100644
--- a/pkg/client/informers/externalversions/generic.go
+++ b/pkg/client/informers/externalversions/generic.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 
 	v1beta1 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta1"
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
 	flaggerv1beta1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
 	v1 "github.com/weaveworks/flagger/pkg/apis/gloo/v1"
 	v1alpha3 "github.com/weaveworks/flagger/pkg/apis/istio/v1alpha3"
@@ -66,6 +67,14 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 	case v1beta1.SchemeGroupVersion.WithResource("virtualservices"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Appmesh().V1beta1().VirtualServices().Informer()}, nil
 
+		// Group=appmesh.k8s.aws, Version=v1beta2
+	case v1beta2.SchemeGroupVersion.WithResource("virtualnodes"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Appmesh().V1beta2().VirtualNodes().Informer()}, nil
+	case v1beta2.SchemeGroupVersion.WithResource("virtualrouters"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Appmesh().V1beta2().VirtualRouters().Informer()}, nil
+	case v1beta2.SchemeGroupVersion.WithResource("virtualservices"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Appmesh().V1beta2().VirtualServices().Informer()}, nil
+
 		// Group=flagger.app, Version=v1beta1
 	case flaggerv1beta1.SchemeGroupVersion.WithResource("alertproviders"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Flagger().V1beta1().AlertProviders().Informer()}, nil
diff --git a/pkg/client/listers/appmesh/v1beta2/expansion_generated.go b/pkg/client/listers/appmesh/v1beta2/expansion_generated.go
new file mode 100644
index 00000000..c5778459
--- /dev/null
+++ b/pkg/client/listers/appmesh/v1beta2/expansion_generated.go
@@ -0,0 +1,43 @@
+/*
+Copyright The Flagger Authors.
+
+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 lister-gen. DO NOT EDIT.
+
+package v1beta2
+
+// VirtualNodeListerExpansion allows custom methods to be added to
+// VirtualNodeLister.
+type VirtualNodeListerExpansion interface{}
+
+// VirtualNodeNamespaceListerExpansion allows custom methods to be added to
+// VirtualNodeNamespaceLister.
+type VirtualNodeNamespaceListerExpansion interface{}
+
+// VirtualRouterListerExpansion allows custom methods to be added to
+// VirtualRouterLister.
+type VirtualRouterListerExpansion interface{}
+
+// VirtualRouterNamespaceListerExpansion allows custom methods to be added to
+// VirtualRouterNamespaceLister.
+type VirtualRouterNamespaceListerExpansion interface{}
+
+// VirtualServiceListerExpansion allows custom methods to be added to
+// VirtualServiceLister.
+type VirtualServiceListerExpansion interface{}
+
+// VirtualServiceNamespaceListerExpansion allows custom methods to be added to
+// VirtualServiceNamespaceLister.
+type VirtualServiceNamespaceListerExpansion interface{}
diff --git a/pkg/client/listers/appmesh/v1beta2/virtualnode.go b/pkg/client/listers/appmesh/v1beta2/virtualnode.go
new file mode 100644
index 00000000..ed62b8fd
--- /dev/null
+++ b/pkg/client/listers/appmesh/v1beta2/virtualnode.go
@@ -0,0 +1,94 @@
+/*
+Copyright The Flagger Authors.
+
+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 lister-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// VirtualNodeLister helps list VirtualNodes.
+type VirtualNodeLister interface {
+	// List lists all VirtualNodes in the indexer.
+	List(selector labels.Selector) (ret []*v1beta2.VirtualNode, err error)
+	// VirtualNodes returns an object that can list and get VirtualNodes.
+	VirtualNodes(namespace string) VirtualNodeNamespaceLister
+	VirtualNodeListerExpansion
+}
+
+// virtualNodeLister implements the VirtualNodeLister interface.
+type virtualNodeLister struct {
+	indexer cache.Indexer
+}
+
+// NewVirtualNodeLister returns a new VirtualNodeLister.
+func NewVirtualNodeLister(indexer cache.Indexer) VirtualNodeLister {
+	return &virtualNodeLister{indexer: indexer}
+}
+
+// List lists all VirtualNodes in the indexer.
+func (s *virtualNodeLister) List(selector labels.Selector) (ret []*v1beta2.VirtualNode, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.VirtualNode))
+	})
+	return ret, err
+}
+
+// VirtualNodes returns an object that can list and get VirtualNodes.
+func (s *virtualNodeLister) VirtualNodes(namespace string) VirtualNodeNamespaceLister {
+	return virtualNodeNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// VirtualNodeNamespaceLister helps list and get VirtualNodes.
+type VirtualNodeNamespaceLister interface {
+	// List lists all VirtualNodes in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1beta2.VirtualNode, err error)
+	// Get retrieves the VirtualNode from the indexer for a given namespace and name.
+	Get(name string) (*v1beta2.VirtualNode, error)
+	VirtualNodeNamespaceListerExpansion
+}
+
+// virtualNodeNamespaceLister implements the VirtualNodeNamespaceLister
+// interface.
+type virtualNodeNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all VirtualNodes in the indexer for a given namespace.
+func (s virtualNodeNamespaceLister) List(selector labels.Selector) (ret []*v1beta2.VirtualNode, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.VirtualNode))
+	})
+	return ret, err
+}
+
+// Get retrieves the VirtualNode from the indexer for a given namespace and name.
+func (s virtualNodeNamespaceLister) Get(name string) (*v1beta2.VirtualNode, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta2.Resource("virtualnode"), name)
+	}
+	return obj.(*v1beta2.VirtualNode), nil
+}
diff --git a/pkg/client/listers/appmesh/v1beta2/virtualrouter.go b/pkg/client/listers/appmesh/v1beta2/virtualrouter.go
new file mode 100644
index 00000000..5b077baa
--- /dev/null
+++ b/pkg/client/listers/appmesh/v1beta2/virtualrouter.go
@@ -0,0 +1,94 @@
+/*
+Copyright The Flagger Authors.
+
+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 lister-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// VirtualRouterLister helps list VirtualRouters.
+type VirtualRouterLister interface {
+	// List lists all VirtualRouters in the indexer.
+	List(selector labels.Selector) (ret []*v1beta2.VirtualRouter, err error)
+	// VirtualRouters returns an object that can list and get VirtualRouters.
+	VirtualRouters(namespace string) VirtualRouterNamespaceLister
+	VirtualRouterListerExpansion
+}
+
+// virtualRouterLister implements the VirtualRouterLister interface.
+type virtualRouterLister struct {
+	indexer cache.Indexer
+}
+
+// NewVirtualRouterLister returns a new VirtualRouterLister.
+func NewVirtualRouterLister(indexer cache.Indexer) VirtualRouterLister {
+	return &virtualRouterLister{indexer: indexer}
+}
+
+// List lists all VirtualRouters in the indexer.
+func (s *virtualRouterLister) List(selector labels.Selector) (ret []*v1beta2.VirtualRouter, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.VirtualRouter))
+	})
+	return ret, err
+}
+
+// VirtualRouters returns an object that can list and get VirtualRouters.
+func (s *virtualRouterLister) VirtualRouters(namespace string) VirtualRouterNamespaceLister {
+	return virtualRouterNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// VirtualRouterNamespaceLister helps list and get VirtualRouters.
+type VirtualRouterNamespaceLister interface {
+	// List lists all VirtualRouters in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1beta2.VirtualRouter, err error)
+	// Get retrieves the VirtualRouter from the indexer for a given namespace and name.
+	Get(name string) (*v1beta2.VirtualRouter, error)
+	VirtualRouterNamespaceListerExpansion
+}
+
+// virtualRouterNamespaceLister implements the VirtualRouterNamespaceLister
+// interface.
+type virtualRouterNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all VirtualRouters in the indexer for a given namespace.
+func (s virtualRouterNamespaceLister) List(selector labels.Selector) (ret []*v1beta2.VirtualRouter, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.VirtualRouter))
+	})
+	return ret, err
+}
+
+// Get retrieves the VirtualRouter from the indexer for a given namespace and name.
+func (s virtualRouterNamespaceLister) Get(name string) (*v1beta2.VirtualRouter, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta2.Resource("virtualrouter"), name)
+	}
+	return obj.(*v1beta2.VirtualRouter), nil
+}
diff --git a/pkg/client/listers/appmesh/v1beta2/virtualservice.go b/pkg/client/listers/appmesh/v1beta2/virtualservice.go
new file mode 100644
index 00000000..b87cd0e4
--- /dev/null
+++ b/pkg/client/listers/appmesh/v1beta2/virtualservice.go
@@ -0,0 +1,94 @@
+/*
+Copyright The Flagger Authors.
+
+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 lister-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1beta2 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// VirtualServiceLister helps list VirtualServices.
+type VirtualServiceLister interface {
+	// List lists all VirtualServices in the indexer.
+	List(selector labels.Selector) (ret []*v1beta2.VirtualService, err error)
+	// VirtualServices returns an object that can list and get VirtualServices.
+	VirtualServices(namespace string) VirtualServiceNamespaceLister
+	VirtualServiceListerExpansion
+}
+
+// virtualServiceLister implements the VirtualServiceLister interface.
+type virtualServiceLister struct {
+	indexer cache.Indexer
+}
+
+// NewVirtualServiceLister returns a new VirtualServiceLister.
+func NewVirtualServiceLister(indexer cache.Indexer) VirtualServiceLister {
+	return &virtualServiceLister{indexer: indexer}
+}
+
+// List lists all VirtualServices in the indexer.
+func (s *virtualServiceLister) List(selector labels.Selector) (ret []*v1beta2.VirtualService, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.VirtualService))
+	})
+	return ret, err
+}
+
+// VirtualServices returns an object that can list and get VirtualServices.
+func (s *virtualServiceLister) VirtualServices(namespace string) VirtualServiceNamespaceLister {
+	return virtualServiceNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// VirtualServiceNamespaceLister helps list and get VirtualServices.
+type VirtualServiceNamespaceLister interface {
+	// List lists all VirtualServices in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1beta2.VirtualService, err error)
+	// Get retrieves the VirtualService from the indexer for a given namespace and name.
+	Get(name string) (*v1beta2.VirtualService, error)
+	VirtualServiceNamespaceListerExpansion
+}
+
+// virtualServiceNamespaceLister implements the VirtualServiceNamespaceLister
+// interface.
+type virtualServiceNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all VirtualServices in the indexer for a given namespace.
+func (s virtualServiceNamespaceLister) List(selector labels.Selector) (ret []*v1beta2.VirtualService, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.VirtualService))
+	})
+	return ret, err
+}
+
+// Get retrieves the VirtualService from the indexer for a given namespace and name.
+func (s virtualServiceNamespaceLister) Get(name string) (*v1beta2.VirtualService, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta2.Resource("virtualservice"), name)
+	}
+	return obj.(*v1beta2.VirtualService), nil
+}
diff --git a/pkg/controller/finalizer.go b/pkg/controller/finalizer.go
index 50596272..4ea2a139 100644
--- a/pkg/controller/finalizer.go
+++ b/pkg/controller/finalizer.go
@@ -79,7 +79,7 @@ func (c *Controller) revertMesh(r *flaggerv1.Canary) error {
 		provider = r.Spec.Provider
 	}
 
-	meshRouter := c.routerFactory.MeshRouter(provider)
+	meshRouter := c.routerFactory.MeshRouter(provider, "")
 	if err := meshRouter.Finalize(r); err != nil {
 		return fmt.Errorf("meshRouter.Finlize failed: %w", err)
 	}
diff --git a/pkg/controller/scheduler.go b/pkg/controller/scheduler.go
index e8d9a2c3..5960df85 100644
--- a/pkg/controller/scheduler.go
+++ b/pkg/controller/scheduler.go
@@ -3,6 +3,7 @@ package controller
 import (
 	"context"
 	"fmt"
+	"strings"
 	"time"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -110,31 +111,45 @@ func (c *Controller) advanceCanary(name string, namespace string) {
 
 	// init Kubernetes router
 	kubeRouter := c.routerFactory.KubernetesRouter(cd.Spec.TargetRef.Kind, labelSelector, ports)
+
+	// reconcile the canary/primary services
 	if err := kubeRouter.Initialize(cd); err != nil {
 		c.recordEventWarningf(cd, "%v", err)
 		return
 	}
 
-	// create primary
+	// init mesh router
+	meshRouter := c.routerFactory.MeshRouter(provider, labelSelector)
+
+	// register the AppMesh VirtualNodes before creating the primary deployment
+	// otherwise the pods will not be injected with the Envoy proxy
+	if strings.HasPrefix(provider, flaggerv1.AppMeshProvider) {
+		if err := meshRouter.Reconcile(cd); err != nil {
+			c.recordEventWarningf(cd, "%v", err)
+			return
+		}
+	}
+
+	// create primary workload
 	err = canaryController.Initialize(cd)
 	if err != nil {
 		c.recordEventWarningf(cd, "%v", err)
 		return
 	}
 
-	// init mesh router
-	meshRouter := c.routerFactory.MeshRouter(provider)
-
-	// create or update svc
+	// change the apex service pod selector to primary
 	if err := kubeRouter.Reconcile(cd); err != nil {
 		c.recordEventWarningf(cd, "%v", err)
 		return
 	}
 
-	// create or update mesh routes
-	if err := meshRouter.Reconcile(cd); err != nil {
-		c.recordEventWarningf(cd, "%v", err)
-		return
+	// take over an existing virtual service or ingress
+	// runs after the primary is ready to ensure zero downtime
+	if !strings.HasPrefix(provider, flaggerv1.AppMeshProvider) {
+		if err := meshRouter.Reconcile(cd); err != nil {
+			c.recordEventWarningf(cd, "%v", err)
+			return
+		}
 	}
 
 	// check for changes
diff --git a/pkg/metrics/observers/crossover.go b/pkg/metrics/observers/crossover.go
deleted file mode 100644
index 0ecf3d15..00000000
--- a/pkg/metrics/observers/crossover.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package observers
-
-import (
-	"fmt"
-	"time"
-
-	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
-	"github.com/weaveworks/flagger/pkg/metrics/providers"
-)
-
-var crossoverQueries = map[string]string{
-	"request-success-rate": `
-	sum(
-		rate(
-			envoy_cluster_upstream_rq{
-				kubernetes_namespace="{{ namespace }}",
-				envoy_cluster_name=~"{{ target }}-canary",
-				envoy_response_code!~"5.*"
-			}[{{ interval }}]
-		)
-	) 
-	/ 
-	sum(
-		rate(
-			envoy_cluster_upstream_rq{
-				kubernetes_namespace="{{ namespace }}",
-				envoy_cluster_name=~"{{ target }}-canary"
-			}[{{ interval }}]
-		)
-	) 
-	* 100`,
-	"request-duration": `
-	histogram_quantile(
-		0.99,
-		sum(
-			rate(
-				envoy_cluster_upstream_rq_time_bucket{
-					kubernetes_namespace="{{ namespace }}",
-					envoy_cluster_name=~"{{ target }}-canary"
-				}[{{ interval }}]
-			)
-		) by (le)
-	)`,
-}
-
-type CrossoverObserver struct {
-	client providers.Interface
-}
-
-func (ob *CrossoverObserver) GetRequestSuccessRate(model flaggerv1.MetricTemplateModel) (float64, error) {
-	query, err := RenderQuery(crossoverQueries["request-success-rate"], model)
-	if err != nil {
-		return 0, err
-	}
-
-	value, err := ob.client.RunQuery(query)
-	if err != nil {
-		return 0, err
-	}
-
-	return value, nil
-}
-
-func (ob *CrossoverObserver) GetRequestDuration(model flaggerv1.MetricTemplateModel) (time.Duration, error) {
-	query, err := RenderQuery(crossoverQueries["request-duration"], model)
-	if err != nil {
-		return 0, fmt.Errorf("rendering query failed: %w", err)
-	}
-
-	value, err := ob.client.RunQuery(query)
-	if err != nil {
-		return 0, fmt.Errorf("running query failed: %w", err)
-	}
-
-	ms := time.Duration(int64(value)) * time.Millisecond
-	return ms, nil
-}
diff --git a/pkg/metrics/observers/crossover_service.go b/pkg/metrics/observers/crossover_service.go
deleted file mode 100644
index 0aa27a6a..00000000
--- a/pkg/metrics/observers/crossover_service.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package observers
-
-import (
-	"fmt"
-	"time"
-
-	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
-	"github.com/weaveworks/flagger/pkg/metrics/providers"
-)
-
-var crossoverServiceQueries = map[string]string{
-	"request-success-rate": `
-	sum(
-		rate(
-			envoy_cluster_upstream_rq{
-				kubernetes_namespace="{{ namespace }}",
-				envoy_cluster_name="{{ target }}-canary",
-				envoy_response_code!~"5.*"
-			}[{{ interval }}]
-		)
-	) 
-	/ 
-	sum(
-		rate(
-			envoy_cluster_upstream_rq{
-				kubernetes_namespace="{{ namespace }}",
-				envoy_cluster_name="{{ target }}-canary"
-			}[{{ interval }}]
-		)
-	) 
-	* 100`,
-	"request-duration": `
-	histogram_quantile(
-		0.99,
-		sum(
-			rate(
-				envoy_cluster_upstream_rq_time_bucket{
-					kubernetes_namespace="{{ namespace }}",
-					envoy_cluster_name="{{ target }}-canary"
-				}[{{ interval }}]
-			)
-		) by (le)
-	)`,
-}
-
-type CrossoverServiceObserver struct {
-	client providers.Interface
-}
-
-func (ob *CrossoverServiceObserver) GetRequestSuccessRate(model flaggerv1.MetricTemplateModel) (float64, error) {
-	query, err := RenderQuery(crossoverServiceQueries["request-success-rate"], model)
-	if err != nil {
-		return 0, fmt.Errorf("rendering query failed: %w", err)
-	}
-
-	value, err := ob.client.RunQuery(query)
-	if err != nil {
-		return 0, fmt.Errorf("running query failed: %w", err)
-	}
-
-	return value, nil
-}
-
-func (ob *CrossoverServiceObserver) GetRequestDuration(model flaggerv1.MetricTemplateModel) (time.Duration, error) {
-	query, err := RenderQuery(crossoverServiceQueries["request-duration"], model)
-	if err != nil {
-		return 0, fmt.Errorf("rendering query failed: %w", err)
-	}
-
-	value, err := ob.client.RunQuery(query)
-	if err != nil {
-		return 0, fmt.Errorf("running query failed: %w", err)
-	}
-
-	ms := time.Duration(int64(value)) * time.Millisecond
-	return ms, nil
-}
diff --git a/pkg/metrics/observers/factory.go b/pkg/metrics/observers/factory.go
index f335354f..35555c7c 100644
--- a/pkg/metrics/observers/factory.go
+++ b/pkg/metrics/observers/factory.go
@@ -28,44 +28,32 @@ func NewFactory(metricsServer string) (*Factory, error) {
 
 func (factory Factory) Observer(provider string) Interface {
 	switch {
-	case provider == "none":
-		return &HttpObserver{
-			client: factory.Client,
-		}
-	case provider == "kubernetes":
-		return &HttpObserver{
-			client: factory.Client,
-		}
-	case provider == "appmesh":
+	case strings.HasPrefix(provider, flaggerv1.AppMeshProvider):
 		return &AppMeshObserver{
 			client: factory.Client,
 		}
-	case provider == "crossover":
-		return &CrossoverObserver{
-			client: factory.Client,
-		}
-	case provider == "nginx":
-		return &NginxObserver{
+	case provider == flaggerv1.LinkerdProvider:
+		return &LinkerdObserver{
 			client: factory.Client,
 		}
-	case strings.HasPrefix(provider, "gloo"):
-		return &GlooObserver{
+	case provider == flaggerv1.IstioProvider:
+		return &IstioObserver{
 			client: factory.Client,
 		}
-	case provider == "smi:linkerd":
-		return &LinkerdObserver{
+	case provider == flaggerv1.ContourProvider:
+		return &ContourObserver{
 			client: factory.Client,
 		}
-	case provider == "crossover:service":
-		return &CrossoverServiceObserver{
+	case strings.HasPrefix(provider, flaggerv1.GlooProvider):
+		return &GlooObserver{
 			client: factory.Client,
 		}
-	case provider == "linkerd":
-		return &LinkerdObserver{
+	case provider == flaggerv1.NGINXProvider:
+		return &NginxObserver{
 			client: factory.Client,
 		}
-	case provider == "contour":
-		return &ContourObserver{
+	case provider == flaggerv1.KubernetesProvider:
+		return &HttpObserver{
 			client: factory.Client,
 		}
 	default:
diff --git a/pkg/router/appmesh_v1beta2.go b/pkg/router/appmesh_v1beta2.go
new file mode 100644
index 00000000..527099fc
--- /dev/null
+++ b/pkg/router/appmesh_v1beta2.go
@@ -0,0 +1,540 @@
+package router
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"go.uber.org/zap"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/kubernetes"
+
+	appmeshv1 "github.com/weaveworks/flagger/pkg/apis/appmesh/v1beta2"
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
+)
+
+// AppMeshRouter is managing AppMesh virtual services
+type AppMeshv1beta2Router struct {
+	kubeClient    kubernetes.Interface
+	appmeshClient clientset.Interface
+	flaggerClient clientset.Interface
+	logger        *zap.SugaredLogger
+	labelSelector string
+}
+
+// Reconcile creates or updates App Mesh virtual nodes and virtual services
+func (ar *AppMeshv1beta2Router) Reconcile(canary *flaggerv1.Canary) error {
+	svcSuffix := "svc.cluster.local."
+	apexName, primaryName, canaryName := canary.GetServiceNames()
+	primaryHost := fmt.Sprintf("%s.%s.%s", primaryName, canary.Namespace, svcSuffix)
+	canaryHost := fmt.Sprintf("%s.%s.%s", canaryName, canary.Namespace, svcSuffix)
+
+	// sync virtual node e.g. app-namespace
+	// DNS app.namespace
+	//err := ar.reconcileVirtualNode(canary, apexName, fmt.Sprintf("%s-primary", canary.Spec.TargetRef.Name), primaryHost)
+	//if err != nil {
+	//	return fmt.Errorf("reconcileVirtualNode failed: %w", err)
+	//}
+
+	// sync virtual node e.g. app-primary-namespace
+	// DNS app-primary.namespace
+	err := ar.reconcileVirtualNode(canary, primaryName, fmt.Sprintf("%s-primary", canary.Spec.TargetRef.Name), primaryHost)
+	if err != nil {
+		return fmt.Errorf("reconcileVirtualNode failed: %w", err)
+	}
+
+	// sync virtual node e.g. app-canary-namespace
+	// DNS app-canary.namespace
+	err = ar.reconcileVirtualNode(canary, canaryName, canary.Spec.TargetRef.Name, canaryHost)
+	if err != nil {
+		return fmt.Errorf("reconcileVirtualNode failed: %w", err)
+	}
+
+	// sync main virtual router
+	// DNS app.namespace
+	err = ar.reconcileVirtualRouter(canary, apexName, 0)
+	if err != nil {
+		return fmt.Errorf("reconcileVirtualService failed: %w", err)
+	}
+
+	// sync canary virtual router
+	// DNS app-canary.namespace
+	err = ar.reconcileVirtualRouter(canary, canaryName, 100)
+	if err != nil {
+		return fmt.Errorf("reconcileVirtualRouter failed: %w", err)
+	}
+
+	return nil
+}
+
+// reconcileVirtualNode creates or updates a virtual node
+// the virtual node naming format is name-role-namespace
+func (ar *AppMeshv1beta2Router) reconcileVirtualNode(canary *flaggerv1.Canary, name string, podSelector string, host string) error {
+	protocol := ar.getProtocol(canary)
+	vnSpec := appmeshv1.VirtualNodeSpec{
+		Listeners: []appmeshv1.Listener{
+			{
+				PortMapping: appmeshv1.PortMapping{
+					Port:     ar.getContainerPort(canary),
+					Protocol: protocol,
+				},
+			},
+		},
+		ServiceDiscovery: &appmeshv1.ServiceDiscovery{
+			DNS: &appmeshv1.DNSServiceDiscovery{
+				Hostname: host,
+			},
+		},
+		PodSelector: &metav1.LabelSelector{
+			MatchLabels: map[string]string{
+				ar.labelSelector: podSelector,
+			},
+		},
+	}
+
+	backends := make([]appmeshv1.Backend, 0)
+	for _, b := range canary.Spec.Service.Backends {
+		bk := appmeshv1.Backend{
+			VirtualService: appmeshv1.VirtualServiceBackend{
+				VirtualServiceRef: appmeshv1.VirtualServiceReference{
+					Name: b,
+				},
+			},
+		}
+		backends = append(backends, bk)
+	}
+
+	if len(backends) > 0 {
+		vnSpec.Backends = backends
+	}
+
+	virtualnode, err := ar.appmeshClient.AppmeshV1beta2().VirtualNodes(canary.Namespace).Get(context.TODO(), name, metav1.GetOptions{})
+
+	// create virtual node
+	if errors.IsNotFound(err) {
+		virtualnode = &appmeshv1.VirtualNode{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name,
+				Namespace: canary.Namespace,
+				OwnerReferences: []metav1.OwnerReference{
+					*metav1.NewControllerRef(canary, schema.GroupVersionKind{
+						Group:   flaggerv1.SchemeGroupVersion.Group,
+						Version: flaggerv1.SchemeGroupVersion.Version,
+						Kind:    flaggerv1.CanaryKind,
+					}),
+				},
+			},
+			Spec: vnSpec,
+		}
+		_, err = ar.appmeshClient.AppmeshV1beta2().VirtualNodes(canary.Namespace).Create(context.TODO(), virtualnode, metav1.CreateOptions{})
+		if err != nil {
+			return fmt.Errorf("VirtualNode %s.%s create error %w", name, canary.Namespace, err)
+		}
+		ar.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+			Infof("VirtualNode %s.%s created", virtualnode.GetName(), canary.Namespace)
+		return nil
+	} else if err != nil {
+		return fmt.Errorf("VirtualNode %s get query error %w", name, err)
+	}
+
+	// update virtual node
+	if virtualnode != nil {
+		if diff := cmp.Diff(vnSpec, virtualnode.Spec,
+			cmpopts.IgnoreFields(appmeshv1.VirtualNodeSpec{}, "AWSName", "MeshRef")); diff != "" {
+			vnClone := virtualnode.DeepCopy()
+			vnClone.Spec = vnSpec
+			vnClone.Spec.AWSName = virtualnode.Spec.AWSName
+			vnClone.Spec.MeshRef = virtualnode.Spec.MeshRef
+			_, err = ar.appmeshClient.AppmeshV1beta2().VirtualNodes(canary.Namespace).Update(context.TODO(), vnClone, metav1.UpdateOptions{})
+			if err != nil {
+				return fmt.Errorf("VirtualNode %s update error %w", name, err)
+			}
+			ar.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+				Infof("VirtualNode %s updated", virtualnode.GetName())
+		}
+	}
+
+	return nil
+}
+
+// reconcileVirtualRouter creates or updates a virtual router
+func (ar *AppMeshv1beta2Router) reconcileVirtualRouter(canary *flaggerv1.Canary, name string, canaryWeight int64) error {
+	apexName, _, _ := canary.GetServiceNames()
+	canaryVirtualNode := fmt.Sprintf("%s-canary", apexName)
+	primaryVirtualNode := fmt.Sprintf("%s-primary", apexName)
+	protocol := ar.getProtocol(canary)
+
+	routerName := apexName
+	if canaryWeight > 0 {
+		routerName = fmt.Sprintf("%s-canary", apexName)
+	}
+	// App Mesh supports only URI prefix
+	routePrefix := "/"
+	if len(canary.Spec.Service.Match) > 0 &&
+		canary.Spec.Service.Match[0].Uri != nil &&
+		canary.Spec.Service.Match[0].Uri.Prefix != "" {
+		routePrefix = canary.Spec.Service.Match[0].Uri.Prefix
+	}
+
+	// Canary progressive traffic shift
+	routes := []appmeshv1.Route{
+		{
+			Name: routerName,
+			HTTPRoute: &appmeshv1.HTTPRoute{
+				Match: appmeshv1.HTTPRouteMatch{
+					Prefix: routePrefix,
+				},
+				RetryPolicy: ar.makeRetryPolicy(canary),
+				Action: appmeshv1.HTTPRouteAction{
+					WeightedTargets: []appmeshv1.WeightedTarget{
+						{
+							VirtualNodeRef: appmeshv1.VirtualNodeReference{
+								Name: canaryVirtualNode,
+							},
+							Weight: canaryWeight,
+						},
+						{
+							VirtualNodeRef: appmeshv1.VirtualNodeReference{
+								Name: primaryVirtualNode,
+							},
+							Weight: 100 - canaryWeight,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	// A/B testing - header based routing
+	if len(canary.GetAnalysis().Match) > 0 && canaryWeight == 0 {
+		routes = []appmeshv1.Route{
+			{
+				Name:     fmt.Sprintf("%s-a", apexName),
+				Priority: int64p(10),
+				HTTPRoute: &appmeshv1.HTTPRoute{
+					Match: appmeshv1.HTTPRouteMatch{
+						Prefix:  routePrefix,
+						Headers: ar.makeHeaders(canary),
+					},
+					RetryPolicy: ar.makeRetryPolicy(canary),
+					Action: appmeshv1.HTTPRouteAction{
+						WeightedTargets: []appmeshv1.WeightedTarget{
+							{
+								VirtualNodeRef: appmeshv1.VirtualNodeReference{
+									Name: canaryVirtualNode,
+								},
+								Weight: canaryWeight,
+							},
+							{
+								VirtualNodeRef: appmeshv1.VirtualNodeReference{
+									Name: primaryVirtualNode,
+								},
+								Weight: 100 - canaryWeight,
+							},
+						},
+					},
+				},
+			},
+			{
+				Name:     fmt.Sprintf("%s-b", apexName),
+				Priority: int64p(20),
+				HTTPRoute: &appmeshv1.HTTPRoute{
+					Match: appmeshv1.HTTPRouteMatch{
+						Prefix: routePrefix,
+					},
+					RetryPolicy: ar.makeRetryPolicy(canary),
+					Action: appmeshv1.HTTPRouteAction{
+						WeightedTargets: []appmeshv1.WeightedTarget{
+							{
+								VirtualNodeRef: appmeshv1.VirtualNodeReference{
+									Name: primaryVirtualNode,
+								},
+								Weight: 100,
+							},
+						},
+					},
+				},
+			},
+		}
+	}
+
+	vrSpec := appmeshv1.VirtualRouterSpec{
+		Listeners: []appmeshv1.VirtualRouterListener{
+			{
+				PortMapping: appmeshv1.PortMapping{
+					Port:     ar.getContainerPort(canary),
+					Protocol: protocol,
+				},
+			},
+		},
+		Routes: routes,
+	}
+
+	virtualRouter, err := ar.appmeshClient.AppmeshV1beta2().VirtualRouters(canary.Namespace).Get(context.TODO(), name, metav1.GetOptions{})
+
+	// create virtual router
+	if errors.IsNotFound(err) {
+		virtualRouter = &appmeshv1.VirtualRouter{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name,
+				Namespace: canary.Namespace,
+				OwnerReferences: []metav1.OwnerReference{
+					*metav1.NewControllerRef(canary, schema.GroupVersionKind{
+						Group:   flaggerv1.SchemeGroupVersion.Group,
+						Version: flaggerv1.SchemeGroupVersion.Version,
+						Kind:    flaggerv1.CanaryKind,
+					}),
+				},
+			},
+			Spec: vrSpec,
+		}
+
+		_, err = ar.appmeshClient.AppmeshV1beta2().VirtualRouters(canary.Namespace).Create(context.TODO(), virtualRouter, metav1.CreateOptions{})
+		if err != nil {
+			return fmt.Errorf("VirtualRouter %s create error %w", name, err)
+		}
+		ar.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+			Infof("VirtualRouter %s created", virtualRouter.GetName())
+
+		virtualService := &appmeshv1.VirtualService{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name,
+				Namespace: canary.Namespace,
+				OwnerReferences: []metav1.OwnerReference{
+					*metav1.NewControllerRef(canary, schema.GroupVersionKind{
+						Group:   flaggerv1.SchemeGroupVersion.Group,
+						Version: flaggerv1.SchemeGroupVersion.Version,
+						Kind:    flaggerv1.CanaryKind,
+					}),
+				},
+			},
+			Spec: appmeshv1.VirtualServiceSpec{
+				Provider: &appmeshv1.VirtualServiceProvider{
+					VirtualRouter: &appmeshv1.VirtualRouterServiceProvider{
+						VirtualRouterRef: appmeshv1.VirtualRouterReference{
+							Name: name,
+						},
+					},
+				},
+			},
+		}
+
+		// set App Mesh Gateway annotation on primary virtual service
+		if canaryWeight == 0 {
+			a := ar.gatewayAnnotations(canary)
+			if len(a) > 0 {
+				virtualService.ObjectMeta.Annotations = a
+			}
+		}
+
+		_, err = ar.appmeshClient.AppmeshV1beta2().VirtualServices(canary.Namespace).Create(context.TODO(), virtualService, metav1.CreateOptions{})
+		if err != nil {
+			return fmt.Errorf("VirtualService %s create error %w", name, err)
+		}
+		ar.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+			Infof("VirtualService %s created", virtualRouter.GetName())
+
+		return nil
+	} else if err != nil {
+		return fmt.Errorf("VirtualRouter %s get query error: %w", name, err)
+	}
+
+	// update virtual router but keep the original target weights
+	if virtualRouter != nil {
+		if diff := cmp.Diff(vrSpec, virtualRouter.Spec,
+			cmpopts.IgnoreFields(appmeshv1.VirtualRouterSpec{}, "AWSName", "MeshRef"),
+			cmpopts.IgnoreTypes(appmeshv1.WeightedTarget{}, appmeshv1.MeshReference{})); diff != "" {
+			vrClone := virtualRouter.DeepCopy()
+			vrClone.Spec = vrSpec
+			vrClone.Spec.Routes[0].HTTPRoute.Action = virtualRouter.Spec.Routes[0].HTTPRoute.Action
+			vrClone.Spec.AWSName = virtualRouter.Spec.AWSName
+			vrClone.Spec.MeshRef = virtualRouter.Spec.MeshRef
+			_, err = ar.appmeshClient.AppmeshV1beta2().VirtualRouters(canary.Namespace).Update(context.TODO(), vrClone, metav1.UpdateOptions{})
+			if err != nil {
+				return fmt.Errorf("VirtualRouter %s update error: %w", name, err)
+			}
+			ar.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+				Infof("VirtualRouter %s updated", virtualRouter.GetName())
+		}
+	}
+
+	return nil
+}
+
+// GetRoutes returns the destinations weight for primary and canary
+func (ar *AppMeshv1beta2Router) GetRoutes(canary *flaggerv1.Canary) (
+	primaryWeight int,
+	canaryWeight int,
+	mirrored bool,
+	err error,
+) {
+	apexName, primaryName, canaryName := canary.GetServiceNames()
+	virtualRouter, err := ar.appmeshClient.AppmeshV1beta2().VirtualRouters(canary.Namespace).Get(context.TODO(), apexName, metav1.GetOptions{})
+	if err != nil {
+		err = fmt.Errorf("VirtualRouter %s get query error: %w", apexName, err)
+		return
+	}
+
+	if len(virtualRouter.Spec.Routes) < 1 || len(virtualRouter.Spec.Routes[0].HTTPRoute.Action.WeightedTargets) != 2 {
+		err = fmt.Errorf("VirtualRouter routes %s not found", apexName)
+		return
+	}
+
+	targets := virtualRouter.Spec.Routes[0].HTTPRoute.Action.WeightedTargets
+	for _, t := range targets {
+		if t.VirtualNodeRef.Name == canaryName {
+			canaryWeight = int(t.Weight)
+		}
+		if t.VirtualNodeRef.Name == primaryName {
+			primaryWeight = int(t.Weight)
+		}
+	}
+
+	if primaryWeight == 0 && canaryWeight == 0 {
+		err = fmt.Errorf("VirtualRouter %s does not contain routes for %s-primary and %s-canary",
+			apexName, apexName, apexName)
+	}
+
+	mirrored = false
+
+	return
+}
+
+// SetRoutes updates the destinations weight for primary and canary
+func (ar *AppMeshv1beta2Router) SetRoutes(
+	canary *flaggerv1.Canary,
+	primaryWeight int,
+	canaryWeight int,
+	_ bool,
+) error {
+	apexName, primaryName, canaryName := canary.GetServiceNames()
+	virtualRouter, err := ar.appmeshClient.AppmeshV1beta2().VirtualRouters(canary.Namespace).Get(context.TODO(), apexName, metav1.GetOptions{})
+	if err != nil {
+		return fmt.Errorf("VirtualRouter %s get query error: %w", apexName, err)
+	}
+
+	vrClone := virtualRouter.DeepCopy()
+	vrClone.Spec.Routes[0].HTTPRoute.Action = appmeshv1.HTTPRouteAction{
+		WeightedTargets: []appmeshv1.WeightedTarget{
+			{
+				VirtualNodeRef: appmeshv1.VirtualNodeReference{
+					Name: canaryName,
+				},
+				Weight: int64(canaryWeight),
+			},
+			{
+				VirtualNodeRef: appmeshv1.VirtualNodeReference{
+					Name: primaryName,
+				},
+				Weight: int64(primaryWeight),
+			},
+		},
+	}
+
+	_, err = ar.appmeshClient.AppmeshV1beta2().VirtualRouters(canary.Namespace).Update(context.TODO(), vrClone, metav1.UpdateOptions{})
+	if err != nil {
+		return fmt.Errorf("VirtualRouter %s update error: %w", apexName, err)
+	}
+	return nil
+}
+
+// makeRetryPolicy creates an AppMesh HTTPRetryPolicy from the Canary.Service.Retries
+// default: one retry on gateway error with a 250ms timeout
+func (ar *AppMeshv1beta2Router) makeRetryPolicy(canary *flaggerv1.Canary) *appmeshv1.HTTPRetryPolicy {
+	if canary.Spec.Service.Retries != nil {
+		timeout := int64(250)
+		if d, err := time.ParseDuration(canary.Spec.Service.Retries.PerTryTimeout); err == nil {
+			timeout = d.Milliseconds()
+		}
+
+		attempts := 1
+		if canary.Spec.Service.Retries.Attempts > 0 {
+			attempts = canary.Spec.Service.Retries.Attempts
+		}
+		retryPolicy := &appmeshv1.HTTPRetryPolicy{
+			PerRetryTimeout: appmeshv1.Duration{
+				Unit:  appmeshv1.DurationUnitMS,
+				Value: timeout,
+			},
+			MaxRetries: int64(attempts),
+		}
+
+		events := []string{"gateway-error"}
+		if len(canary.Spec.Service.Retries.RetryOn) > 0 {
+			events = strings.Split(canary.Spec.Service.Retries.RetryOn, ",")
+		}
+		for _, value := range events {
+			retryPolicy.HTTPRetryEvents = append(retryPolicy.HTTPRetryEvents, appmeshv1.HTTPRetryPolicyEvent(value))
+		}
+		return retryPolicy
+	}
+
+	return nil
+}
+
+// makeRetryPolicy creates an App Mesh HttpRouteHeader from the Canary.CanaryAnalysis.Match
+func (ar *AppMeshv1beta2Router) makeHeaders(canary *flaggerv1.Canary) []appmeshv1.HTTPRouteHeader {
+
+	var headers []appmeshv1.HTTPRouteHeader
+	for _, m := range canary.GetAnalysis().Match {
+		for key, value := range m.Headers {
+			header := appmeshv1.HTTPRouteHeader{
+				Name: key,
+				Match: &appmeshv1.HeaderMatchMethod{
+					Exact:  stringp(value.Exact),
+					Prefix: stringp(value.Prefix),
+					Regex:  stringp(value.Regex),
+					Suffix: stringp(value.Suffix),
+				},
+			}
+			headers = append(headers, header)
+		}
+	}
+
+	return headers
+}
+
+func (ar *AppMeshv1beta2Router) getProtocol(canary *flaggerv1.Canary) appmeshv1.PortProtocol {
+	if strings.Contains(canary.Spec.Service.PortName, "grpc") {
+		return appmeshv1.PortProtocolGRPC
+	}
+	return appmeshv1.PortProtocolHTTP
+}
+
+func (ar *AppMeshv1beta2Router) getContainerPort(canary *flaggerv1.Canary) appmeshv1.PortNumber {
+	containerPort := canary.Spec.Service.Port
+	if canary.Spec.Service.TargetPort.IntVal > 0 {
+		containerPort = canary.Spec.Service.TargetPort.IntVal
+	}
+	return appmeshv1.PortNumber(containerPort)
+}
+
+func (ar *AppMeshv1beta2Router) gatewayAnnotations(canary *flaggerv1.Canary) map[string]string {
+	a := make(map[string]string)
+	domains := ""
+	for _, value := range canary.Spec.Service.Hosts {
+		domains += value + ","
+	}
+	if domains != "" {
+		a["gateway.appmesh.k8s.aws/expose"] = "true"
+		a["gateway.appmesh.k8s.aws/domain"] = domains
+		if canary.Spec.Service.Timeout != "" {
+			a["gateway.appmesh.k8s.aws/timeout"] = canary.Spec.Service.Timeout
+		}
+		if canary.Spec.Service.Retries != nil && canary.Spec.Service.Retries.Attempts > 0 {
+			a["gateway.appmesh.k8s.aws/retries"] = strconv.Itoa(canary.Spec.Service.Retries.Attempts)
+		}
+	}
+	return a
+}
+
+func (ar *AppMeshv1beta2Router) Finalize(_ *flaggerv1.Canary) error {
+	return nil
+}
diff --git a/pkg/router/factory.go b/pkg/router/factory.go
index 5e28868a..9efcf8a6 100644
--- a/pkg/router/factory.go
+++ b/pkg/router/factory.go
@@ -7,6 +7,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 	restclient "k8s.io/client-go/rest"
 
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
 	clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
 )
 
@@ -51,53 +52,58 @@ func (factory *Factory) KubernetesRouter(kind string, labelSelector string, port
 }
 
 // MeshRouter returns a service mesh router
-func (factory *Factory) MeshRouter(provider string) Interface {
+func (factory *Factory) MeshRouter(provider string, labelSelector string) Interface {
 	switch {
-	case provider == "none":
-		return &NopRouter{}
-	case provider == "kubernetes":
-		return &NopRouter{}
-	case provider == "nginx":
-		return &IngressRouter{
-			logger:            factory.logger,
-			kubeClient:        factory.kubeClient,
-			annotationsPrefix: factory.ingressAnnotationsPrefix,
+	case strings.HasPrefix(provider, flaggerv1.AppMeshProvider+":v1beta2"):
+		return &AppMeshv1beta2Router{
+			logger:        factory.logger,
+			flaggerClient: factory.flaggerClient,
+			kubeClient:    factory.kubeClient,
+			appmeshClient: factory.meshClient,
+			labelSelector: labelSelector,
 		}
-	case provider == "appmesh":
+	case provider == flaggerv1.AppMeshProvider:
 		return &AppMeshRouter{
 			logger:        factory.logger,
 			flaggerClient: factory.flaggerClient,
 			kubeClient:    factory.kubeClient,
 			appmeshClient: factory.meshClient,
 		}
-	case strings.HasPrefix(provider, "smi:"):
-		mesh := strings.TrimPrefix(provider, "smi:")
+	case provider == flaggerv1.LinkerdProvider:
 		return &SmiRouter{
 			logger:        factory.logger,
 			flaggerClient: factory.flaggerClient,
 			kubeClient:    factory.kubeClient,
 			smiClient:     factory.meshClient,
-			targetMesh:    mesh,
+			targetMesh:    flaggerv1.LinkerdProvider,
+		}
+	case provider == flaggerv1.IstioProvider:
+		return &IstioRouter{
+			logger:        factory.logger,
+			flaggerClient: factory.flaggerClient,
+			kubeClient:    factory.kubeClient,
+			istioClient:   factory.meshClient,
 		}
-	case provider == "linkerd":
+	case strings.HasPrefix(provider, flaggerv1.SMIProvider):
+		mesh := strings.TrimPrefix(provider, flaggerv1.SMIProvider+":")
 		return &SmiRouter{
 			logger:        factory.logger,
 			flaggerClient: factory.flaggerClient,
 			kubeClient:    factory.kubeClient,
 			smiClient:     factory.meshClient,
-			targetMesh:    "linkerd",
+			targetMesh:    mesh,
 		}
-	case provider == "contour":
+	case provider == flaggerv1.ContourProvider:
 		return &ContourRouter{
 			logger:        factory.logger,
 			flaggerClient: factory.flaggerClient,
 			kubeClient:    factory.kubeClient,
 			contourClient: factory.meshClient,
 		}
-	case strings.HasPrefix(provider, "gloo"):
-		upstreamDiscoveryNs := "gloo-system"
-		if strings.HasPrefix(provider, "gloo:") {
-			upstreamDiscoveryNs = strings.TrimPrefix(provider, "gloo:")
+	case strings.HasPrefix(provider, flaggerv1.GlooProvider):
+		upstreamDiscoveryNs := flaggerv1.GlooProvider + "-system"
+		if strings.HasPrefix(provider, flaggerv1.GlooProvider+":") {
+			upstreamDiscoveryNs = strings.TrimPrefix(provider, flaggerv1.GlooProvider+":")
 		}
 		return &GlooRouter{
 			logger:              factory.logger,
@@ -106,28 +112,14 @@ func (factory *Factory) MeshRouter(provider string) Interface {
 			glooClient:          factory.meshClient,
 			upstreamDiscoveryNs: upstreamDiscoveryNs,
 		}
-	case strings.HasPrefix(provider, "supergloo:appmesh"):
-		return &AppMeshRouter{
-			logger:        factory.logger,
-			flaggerClient: factory.flaggerClient,
-			kubeClient:    factory.kubeClient,
-			appmeshClient: factory.meshClient,
-		}
-	case strings.HasPrefix(provider, "supergloo:istio"):
-		return &IstioRouter{
-			logger:        factory.logger,
-			flaggerClient: factory.flaggerClient,
-			kubeClient:    factory.kubeClient,
-			istioClient:   factory.meshClient,
-		}
-	case strings.HasPrefix(provider, "supergloo:linkerd"):
-		return &SmiRouter{
-			logger:        factory.logger,
-			flaggerClient: factory.flaggerClient,
-			kubeClient:    factory.kubeClient,
-			smiClient:     factory.meshClient,
-			targetMesh:    "linkerd",
+	case provider == flaggerv1.NGINXProvider:
+		return &IngressRouter{
+			logger:            factory.logger,
+			kubeClient:        factory.kubeClient,
+			annotationsPrefix: factory.ingressAnnotationsPrefix,
 		}
+	case provider == flaggerv1.KubernetesProvider:
+		return &NopRouter{}
 	default:
 		return &IstioRouter{
 			logger:        factory.logger,
diff --git a/pkg/router/kubernetes_default.go b/pkg/router/kubernetes_default.go
index a8d4a9c9..614f79aa 100644
--- a/pkg/router/kubernetes_default.go
+++ b/pkg/router/kubernetes_default.go
@@ -181,7 +181,6 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam
 
 		portsDiff := cmp.Diff(svcSpec.Ports, svc.Spec.Ports, cmpopts.SortSlices(sortPorts))
 		selectorsDiff := cmp.Diff(svcSpec.Selector, svc.Spec.Selector)
-
 		if portsDiff != "" || selectorsDiff != "" {
 			svcClone.Spec.Ports = svcSpec.Ports
 			svcClone.Spec.Selector = svcSpec.Selector
@@ -191,11 +190,14 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam
 
 		// update annotations and labels only if the service has been created by Flagger
 		if _, owned := c.isOwnedByCanary(svc, canary.Name); owned {
-			if cmp.Diff(metadata.Annotations, svc.ObjectMeta.Annotations) != "" {
+			if svc.ObjectMeta.Annotations == nil {
+				svc.ObjectMeta.Annotations = make(map[string]string)
+			}
+			if diff := cmp.Diff(metadata.Annotations, svc.ObjectMeta.Annotations); diff != "" {
 				svcClone.ObjectMeta.Annotations = metadata.Annotations
 				updateService = true
 			}
-			if cmp.Diff(metadata.Labels, svc.ObjectMeta.Labels) != "" {
+			if diff := cmp.Diff(metadata.Labels, svc.ObjectMeta.Labels); diff != "" {
 				svcClone.ObjectMeta.Labels = metadata.Labels
 				updateService = true
 			}


Test patch:
diff --git a/pkg/controller/scheduler_daemonset_fixture_test.go b/pkg/controller/scheduler_daemonset_fixture_test.go
index cb70efcf..02d8778c 100644
--- a/pkg/controller/scheduler_daemonset_fixture_test.go
+++ b/pkg/controller/scheduler_daemonset_fixture_test.go
@@ -110,7 +110,7 @@ func newDaemonSetFixture(c *flaggerv1.Canary) daemonSetFixture {
 	ctrl.flaggerInformers.MetricInformer.Informer().GetIndexer().Add(newDaemonSetTestMetricTemplate())
 	ctrl.flaggerInformers.AlertInformer.Informer().GetIndexer().Add(newDaemonSetTestAlertProvider())
 
-	meshRouter := rf.MeshRouter("istio")
+	meshRouter := rf.MeshRouter("istio", "")
 
 	return daemonSetFixture{
 		canary:        c,
diff --git a/pkg/controller/scheduler_deployment_fixture_test.go b/pkg/controller/scheduler_deployment_fixture_test.go
index f44415d6..fadf8366 100644
--- a/pkg/controller/scheduler_deployment_fixture_test.go
+++ b/pkg/controller/scheduler_deployment_fixture_test.go
@@ -138,7 +138,7 @@ func newDeploymentFixture(c *flaggerv1.Canary) fixture {
 	ctrl.flaggerInformers.MetricInformer.Informer().GetIndexer().Add(newDeploymentTestMetricTemplate())
 	ctrl.flaggerInformers.AlertInformer.Informer().GetIndexer().Add(newDeploymentTestAlertProvider())
 
-	meshRouter := rf.MeshRouter("istio")
+	meshRouter := rf.MeshRouter("istio", "")
 
 	return fixture{
 		canary:        c,
diff --git a/pkg/metrics/observers/crossover_service_test.go b/pkg/metrics/observers/crossover_service_test.go
deleted file mode 100644
index b0c9fc4e..00000000
--- a/pkg/metrics/observers/crossover_service_test.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package observers
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
-	"github.com/weaveworks/flagger/pkg/metrics/providers"
-)
-
-func TestCrossoverServiceObserver_GetRequestSuccessRate(t *testing.T) {
-	expected := ` sum( rate( envoy_cluster_upstream_rq{ kubernetes_namespace="default", envoy_cluster_name="podinfo-canary", envoy_response_code!~"5.*" }[1m] ) ) / sum( rate( envoy_cluster_upstream_rq{ kubernetes_namespace="default", envoy_cluster_name="podinfo-canary" }[1m] ) ) * 100`
-
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		promql := r.URL.Query()["query"][0]
-		assert.Equal(t, expected, promql)
-
-		json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
-		w.Write([]byte(json))
-	}))
-	defer ts.Close()
-
-	client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
-		Type:      "prometheus",
-		Address:   ts.URL,
-		SecretRef: nil,
-	}, nil)
-	require.NoError(t, err)
-
-	observer := &CrossoverServiceObserver{
-		client: client,
-	}
-
-	val, err := observer.GetRequestSuccessRate(flaggerv1.MetricTemplateModel{
-		Name:      "podinfo",
-		Namespace: "default",
-		Target:    "podinfo",
-		Service:   "podinfo",
-		Interval:  "1m",
-	})
-	require.NoError(t, err)
-
-	assert.Equal(t, float64(100), val)
-}
-
-func TestCrossoverServiceObserver_GetRequestDuration(t *testing.T) {
-	expected := ` histogram_quantile( 0.99, sum( rate( envoy_cluster_upstream_rq_time_bucket{ kubernetes_namespace="default", envoy_cluster_name="podinfo-canary" }[1m] ) ) by (le) )`
-
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		promql := r.URL.Query()["query"][0]
-		assert.Equal(t, expected, promql)
-
-		json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
-		w.Write([]byte(json))
-	}))
-	defer ts.Close()
-
-	client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
-		Type:      "prometheus",
-		Address:   ts.URL,
-		SecretRef: nil,
-	}, nil)
-	require.NoError(t, err)
-
-	observer := &CrossoverServiceObserver{
-		client: client,
-	}
-
-	val, err := observer.GetRequestDuration(flaggerv1.MetricTemplateModel{
-		Name:      "podinfo",
-		Namespace: "default",
-		Target:    "podinfo",
-		Service:   "podinfo",
-		Interval:  "1m",
-	})
-	require.NoError(t, err)
-
-	assert.Equal(t, 100*time.Millisecond, val)
-}
diff --git a/pkg/metrics/observers/crossover_test.go b/pkg/metrics/observers/crossover_test.go
deleted file mode 100644
index 7cf6ed65..00000000
--- a/pkg/metrics/observers/crossover_test.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package observers
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
-	"github.com/weaveworks/flagger/pkg/metrics/providers"
-)
-
-func TestCrossoverObserver_GetRequestSuccessRate(t *testing.T) {
-	expected := ` sum( rate( envoy_cluster_upstream_rq{ kubernetes_namespace="default", envoy_cluster_name=~"podinfo-canary", envoy_response_code!~"5.*" }[1m] ) ) / sum( rate( envoy_cluster_upstream_rq{ kubernetes_namespace="default", envoy_cluster_name=~"podinfo-canary" }[1m] ) ) * 100`
-
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		promql := r.URL.Query()["query"][0]
-		assert.Equal(t, expected, promql)
-
-		json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
-		w.Write([]byte(json))
-	}))
-	defer ts.Close()
-
-	client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
-		Type:      "prometheus",
-		Address:   ts.URL,
-		SecretRef: nil,
-	}, nil)
-	require.NoError(t, err)
-
-	observer := &CrossoverObserver{
-		client: client,
-	}
-
-	val, err := observer.GetRequestSuccessRate(flaggerv1.MetricTemplateModel{
-		Name:      "podinfo",
-		Namespace: "default",
-		Target:    "podinfo",
-		Service:   "podinfo",
-		Interval:  "1m",
-	})
-	require.NoError(t, err)
-
-	assert.Equal(t, float64(100), val)
-}
-
-func TestCrossoverObserver_GetRequestDuration(t *testing.T) {
-	expected := ` histogram_quantile( 0.99, sum( rate( envoy_cluster_upstream_rq_time_bucket{ kubernetes_namespace="default", envoy_cluster_name=~"podinfo-canary" }[1m] ) ) by (le) )`
-
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		promql := r.URL.Query()["query"][0]
-		assert.Equal(t, expected, promql)
-
-		json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
-		w.Write([]byte(json))
-	}))
-	defer ts.Close()
-
-	client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
-		Type:      "prometheus",
-		Address:   ts.URL,
-		SecretRef: nil,
-	}, nil)
-	require.NoError(t, err)
-
-	observer := &CrossoverObserver{
-		client: client,
-	}
-
-	val, err := observer.GetRequestDuration(flaggerv1.MetricTemplateModel{
-		Name:      "podinfo",
-		Namespace: "default",
-		Target:    "podinfo",
-		Service:   "podinfo",
-		Interval:  "1m",
-	})
-	require.NoError(t, err)
-
-	assert.Equal(t, 100*time.Millisecond, val)
-}
diff --git a/pkg/router/appmesh_v1beta2_test.go b/pkg/router/appmesh_v1beta2_test.go
new file mode 100644
index 00000000..9425cc3f
--- /dev/null
+++ b/pkg/router/appmesh_v1beta2_test.go
@@ -0,0 +1,162 @@
+package router
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestAppmeshv1beta2Router_Reconcile(t *testing.T) {
+	mocks := newFixture(nil)
+	router := &AppMeshv1beta2Router{
+		logger:        mocks.logger,
+		flaggerClient: mocks.flaggerClient,
+		appmeshClient: mocks.meshClient,
+		kubeClient:    mocks.kubeClient,
+	}
+
+	apexName, primaryName, canaryName := mocks.appmeshCanary.GetServiceNames()
+	err := router.Reconcile(mocks.appmeshCanary)
+	require.NoError(t, err)
+
+	// check apex virtual service
+	_, err = router.appmeshClient.AppmeshV1beta2().VirtualServices("default").Get(context.TODO(), apexName, metav1.GetOptions{})
+	require.NoError(t, err)
+
+	// check canary virtual service
+	_, err = router.appmeshClient.AppmeshV1beta2().VirtualServices("default").Get(context.TODO(), canaryName, metav1.GetOptions{})
+	require.NoError(t, err)
+
+	// check apex virtual router
+	vrApex, err := router.appmeshClient.AppmeshV1beta2().VirtualRouters("default").Get(context.TODO(), apexName, metav1.GetOptions{})
+	require.NoError(t, err)
+	assert.Len(t, vrApex.Spec.Routes[0].HTTPRoute.Action.WeightedTargets, 2)
+
+	// check canary virtual router
+	vrCanary, err := router.appmeshClient.AppmeshV1beta2().VirtualRouters("default").Get(context.TODO(), canaryName, metav1.GetOptions{})
+	require.NoError(t, err)
+
+	// check if the canary virtual service routes all traffic to the canary virtual node
+	target := vrCanary.Spec.Routes[0].HTTPRoute.Action.WeightedTargets[0]
+	assert.Equal(t, canaryName, target.VirtualNodeRef.Name)
+	assert.Equal(t, int64(100), target.Weight)
+
+	// check primary virtual node
+	vnPrimary, err := router.appmeshClient.AppmeshV1beta2().VirtualNodes("default").Get(context.TODO(), primaryName, metav1.GetOptions{})
+	require.NoError(t, err)
+
+	primaryDNS := fmt.Sprintf("%s.%s.svc.cluster.local.", primaryName, mocks.appmeshCanary.Namespace)
+	assert.Equal(t, primaryDNS, vnPrimary.Spec.ServiceDiscovery.DNS.Hostname)
+
+	// test backends update
+	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), mocks.appmeshCanary.Name, metav1.GetOptions{})
+	require.NoError(t, err)
+
+	cdClone := cd.DeepCopy()
+	backends := cdClone.Spec.Service.Backends
+	backends = append(backends, "test.example.com")
+	cdClone.Spec.Service.Backends = backends
+	canary, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(context.TODO(), cdClone, metav1.UpdateOptions{})
+	require.NoError(t, err)
+
+	// apply change
+	err = router.Reconcile(canary)
+	require.NoError(t, err)
+
+	// verify
+	vnPrimary, err = router.appmeshClient.AppmeshV1beta2().VirtualNodes("default").Get(context.TODO(), primaryName, metav1.GetOptions{})
+	require.NoError(t, err)
+	require.Len(t, vnPrimary.Spec.Backends, 2)
+
+	// update URI
+	vrClone := vrApex.DeepCopy()
+	vrClone.Spec.Routes[0].HTTPRoute.Match.Prefix = "api"
+	vrApex, err = mocks.meshClient.AppmeshV1beta2().VirtualRouters("default").Update(context.TODO(), vrClone, metav1.UpdateOptions{})
+	require.NoError(t, err)
+
+	// undo URI change
+	err = router.Reconcile(canary)
+	require.NoError(t, err)
+	vrApex, err = router.appmeshClient.AppmeshV1beta2().VirtualRouters("default").Get(context.TODO(), apexName, metav1.GetOptions{})
+	require.NoError(t, err)
+	assert.Equal(t, "/", vrApex.Spec.Routes[0].HTTPRoute.Match.Prefix)
+}
+
+func TestAppmeshv1beta2Router_GetSetRoutes(t *testing.T) {
+	mocks := newFixture(nil)
+	router := &AppMeshv1beta2Router{
+		logger:        mocks.logger,
+		flaggerClient: mocks.flaggerClient,
+		appmeshClient: mocks.meshClient,
+		kubeClient:    mocks.kubeClient,
+	}
+
+	err := router.Reconcile(mocks.appmeshCanary)
+	require.NoError(t, err)
+
+	err = router.SetRoutes(mocks.appmeshCanary, 60, 40, false)
+	require.NoError(t, err)
+
+	p, c, m, err := router.GetRoutes(mocks.appmeshCanary)
+	require.NoError(t, err)
+	assert.Equal(t, 60, p)
+	assert.Equal(t, 40, c)
+	assert.False(t, m)
+}
+
+func TestAppmesv1beta2hRouter_ABTest(t *testing.T) {
+	mocks := newFixture(nil)
+	router := &AppMeshv1beta2Router{
+		logger:        mocks.logger,
+		flaggerClient: mocks.flaggerClient,
+		appmeshClient: mocks.meshClient,
+		kubeClient:    mocks.kubeClient,
+	}
+
+	apexName, _, _ := mocks.abtest.GetServiceNames()
+	err := router.Reconcile(mocks.abtest)
+	require.NoError(t, err)
+
+	vrApex, err := router.appmeshClient.AppmeshV1beta2().VirtualRouters("default").Get(context.TODO(), apexName, metav1.GetOptions{})
+	require.NoError(t, err)
+
+	// check routes
+	assert.Len(t, vrApex.Spec.Routes, 2)
+
+	// check headers
+	assert.GreaterOrEqual(t, len(vrApex.Spec.Routes[0].HTTPRoute.Match.Headers), 1, "Got no http match headers")
+	assert.Equal(t, "x-user-type", vrApex.Spec.Routes[0].HTTPRoute.Match.Headers[0].Name)
+	assert.Equal(t, "test", *vrApex.Spec.Routes[0].HTTPRoute.Match.Headers[0].Match.Exact)
+}
+
+func TestAppmeshv1beta2Router_Gateway(t *testing.T) {
+	mocks := newFixture(nil)
+	router := &AppMeshv1beta2Router{
+		logger:        mocks.logger,
+		flaggerClient: mocks.flaggerClient,
+		appmeshClient: mocks.meshClient,
+		kubeClient:    mocks.kubeClient,
+	}
+
+	apexName, _, _ := mocks.appmeshCanary.GetServiceNames()
+	err := router.Reconcile(mocks.appmeshCanary)
+	require.NoError(t, err)
+
+	vs, err := router.appmeshClient.AppmeshV1beta2().VirtualServices("default").Get(context.TODO(), apexName, metav1.GetOptions{})
+	require.NoError(t, err)
+
+	// check Flagger's gateway annotations
+	assert.Equal(t, "true", vs.Annotations["gateway.appmesh.k8s.aws/expose"])
+	assert.True(t, strings.Contains(vs.Annotations["gateway.appmesh.k8s.aws/domain"], mocks.appmeshCanary.Spec.Service.Hosts[0]))
+	assert.Equal(t, mocks.appmeshCanary.Spec.Service.Timeout, vs.Annotations["gateway.appmesh.k8s.aws/timeout"])
+
+	retries := vs.Annotations["gateway.appmesh.k8s.aws/retries"]
+	assert.Equal(t, strconv.Itoa(mocks.appmeshCanary.Spec.Service.Retries.Attempts), retries)
+}