Repository: crossplane/provider-aws (go)

Issue:
Replication group MultiAZ
I am launching an Elasticache (non cluster mode) with below composition.

```
 - name: ElastiCacheinstance
      base:
        apiVersion: cache.aws.crossplane.io/v1beta1
        kind: ReplicationGroup
        spec:
          forProvider:
            cacheSubnetGroupNameSelector:
              matchControllerRef: true
            securityGroupIDSelector:
              matchControllerRef: true
            applyModificationsImmediately: true
            atRestEncryptionEnabled: true
            numCacheClusters: 2
            automaticFailoverEnabled: true
          writeConnectionSecretToRef:
```

numCacheClusters value is 2 and automaticFailoverEnabled is enabled but MultiAZ is not enabled .
As per documentation it should be enabled when automaticFailoverEnabled is set to true.

I am able to enable Multi AZ by modifying it manually. 

provider-aws: v0.20.0

Pull request description:
Replication group fixes: multiAz and Tag update support
### Description of your changes

This PR adds functionality to update multiAZ and tags for ElastiCache Replication groups. 

Fixes #914

I have:

- [x] Read and followed Crossplane's [contribution process].
- [x] Run `make reviewable test` to ensure this PR is ready for review.

### How has this code been tested

- Created a replication group and changed Tags and MultiAZenabled.

```yaml
apiVersion: cache.aws.crossplane.io/v1beta1
kind: ReplicationGroup
metadata:
  name: redis-replication-group
spec:
  forProvider:
    region: us-east-1
    replicationGroupDescription: "A redis replication group"
    applyModificationsImmediately: true
    engine: "redis"
    engineVersion: "5.0.6"
    port: 6379
    cacheSubnetGroupName: borrelli-cache-test
    #cacheSubnetGroupName: default
    numCacheClusters: 2
    cacheParameterGroupName: default.redis5.0
    cacheNodeType: cache.t3.medium
    securityGroupIds:
      - sg-xxxxxyyyyy 
    automaticFailoverEnabled: true
    multiAZEnabled: false
    tags:
      - key: Name
        value: redis-replication-group
      #- key: VS
      #  value: sandbox
      - key: Environment
        value: dev
      - key: Squad
        value: my-team
      - key: Tier
        value: sandbox
```

[contribution process]: https://git.io/fj2m9


Patch:
diff --git a/apis/cache/v1beta1/replication_group_types.go b/apis/cache/v1beta1/replication_group_types.go
index 08def5c95..dd28580e5 100644
--- a/apis/cache/v1beta1/replication_group_types.go
+++ b/apis/cache/v1beta1/replication_group_types.go
@@ -282,8 +282,8 @@ type ReplicationGroupParameters struct {
 
 	// AutomaticFailoverEnabled specifies whether a read-only replica is
 	// automatically promoted to read/write primary if the existing primary
-	// fails. If true, Multi-AZ is enabled for this replication group. If false,
-	// Multi-AZ is disabled for this replication group.
+	// fails. Must be set to true if Multi-AZ is enabled for this replication group.
+	// If false, Multi-AZ cannot be enabled for this replication group.
 	//
 	// AutomaticFailoverEnabled must be enabled for Redis (cluster mode enabled)
 	// replication groups.
@@ -386,6 +386,13 @@ type ReplicationGroupParameters struct {
 	// +optional
 	EngineVersion *string `json:"engineVersion,omitempty"`
 
+	// MultiAZEnabled specifies if Multi-AZ is enabled to enhance fault tolerance
+	// You must have nodes across two or more Availability Zones in order to enable
+	// this feature.
+	// If this feature is set, automaticFailoverEnabled must be set to true.
+	// +optional
+	MultiAZEnabled *bool `json:"multiAZEnabled,omitempty"`
+
 	// NodeGroupConfigurationSpec specifies a list of node group (shard)
 	// configuration options.
 	//
@@ -423,7 +430,7 @@ type ReplicationGroupParameters struct {
 	// between 2 and 6.
 	//
 	// The maximum permitted value for NumCacheClusters is 6 (1 primary plus 5 replicas).
-	// +immutable
+	// +kubebuilder:validation:Maximum=6
 	// +optional
 	NumCacheClusters *int `json:"numCacheClusters,omitempty"`
 
@@ -550,7 +557,6 @@ type ReplicationGroupParameters struct {
 
 	// A list of cost allocation tags to be added to this resource. A tag is a key-value
 	// pair.
-	// +immutable
 	// +optional
 	Tags []Tag `json:"tags,omitempty"`
 
diff --git a/apis/cache/v1beta1/zz_generated.deepcopy.go b/apis/cache/v1beta1/zz_generated.deepcopy.go
index c46dc97af..64ebb108f 100644
--- a/apis/cache/v1beta1/zz_generated.deepcopy.go
+++ b/apis/cache/v1beta1/zz_generated.deepcopy.go
@@ -269,6 +269,11 @@ func (in *ReplicationGroupParameters) DeepCopyInto(out *ReplicationGroupParamete
 		*out = new(string)
 		**out = **in
 	}
+	if in.MultiAZEnabled != nil {
+		in, out := &in.MultiAZEnabled, &out.MultiAZEnabled
+		*out = new(bool)
+		**out = **in
+	}
 	if in.NodeGroupConfiguration != nil {
 		in, out := &in.NodeGroupConfiguration, &out.NodeGroupConfiguration
 		*out = make([]NodeGroupConfigurationSpec, len(*in))
diff --git a/package/crds/cache.aws.crossplane.io_replicationgroups.yaml b/package/crds/cache.aws.crossplane.io_replicationgroups.yaml
index 0df64e09a..efd7b4971 100644
--- a/package/crds/cache.aws.crossplane.io_replicationgroups.yaml
+++ b/package/crds/cache.aws.crossplane.io_replicationgroups.yaml
@@ -100,14 +100,14 @@ spec:
                   automaticFailoverEnabled:
                     description: "AutomaticFailoverEnabled specifies whether a read-only
                       replica is automatically promoted to read/write primary if the
-                      existing primary fails. If true, Multi-AZ is enabled for this
-                      replication group. If false, Multi-AZ is disabled for this replication
-                      group. \n AutomaticFailoverEnabled must be enabled for Redis
-                      (cluster mode enabled) replication groups. \n Amazon ElastiCache
-                      for Redis does not support Multi-AZ with automatic failover
-                      on: * Redis versions earlier than 2.8.6. * Redis (cluster mode
-                      disabled): T1 and T2 cache node types. * Redis (cluster mode
-                      enabled): T1 node types."
+                      existing primary fails. Must be set to true if Multi-AZ is enabled
+                      for this replication group. If false, Multi-AZ cannot be enabled
+                      for this replication group. \n AutomaticFailoverEnabled must
+                      be enabled for Redis (cluster mode enabled) replication groups.
+                      \n Amazon ElastiCache for Redis does not support Multi-AZ with
+                      automatic failover on: * Redis versions earlier than 2.8.6.
+                      * Redis (cluster mode disabled): T1 and T2 cache node types.
+                      * Redis (cluster mode enabled): T1 node types."
                     type: boolean
                   cacheNodeType:
                     description: 'CacheNodeType specifies the compute and memory capacity
@@ -227,6 +227,12 @@ spec:
                       version, you must delete the existing cluster or replication
                       group and create it anew with the earlier engine version."
                     type: string
+                  multiAZEnabled:
+                    description: MultiAZEnabled specifies if Multi-AZ is enabled to
+                      enhance fault tolerance You must have nodes across two or more
+                      Availability Zones in order to enable this feature. If this
+                      feature is set, automaticFailoverEnabled must be set to true.
+                    type: boolean
                   nodeGroupConfiguration:
                     description: "NodeGroupConfigurationSpec specifies a list of node
                       group (shard) configuration options. \n If you're creating a
@@ -287,6 +293,7 @@ spec:
                       (it will default to 1), or you can explicitly set it to a value
                       between 2 and 6. \n The maximum permitted value for NumCacheClusters
                       is 6 (1 primary plus 5 replicas)."
+                    maximum: 6
                     type: integer
                   numNodeGroups:
                     description: "NumNodeGroups specifies the number of node groups
diff --git a/pkg/clients/ec2/routetable.go b/pkg/clients/ec2/routetable.go
index 7e1bd2889..4e7db1c5b 100644
--- a/pkg/clients/ec2/routetable.go
+++ b/pkg/clients/ec2/routetable.go
@@ -4,7 +4,9 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"sort"
+	"strings"
 
 	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/service/ec2"
@@ -214,12 +216,56 @@ func IsRtUpToDate(p v1beta1.RouteTableParameters, rt ec2types.RouteTable) (bool,
 // SortRoutes sorts array of Routes on DestinationCIDR
 func SortRoutes(route []v1beta1.RouteBeta, ec2Route []ec2types.Route) {
 	sort.Slice(route, func(i, j int) bool {
-		return (route[i].DestinationCIDRBlock != nil && *route[i].DestinationCIDRBlock < *route[j].DestinationCIDRBlock) ||
-			(route[i].DestinationIPV6CIDRBlock != nil && *route[i].DestinationIPV6CIDRBlock < *route[j].DestinationIPV6CIDRBlock)
+		return compareRoutes(i, j, route)
 	})
 
 	sort.Slice(ec2Route, func(i, j int) bool {
-		return (ec2Route[i].DestinationCidrBlock != nil && *ec2Route[i].DestinationCidrBlock < *ec2Route[j].DestinationCidrBlock) ||
-			(ec2Route[i].DestinationIpv6CidrBlock != nil && *ec2Route[i].DestinationIpv6CidrBlock < *ec2Route[j].DestinationIpv6CidrBlock)
+		return compareEC2Routes(i, j, ec2Route)
 	})
 }
+
+func compareRoutes(i, j int, route []v1beta1.RouteBeta) bool {
+	if route[i].DestinationCIDRBlock != nil && route[j].DestinationCIDRBlock != nil {
+		return *route[i].DestinationCIDRBlock < *route[j].DestinationCIDRBlock
+	}
+	if route[i].DestinationIPV6CIDRBlock != nil && route[j].DestinationIPV6CIDRBlock != nil {
+		return *route[i].DestinationIPV6CIDRBlock < *route[j].DestinationIPV6CIDRBlock
+	}
+	if route[i].DestinationCIDRBlock != nil && route[j].DestinationIPV6CIDRBlock != nil {
+		return true
+	}
+	if route[i].DestinationIPV6CIDRBlock != nil && route[j].DestinationCIDRBlock != nil {
+		return false
+	}
+	return false
+}
+
+func compareEC2Routes(i, j int, ec2Route []ec2types.Route) bool {
+	if ec2Route[i].DestinationCidrBlock != nil && ec2Route[j].DestinationCidrBlock != nil {
+		return *ec2Route[i].DestinationCidrBlock < *ec2Route[j].DestinationCidrBlock
+	}
+	if ec2Route[i].DestinationIpv6CidrBlock != nil && ec2Route[j].DestinationIpv6CidrBlock != nil {
+		return *ec2Route[i].DestinationIpv6CidrBlock < *ec2Route[j].DestinationIpv6CidrBlock
+	}
+	if ec2Route[i].DestinationCidrBlock != nil && ec2Route[j].DestinationIpv6CidrBlock != nil {
+		return true
+	}
+	if ec2Route[i].DestinationIpv6CidrBlock != nil && ec2Route[j].DestinationCidrBlock != nil {
+		return false
+	}
+	return false
+}
+
+// ValidateRoutes on empty cidrs
+func ValidateRoutes(route []v1beta1.RouteBeta) error {
+	errs := make([]string, 0, len(route))
+	for i, r := range route {
+		if r.DestinationCIDRBlock == nil && r.DestinationIPV6CIDRBlock == nil {
+			errs = append(errs, fmt.Sprintf("route[%d]: both v4 and v6 cidrs are empty", i))
+		}
+	}
+	if len(errs) > 0 {
+		return fmt.Errorf("invalid routes: %s", strings.Join(errs, ";"))
+	}
+	return nil
+}
diff --git a/pkg/clients/elasticache/elasticache.go b/pkg/clients/elasticache/elasticache.go
index 945526abe..f2fbb480a 100644
--- a/pkg/clients/elasticache/elasticache.go
+++ b/pkg/clients/elasticache/elasticache.go
@@ -61,6 +61,10 @@ type Client interface {
 	ModifyCacheCluster(context.Context, *elasticache.ModifyCacheClusterInput, ...func(*elasticache.Options)) (*elasticache.ModifyCacheClusterOutput, error)
 
 	ModifyReplicationGroupShardConfiguration(context.Context, *elasticache.ModifyReplicationGroupShardConfigurationInput, ...func(*elasticache.Options)) (*elasticache.ModifyReplicationGroupShardConfigurationOutput, error)
+
+	ListTagsForResource(context.Context, *elasticache.ListTagsForResourceInput, ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error)
+	AddTagsToResource(context.Context, *elasticache.AddTagsToResourceInput, ...func(*elasticache.Options)) (*elasticache.AddTagsToResourceOutput, error)
+	RemoveTagsFromResource(context.Context, *elasticache.RemoveTagsFromResourceInput, ...func(*elasticache.Options)) (*elasticache.RemoveTagsFromResourceOutput, error)
 }
 
 // NewClient returns a new ElastiCache client. Credentials must be passed as
@@ -88,6 +92,7 @@ func NewCreateReplicationGroupInput(g v1beta1.ReplicationGroupParameters, id str
 		CacheSecurityGroupNames:    g.CacheSecurityGroupNames,
 		CacheSubnetGroupName:       g.CacheSubnetGroupName,
 		EngineVersion:              g.EngineVersion,
+		MultiAZEnabled:             g.MultiAZEnabled,
 		NotificationTopicArn:       g.NotificationTopicARN,
 		NumCacheClusters:           clients.Int32Address(g.NumCacheClusters),
 		NumNodeGroups:              clients.Int32Address(g.NumNodeGroups),
@@ -137,6 +142,7 @@ func NewModifyReplicationGroupInput(g v1beta1.ReplicationGroupParameters, id str
 		CacheParameterGroupName:     g.CacheParameterGroupName,
 		CacheSecurityGroupNames:     g.CacheSecurityGroupNames,
 		EngineVersion:               g.EngineVersion,
+		MultiAZEnabled:              g.MultiAZEnabled,
 		NotificationTopicArn:        g.NotificationTopicARN,
 		NotificationTopicStatus:     g.NotificationTopicStatus,
 		PreferredMaintenanceWindow:  g.PreferredMaintenanceWindow,
@@ -187,6 +193,12 @@ func NewDescribeCacheClustersInput(clusterID string) *elasticache.DescribeCacheC
 	return &elasticache.DescribeCacheClustersInput{CacheClusterId: &clusterID}
 }
 
+// NewListTagsForResourceInput returns ElastiCache list tags input usable with the
+// AWS API
+func NewListTagsForResourceInput(arn *string) *elasticache.ListTagsForResourceInput {
+	return &elasticache.ListTagsForResourceInput{ResourceName: arn}
+}
+
 // LateInitialize assigns the observed configurations and assigns them to the
 // corresponding fields in ReplicationGroupParameters in order to let user
 // know the defaults and make the changes as wished on that value.
@@ -248,6 +260,8 @@ func ReplicationGroupNeedsUpdate(kube v1beta1.ReplicationGroupParameters, rg ela
 		return true
 	case !reflect.DeepEqual(kube.SnapshotWindow, rg.SnapshotWindow):
 		return true
+	case aws.ToBool(kube.MultiAZEnabled) != aws.ToBool(multiAZEnabled(rg.MultiAZ)):
+		return true
 	}
 	for _, cc := range ccList {
 		if cacheClusterNeedsUpdate(kube, cc) {
@@ -265,6 +279,17 @@ func automaticFailoverEnabled(af elasticachetypes.AutomaticFailoverStatus) *bool
 	return &r
 }
 
+func multiAZEnabled(maz elasticachetypes.MultiAZStatus) *bool {
+	switch maz {
+	case elasticachetypes.MultiAZStatusEnabled:
+		return aws.Bool(true)
+	case elasticachetypes.MultiAZStatusDisabled:
+		return aws.Bool(false)
+	default:
+		return nil
+	}
+}
+
 func versionMatches(kubeVersion *string, awsVersion *string) bool {
 	switch {
 	case clients.StringValue(kubeVersion) == clients.StringValue(awsVersion):
@@ -334,6 +359,33 @@ func sgNamesNeedUpdate(kube []string, cc []elasticachetypes.CacheSecurityGroupMe
 	return false
 }
 
+// ReplicationGroupTagsNeedsUpdate indicates whether tags need updating
+func ReplicationGroupTagsNeedsUpdate(kube []v1beta1.Tag, tags []elasticachetypes.Tag) bool {
+	if len(kube) != len(tags) {
+		return true
+	}
+
+	add, remove := DiffTags(kube, tags)
+
+	return len(add) != 0 || len(remove) != 0
+}
+
+// DiffTags returns tags that should be added or removed.
+func DiffTags(rgtags []v1beta1.Tag, tags []elasticachetypes.Tag) (add map[string]string, remove []string) {
+
+	local := make(map[string]string, len(rgtags))
+	for _, t := range rgtags {
+		local[t.Key] = t.Value
+	}
+
+	remote := make(map[string]string, len(tags))
+	for _, t := range tags {
+		remote[aws.ToString(t.Key)] = aws.ToString(t.Value)
+	}
+
+	return clients.DiffTags(local, remote)
+}
+
 // GenerateObservation produces a ReplicationGroupObservation object out of
 // received elasticache.ReplicationGroup object.
 func GenerateObservation(rg elasticachetypes.ReplicationGroup) v1beta1.ReplicationGroupObservation {
diff --git a/pkg/clients/elasticache/fake/fake.go b/pkg/clients/elasticache/fake/fake.go
index 5dd619edb..c33e53fc3 100644
--- a/pkg/clients/elasticache/fake/fake.go
+++ b/pkg/clients/elasticache/fake/fake.go
@@ -42,6 +42,10 @@ type MockClient struct {
 	MockModifyCacheCluster    func(context.Context, *elasticache.ModifyCacheClusterInput, []func(*elasticache.Options)) (*elasticache.ModifyCacheClusterOutput, error)
 
 	MockModifyReplicationGroupShardConfiguration func(context.Context, *elasticache.ModifyReplicationGroupShardConfigurationInput, []func(*elasticache.Options)) (*elasticache.ModifyReplicationGroupShardConfigurationOutput, error)
+
+	MockListTagsForResource    func(context.Context, *elasticache.ListTagsForResourceInput, []func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error)
+	MockAddTagsToResource      func(context.Context, *elasticache.AddTagsToResourceInput, []func(*elasticache.Options)) (*elasticache.AddTagsToResourceOutput, error)
+	MockRemoveTagsFromResource func(context.Context, *elasticache.RemoveTagsFromResourceInput, []func(*elasticache.Options)) (*elasticache.RemoveTagsFromResourceOutput, error)
 }
 
 // DescribeReplicationGroups calls the underlying
@@ -121,3 +125,21 @@ func (c *MockClient) DeleteCacheCluster(ctx context.Context, i *elasticache.Dele
 func (c *MockClient) ModifyCacheCluster(ctx context.Context, i *elasticache.ModifyCacheClusterInput, opts ...func(*elasticache.Options)) (*elasticache.ModifyCacheClusterOutput, error) {
 	return c.MockModifyCacheCluster(ctx, i, opts)
 }
+
+// ListTagsForResource calls the underlying
+// MockListTagsForResource method
+func (c *MockClient) ListTagsForResource(ctx context.Context, i *elasticache.ListTagsForResourceInput, opts ...func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) {
+	return c.MockListTagsForResource(ctx, i, opts)
+}
+
+// AddTagsToResource calls the underlying
+// MockAddTagsToResource method
+func (c *MockClient) AddTagsToResource(ctx context.Context, i *elasticache.AddTagsToResourceInput, opts ...func(*elasticache.Options)) (*elasticache.AddTagsToResourceOutput, error) {
+	return c.MockAddTagsToResource(ctx, i, opts)
+}
+
+// RemoveTagsFromResource calls the underlying
+// MockRemoveTagsFromResource method
+func (c *MockClient) RemoveTagsFromResource(ctx context.Context, i *elasticache.RemoveTagsFromResourceInput, opts ...func(*elasticache.Options)) (*elasticache.RemoveTagsFromResourceOutput, error) {
+	return c.MockRemoveTagsFromResource(ctx, i, opts)
+}
diff --git a/pkg/controller/cache/managed.go b/pkg/controller/cache/managed.go
index 92b4b2399..f50115a32 100644
--- a/pkg/controller/cache/managed.go
+++ b/pkg/controller/cache/managed.go
@@ -46,15 +46,17 @@ import (
 
 // Error strings.
 const (
-	errUpdateReplicationGroupCR = "cannot update ReplicationGroup Custom Resource"
-	errGetCacheClusterList      = "cannot get cache cluster list"
-	errNotReplicationGroup      = "managed resource is not an ElastiCache replication group"
-	errDescribeReplicationGroup = "cannot describe ElastiCache replication group"
-	errGenerateAuthToken        = "cannot generate ElastiCache auth token"
-	errCreateReplicationGroup   = "cannot create ElastiCache replication group"
-	errModifyReplicationGroup   = "cannot modify ElastiCache replication group"
-	errDeleteReplicationGroup   = "cannot delete ElastiCache replication group"
-	errModifyReplicationGroupSC = "cannot modify ElastiCache replication group shard configuration"
+	errUpdateReplicationGroupCR   = "cannot update ReplicationGroup Custom Resource"
+	errGetCacheClusterList        = "cannot get cache cluster list"
+	errNotReplicationGroup        = "managed resource is not an ElastiCache replication group"
+	errDescribeReplicationGroup   = "cannot describe ElastiCache replication group"
+	errGenerateAuthToken          = "cannot generate ElastiCache auth token"
+	errCreateReplicationGroup     = "cannot create ElastiCache replication group"
+	errModifyReplicationGroup     = "cannot modify ElastiCache replication group"
+	errDeleteReplicationGroup     = "cannot delete ElastiCache replication group"
+	errModifyReplicationGroupSC   = "cannot modify ElastiCache replication group shard configuration"
+	errListReplicationGroupTags   = "cannot list ElastiCache replication group tags"
+	errUpdateReplicationGroupTags = "cannot update ElastiCache replication group tags"
 )
 
 // SetupReplicationGroup adds a controller that reconciles ReplicationGroups.
@@ -117,7 +119,6 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
 	// ask for one group by name, so we should get either a single element list
 	// or an error.
 	rg := rsp.ReplicationGroups[0]
-
 	ccList, err := getCacheClusterList(ctx, e.client, rg.MemberClusters)
 	if err != nil {
 		return managed.ExternalObservation{}, awsclient.Wrap(err, errGetCacheClusterList)
@@ -147,9 +148,20 @@ func (e *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex
 		cr.Status.SetConditions(xpv1.Unavailable())
 	}
 
+	var tagsNeedUpdate bool
+	if cr.Status.AtProvider.Status == v1beta1.StatusAvailable {
+		tags, err := e.client.ListTagsForResource(ctx, elasticache.NewListTagsForResourceInput(rg.ARN))
+		if err != nil {
+			return managed.ExternalObservation{}, awsclient.Wrap(resource.Ignore(elasticache.IsNotFound, err), errListReplicationGroupTags)
+		}
+		tagsNeedUpdate = elasticache.ReplicationGroupTagsNeedsUpdate(cr.Spec.ForProvider.Tags, tags.TagList)
+	}
+
 	return managed.ExternalObservation{
-		ResourceExists:    true,
-		ResourceUpToDate:  !elasticache.ReplicationGroupNeedsUpdate(cr.Spec.ForProvider, rg, ccList) && !elasticache.ReplicationGroupShardConfigurationNeedsUpdate(cr.Spec.ForProvider, rg),
+		ResourceExists: true,
+		ResourceUpToDate: !elasticache.ReplicationGroupNeedsUpdate(cr.Spec.ForProvider, rg, ccList) &&
+			!elasticache.ReplicationGroupShardConfigurationNeedsUpdate(cr.Spec.ForProvider, rg) &&
+			!tagsNeedUpdate,
 		ConnectionDetails: elasticache.ConnectionEndpoint(rg),
 	}, nil
 }
@@ -214,8 +226,23 @@ func (e *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext
 		return managed.ExternalUpdate{}, nil
 	}
 
-	_, err = e.client.ModifyReplicationGroup(ctx, elasticache.NewModifyReplicationGroupInput(cr.Spec.ForProvider, meta.GetExternalName(cr)))
-	return managed.ExternalUpdate{}, awsclient.Wrap(err, errModifyReplicationGroup)
+	ccList, err := getCacheClusterList(ctx, e.client, rg.MemberClusters)
+	if err != nil {
+		return managed.ExternalUpdate{}, awsclient.Wrap(err, errGetCacheClusterList)
+	}
+
+	if elasticache.ReplicationGroupNeedsUpdate(cr.Spec.ForProvider, rg, ccList) {
+		_, err = e.client.ModifyReplicationGroup(ctx, elasticache.NewModifyReplicationGroupInput(cr.Spec.ForProvider, meta.GetExternalName(cr)))
+		if err != nil {
+			return managed.ExternalUpdate{}, awsclient.Wrap(err, errModifyReplicationGroup)
+		}
+		// perform one update at a time
+		return managed.ExternalUpdate{}, nil
+
+	}
+
+	err = e.updateTags(ctx, cr.Spec.ForProvider.Tags, rg.ARN)
+	return managed.ExternalUpdate{}, awsclient.Wrap(err, errUpdateReplicationGroupTags)
 }
 
 func (e *external) Delete(ctx context.Context, mg resource.Managed) error {
@@ -231,6 +258,29 @@ func (e *external) Delete(ctx context.Context, mg resource.Managed) error {
 	return awsclient.Wrap(resource.Ignore(elasticache.IsNotFound, err), errDeleteReplicationGroup)
 }
 
+func (e *external) updateTags(ctx context.Context, tags []v1beta1.Tag, arn *string) error {
+	resp, err := e.client.ListTagsForResource(ctx, elasticache.NewListTagsForResourceInput(arn))
+	if err != nil {
+		return awsclient.Wrap(err, errListReplicationGroupTags)
+	}
+	add, remove := elasticache.DiffTags(tags, resp.TagList)
+	if len(remove) != 0 {
+		if _, err := e.client.RemoveTagsFromResource(ctx, &awselasticache.RemoveTagsFromResourceInput{ResourceName: arn, TagKeys: remove}); err != nil {
+			return awsclient.Wrap(err, errUpdateReplicationGroupTags)
+		}
+	}
+	if len(add) != 0 {
+		addTags := []awselasticachetypes.Tag{}
+		for k, v := range add {
+			addTags = append(addTags, awselasticachetypes.Tag{Key: aws.String(k), Value: aws.String(v)})
+		}
+		if _, err := e.client.AddTagsToResource(ctx, &awselasticache.AddTagsToResourceInput{ResourceName: arn, Tags: addTags}); err != nil {
+			return awsclient.Wrap(err, errUpdateReplicationGroupTags)
+		}
+	}
+	return nil
+}
+
 type tagger struct {
 	kube client.Client
 }
diff --git a/pkg/controller/ec2/routetable/controller.go b/pkg/controller/ec2/routetable/controller.go
index 384fef43f..f6cf736f7 100644
--- a/pkg/controller/ec2/routetable/controller.go
+++ b/pkg/controller/ec2/routetable/controller.go
@@ -45,6 +45,7 @@ import (
 
 const (
 	errUnexpectedObject = "The managed resource is not an RouteTable resource"
+	errInvalidRoutes    = "RouteTable routes are invalid"
 
 	errDescribe           = "failed to describe RouteTable"
 	errMultipleItems      = "retrieved multiple RouteTables for the given routeTableId"
@@ -112,7 +113,9 @@ func (e *external) Observe(ctx context.Context, mgd resource.Managed) (managed.E
 	if !ok {
 		return managed.ExternalObservation{}, errors.New(errUnexpectedObject)
 	}
-
+	if err := ec2.ValidateRoutes(cr.Spec.ForProvider.Routes); err != nil {
+		return managed.ExternalObservation{}, errors.Wrap(err, errInvalidRoutes)
+	}
 	// To find out whether a RouteTable exist:
 	// - the object's ExternalName should have routeTableId populated
 	// - a RouteTable with the given routeTableId should exist
@@ -169,6 +172,9 @@ func (e *external) Create(ctx context.Context, mgd resource.Managed) (managed.Ex
 	if !ok {
 		return managed.ExternalCreation{}, errors.New(errUnexpectedObject)
 	}
+	if err := ec2.ValidateRoutes(cr.Spec.ForProvider.Routes); err != nil {
+		return managed.ExternalCreation{}, errors.Wrap(err, errInvalidRoutes)
+	}
 	result, err := e.client.CreateRouteTable(ctx, &awsec2.CreateRouteTableInput{
 		VpcId: cr.Spec.ForProvider.VPCID,
 	})
@@ -184,7 +190,9 @@ func (e *external) Update(ctx context.Context, mgd resource.Managed) (managed.Ex
 	if !ok {
 		return managed.ExternalUpdate{}, errors.New(errUnexpectedObject)
 	}
-
+	if err := ec2.ValidateRoutes(cr.Spec.ForProvider.Routes); err != nil {
+		return managed.ExternalUpdate{}, errors.Wrap(err, errInvalidRoutes)
+	}
 	response, err := e.client.DescribeRouteTables(ctx, &awsec2.DescribeRouteTablesInput{
 		RouteTableIds: []string{meta.GetExternalName(cr)},
 	})
@@ -248,6 +256,10 @@ func (e *external) Delete(ctx context.Context, mgd resource.Managed) error {
 		return errors.New(errUnexpectedObject)
 	}
 
+	if err := ec2.ValidateRoutes(cr.Spec.ForProvider.Routes); err != nil {
+		return errors.Wrap(err, errInvalidRoutes)
+	}
+
 	cr.Status.SetConditions(xpv1.Deleting())
 
 	// the subnet associations have to be deleted before deleting the route table.
diff --git a/pkg/controller/ec2/securitygroup/controller.go b/pkg/controller/ec2/securitygroup/controller.go
index 49bee9436..157b4d7fe 100644
--- a/pkg/controller/ec2/securitygroup/controller.go
+++ b/pkg/controller/ec2/securitygroup/controller.go
@@ -49,6 +49,7 @@ const (
 	errUnexpectedObject = "The managed resource is not an SecurityGroup resource"
 
 	errDescribe         = "failed to describe SecurityGroup"
+	errGetSecurityGroup = "failed to get SecurityGroup based on groupName"
 	errMultipleItems    = "retrieved multiple SecurityGroups for the given securityGroupId"
 	errCreate           = "failed to create the SecurityGroup resource"
 	errAuthorizeIngress = "failed to authorize ingress rules"
@@ -117,7 +118,13 @@ func (e *external) Observe(ctx context.Context, mgd resource.Managed) (managed.E
 	}
 
 	if meta.GetExternalName(cr) == "" {
-		return managed.ExternalObservation{}, nil
+		securityGroupArn, err := e.getSecurityGroupByName(ctx, cr.Spec.ForProvider.GroupName)
+		if securityGroupArn == nil || err != nil {
+			return managed.ExternalObservation{}, awsclient.Wrap(resource.Ignore(ec2.IsSecurityGroupNotFoundErr, err), errGetSecurityGroup)
+		}
+
+		meta.SetExternalName(cr, aws.ToString(securityGroupArn))
+		_ = e.kube.Update(ctx, cr)
 	}
 
 	response, err := e.sg.DescribeSecurityGroups(ctx, &awsec2.DescribeSecurityGroupsInput{
@@ -292,3 +299,17 @@ func (e *external) Delete(ctx context.Context, mgd resource.Managed) error {
 
 	return awsclient.Wrap(resource.Ignore(ec2.IsSecurityGroupNotFoundErr, err), errDelete)
 }
+
+func (e *external) getSecurityGroupByName(ctx context.Context, groupName string) (*string, error) {
+	groups, err := e.sg.DescribeSecurityGroups(ctx, &awsec2.DescribeSecurityGroupsInput{
+		Filters: []awsec2types.Filter{
+			{Name: aws.String("group-name"), Values: []string{groupName}},
+		},
+	})
+
+	if err != nil || len(groups.SecurityGroups) == 0 {
+		return nil, err
+	}
+
+	return groups.SecurityGroups[0].GroupId, nil
+}
diff --git a/pkg/controller/rds/dbinstance/setup.go b/pkg/controller/rds/dbinstance/setup.go
index 2541ea2d8..2f963540d 100644
--- a/pkg/controller/rds/dbinstance/setup.go
+++ b/pkg/controller/rds/dbinstance/setup.go
@@ -206,17 +206,18 @@ func lateInitialize(in *svcapitypes.DBInstanceParameters, out *svcsdk.DescribeDB
 	in.DBClusterIdentifier = aws.LateInitializeStringPtr(in.DBClusterIdentifier, db.DBClusterIdentifier)
 	if in.DBClusterIdentifier == nil {
 		in.AllocatedStorage = aws.LateInitializeInt64Ptr(in.AllocatedStorage, db.AllocatedStorage)
+		in.BackupRetentionPeriod = aws.LateInitializeInt64Ptr(in.BackupRetentionPeriod, db.BackupRetentionPeriod)
+		in.CopyTagsToSnapshot = aws.LateInitializeBoolPtr(in.CopyTagsToSnapshot, db.CopyTagsToSnapshot)
+		in.EnableIAMDatabaseAuthentication = aws.LateInitializeBoolPtr(in.EnableIAMDatabaseAuthentication, db.IAMDatabaseAuthenticationEnabled)
+		in.PreferredBackupWindow = aws.LateInitializeStringPtr(in.PreferredBackupWindow, db.PreferredBackupWindow)
 		in.StorageEncrypted = aws.LateInitializeBoolPtr(in.StorageEncrypted, db.StorageEncrypted)
 		in.StorageType = aws.LateInitializeStringPtr(in.StorageType, db.StorageType)
 	}
 	in.AutoMinorVersionUpgrade = aws.LateInitializeBoolPtr(in.AutoMinorVersionUpgrade, db.AutoMinorVersionUpgrade)
 	in.AvailabilityZone = aws.LateInitializeStringPtr(in.AvailabilityZone, db.AvailabilityZone)
-	in.BackupRetentionPeriod = aws.LateInitializeInt64Ptr(in.BackupRetentionPeriod, db.BackupRetentionPeriod)
 	in.CharacterSetName = aws.LateInitializeStringPtr(in.CharacterSetName, db.CharacterSetName)
-	in.CopyTagsToSnapshot = aws.LateInitializeBoolPtr(in.CopyTagsToSnapshot, db.CopyTagsToSnapshot)
 	in.DBName = aws.LateInitializeStringPtr(in.DBName, db.DBName)
 	in.DeletionProtection = aws.LateInitializeBoolPtr(in.DeletionProtection, db.DeletionProtection)
-	in.EnableIAMDatabaseAuthentication = aws.LateInitializeBoolPtr(in.EnableIAMDatabaseAuthentication, db.IAMDatabaseAuthenticationEnabled)
 	in.EnablePerformanceInsights = aws.LateInitializeBoolPtr(in.EnablePerformanceInsights, db.PerformanceInsightsEnabled)
 	in.IOPS = aws.LateInitializeInt64Ptr(in.IOPS, db.Iops)
 	in.KMSKeyID = aws.LateInitializeStringPtr(in.KMSKeyID, db.KmsKeyId)
@@ -231,7 +232,6 @@ func lateInitialize(in *svcapitypes.DBInstanceParameters, out *svcsdk.DescribeDB
 	in.MultiAZ = aws.LateInitializeBoolPtr(in.MultiAZ, db.MultiAZ)
 	in.PerformanceInsightsKMSKeyID = aws.LateInitializeStringPtr(in.PerformanceInsightsKMSKeyID, db.PerformanceInsightsKMSKeyId)
 	in.PerformanceInsightsRetentionPeriod = aws.LateInitializeInt64Ptr(in.PerformanceInsightsRetentionPeriod, db.PerformanceInsightsRetentionPeriod)
-	in.PreferredBackupWindow = aws.LateInitializeStringPtr(in.PreferredBackupWindow, db.PreferredBackupWindow)
 	in.PreferredMaintenanceWindow = aws.LateInitializeStringPtr(in.PreferredMaintenanceWindow, db.PreferredMaintenanceWindow)
 	in.PromotionTier = aws.LateInitializeInt64Ptr(in.PromotionTier, db.PromotionTier)
 	in.PubliclyAccessible = aws.LateInitializeBoolPtr(in.PubliclyAccessible, db.PubliclyAccessible)


Test patch:
diff --git a/pkg/clients/ec2/routetable_test.go b/pkg/clients/ec2/routetable_test.go
index 9474baf1d..5f26b49d2 100644
--- a/pkg/clients/ec2/routetable_test.go
+++ b/pkg/clients/ec2/routetable_test.go
@@ -1,6 +1,7 @@
 package ec2
 
 import (
+	"reflect"
 	"testing"
 
 	"github.com/aws/aws-sdk-go-v2/aws"
@@ -170,3 +171,211 @@ func TestCreateRTPatch(t *testing.T) {
 		})
 	}
 }
+
+func TestSortRoutes(t *testing.T) {
+	type args struct {
+		route    []v1beta1.RouteBeta
+		ec2Route []ec2types.Route
+	}
+
+	type want struct {
+		route    []v1beta1.RouteBeta
+		ec2Route []ec2types.Route
+	}
+
+	var (
+		v4RouteMin = aws.String("v4_0")
+		v4RouteMax = aws.String("v4_1")
+		v6RouteMin = aws.String("v6_0")
+		v6RouteMax = aws.String("v6_1")
+	)
+
+	cases := map[string]struct {
+		args
+		want
+	}{
+		"v4": {
+			args: args{
+				route: []v1beta1.RouteBeta{
+					{
+						DestinationCIDRBlock: v4RouteMax,
+					},
+					{
+						DestinationCIDRBlock: v4RouteMin,
+					},
+				},
+				ec2Route: []ec2types.Route{
+					{
+						DestinationCidrBlock: v4RouteMax,
+					},
+					{
+						DestinationCidrBlock: v4RouteMin,
+					},
+				},
+			},
+			want: want{
+				route: []v1beta1.RouteBeta{
+					{
+						DestinationCIDRBlock: v4RouteMin,
+					},
+					{
+						DestinationCIDRBlock: v4RouteMax,
+					},
+				},
+				ec2Route: []ec2types.Route{
+					{
+						DestinationCidrBlock: v4RouteMin,
+					},
+					{
+						DestinationCidrBlock: v4RouteMax,
+					},
+				},
+			},
+		},
+		"v6": {
+			args: args{
+				route: []v1beta1.RouteBeta{
+					{
+						DestinationIPV6CIDRBlock: v6RouteMax,
+					},
+					{
+						DestinationIPV6CIDRBlock: v6RouteMin,
+					},
+				},
+				ec2Route: []ec2types.Route{
+					{
+						DestinationCidrBlock: v6RouteMax,
+					},
+					{
+						DestinationCidrBlock: v6RouteMin,
+					},
+				},
+			},
+			want: want{
+				route: []v1beta1.RouteBeta{
+					{
+						DestinationIPV6CIDRBlock: v6RouteMin,
+					},
+					{
+						DestinationIPV6CIDRBlock: v6RouteMax,
+					},
+				},
+				ec2Route: []ec2types.Route{
+					{
+						DestinationCidrBlock: v6RouteMin,
+					},
+					{
+						DestinationCidrBlock: v6RouteMax,
+					},
+				},
+			},
+		},
+		"both": {
+			args: args{
+				route: []v1beta1.RouteBeta{
+					{
+						DestinationCIDRBlock: v4RouteMax,
+					},
+					{
+						DestinationIPV6CIDRBlock: v6RouteMax,
+					},
+					{
+						DestinationCIDRBlock: v4RouteMin,
+					},
+					{
+						DestinationIPV6CIDRBlock: v6RouteMin,
+					},
+				},
+				ec2Route: []ec2types.Route{
+					{
+						DestinationCidrBlock: v4RouteMax,
+					},
+					{
+						DestinationIpv6CidrBlock: v6RouteMax,
+					},
+					{
+						DestinationCidrBlock: v4RouteMin,
+					},
+					{
+						DestinationIpv6CidrBlock: v6RouteMin,
+					},
+				},
+			},
+			want: want{
+				route: []v1beta1.RouteBeta{
+					{
+						DestinationCIDRBlock: v4RouteMin,
+					},
+					{
+						DestinationCIDRBlock: v4RouteMax,
+					},
+					{
+						DestinationIPV6CIDRBlock: v6RouteMin,
+					},
+					{
+						DestinationIPV6CIDRBlock: v6RouteMax,
+					},
+				},
+				ec2Route: []ec2types.Route{
+					{
+						DestinationCidrBlock: v4RouteMin,
+					},
+					{
+						DestinationCidrBlock: v4RouteMax,
+					},
+					{
+						DestinationIpv6CidrBlock: v6RouteMin,
+					},
+					{
+						DestinationIpv6CidrBlock: v6RouteMax,
+					},
+				},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			SortRoutes(tc.args.route, tc.args.ec2Route)
+			if !reflect.DeepEqual(tc.args.route, tc.want.route) {
+				t.Errorf("SortRoutes() route got = %v, want %v", tc.args.route, tc.want.route)
+			}
+			if !reflect.DeepEqual(tc.args.ec2Route, tc.want.ec2Route) {
+				t.Errorf("SortRoutes() ec2 route got = %v, want %v", tc.args.ec2Route, tc.want.ec2Route)
+			}
+		})
+	}
+}
+
+func TestValidateRoutes(t *testing.T) {
+	type args struct {
+		route []v1beta1.RouteBeta
+	}
+	tests := map[string]struct {
+		args args
+		err  string
+	}{
+		"valid": {
+			args: args{
+				route: []v1beta1.RouteBeta{{DestinationCIDRBlock: aws.String("0.0.0.0/0")}},
+			},
+		},
+		"empty cidrs": {
+			args: args{
+				route: []v1beta1.RouteBeta{{}},
+			},
+			err: "invalid routes: route[0]: both v4 and v6 cidrs are empty",
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			err := ValidateRoutes(tt.args.route)
+			if err != nil && err.Error() != tt.err {
+				t.Errorf("ValidateRoutes() error = %s, wantErr %s", err, err)
+			}
+			if err == nil && tt.err != "" {
+				t.Errorf("ValidateRoutes() error = %s, wantErr %s", err, err)
+			}
+		})
+	}
+}
diff --git a/pkg/clients/elasticache/elasticache_test.go b/pkg/clients/elasticache/elasticache_test.go
index 29eef5b37..394e00e8f 100644
--- a/pkg/clients/elasticache/elasticache_test.go
+++ b/pkg/clients/elasticache/elasticache_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package elasticache
 
 import (
+	"sort"
 	"strconv"
 	"testing"
 
@@ -53,6 +54,7 @@ var (
 	cacheSubnetGroupName      = "coolSubnet"
 	engine                    = "redis"
 	engineVersion             = "5.0.0"
+	multiAZ                   = true
 	notificationTopicARN      = "arn:aws:sns:cooltopic"
 	notificationTopicStatus   = "active"
 	numCacheClusters          = 2
@@ -103,6 +105,7 @@ var (
 				CacheSubnetGroupName:          &cacheSubnetGroupName,
 				Engine:                        engine,
 				EngineVersion:                 &engineVersion,
+				MultiAZEnabled:                &multiAZ,
 				NodeGroupConfiguration: []v1beta1.NodeGroupConfigurationSpec{
 					{
 						PrimaryAvailabilityZone:  &nodeGroupPrimaryAZ,
@@ -168,6 +171,7 @@ func TestNewCreateReplicationGroupInput(t *testing.T) {
 				CacheSecurityGroupNames:     cacheSecurityGroupNames,
 				CacheSubnetGroupName:        aws.String(cacheSubnetGroupName),
 				EngineVersion:               aws.String(engineVersion),
+				MultiAZEnabled:              aws.Bool(multiAZ),
 				NodeGroupConfiguration: []elasticachetypes.NodeGroupConfiguration{
 					{
 						PrimaryAvailabilityZone:  aws.String(nodeGroupPrimaryAZ),
@@ -242,6 +246,7 @@ func TestNewModifyReplicationGroupInput(t *testing.T) {
 				CacheParameterGroupName:     aws.String(cacheParameterGroupName),
 				CacheSecurityGroupNames:     cacheSecurityGroupNames,
 				EngineVersion:               aws.String(engineVersion),
+				MultiAZEnabled:              aws.Bool(true),
 				NotificationTopicArn:        aws.String(notificationTopicARN),
 				NotificationTopicStatus:     aws.String(notificationTopicStatus),
 				PreferredMaintenanceWindow:  aws.String(maintenanceWindow),
@@ -714,6 +719,7 @@ func TestReplicationGroupNeedsUpdate(t *testing.T) {
 			rg: elasticachetypes.ReplicationGroup{
 				AutomaticFailover:      elasticachetypes.AutomaticFailoverStatusEnabling,
 				CacheNodeType:          aws.String(cacheNodeType),
+				MultiAZ:                elasticachetypes.MultiAZStatusEnabled,
 				SnapshotRetentionLimit: aws.Int32Address(&snapshotRetentionLimit),
 				SnapshotWindow:         aws.String(snapshotWindow),
 			},
@@ -741,6 +747,40 @@ func TestReplicationGroupNeedsUpdate(t *testing.T) {
 			},
 			want: false,
 		},
+		{
+			name: "NeedsMultiAZUpdate",
+			kube: replicationGroup.Spec.ForProvider,
+			rg: elasticachetypes.ReplicationGroup{
+				AutomaticFailover:      elasticachetypes.AutomaticFailoverStatusEnabling,
+				CacheNodeType:          aws.String(cacheNodeType),
+				MultiAZ:                elasticachetypes.MultiAZStatusDisabled, // trigger Update
+				SnapshotRetentionLimit: aws.Int32Address(&snapshotRetentionLimit),
+				SnapshotWindow:         aws.String(snapshotWindow),
+			},
+			ccList: []elasticachetypes.CacheCluster{
+				{
+					EngineVersion:              aws.String(engineVersion),
+					CacheParameterGroup:        &elasticachetypes.CacheParameterGroupStatus{CacheParameterGroupName: aws.String(cacheParameterGroupName)},
+					NotificationConfiguration:  &elasticachetypes.NotificationConfiguration{TopicArn: aws.String(notificationTopicARN), TopicStatus: aws.String(notificationTopicStatus)},
+					PreferredMaintenanceWindow: aws.String(maintenanceWindow),
+					SecurityGroups: func() []elasticachetypes.SecurityGroupMembership {
+						ids := make([]elasticachetypes.SecurityGroupMembership, len(securityGroupIDs))
+						for i, id := range securityGroupIDs {
+							ids[i] = elasticachetypes.SecurityGroupMembership{SecurityGroupId: aws.String(id)}
+						}
+						return ids
+					}(),
+					CacheSecurityGroups: func() []elasticachetypes.CacheSecurityGroupMembership {
+						names := make([]elasticachetypes.CacheSecurityGroupMembership, len(cacheSecurityGroupNames))
+						for i, n := range cacheSecurityGroupNames {
+							names[i] = elasticachetypes.CacheSecurityGroupMembership{CacheSecurityGroupName: aws.String(n)}
+						}
+						return names
+					}(),
+				},
+			},
+			want: true,
+		},
 	}
 
 	for _, tc := range cases {
@@ -1138,3 +1178,252 @@ func TestVersionMatches(t *testing.T) {
 		})
 	}
 }
+
+func TestMultiAZEnabled(t *testing.T) {
+	f := false
+	tr := true
+	cases := []struct {
+		name string
+		maz  elasticachetypes.MultiAZStatus
+		want *bool
+	}{
+		{
+			name: "empty status",
+			maz:  elasticachetypes.MultiAZStatus(""),
+			want: nil,
+		},
+		{
+			name: "enabled",
+			maz:  elasticachetypes.MultiAZStatusEnabled,
+			want: &tr,
+		},
+		{
+			name: "disabled",
+			maz:  elasticachetypes.MultiAZStatusDisabled,
+			want: &f,
+		},
+		{
+			name: "unknown status",
+			maz:  elasticachetypes.MultiAZStatus("unknown"),
+			want: nil,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := multiAZEnabled(tc.maz)
+			if tc.want == nil && got != nil {
+				t.Errorf("MultiAZEnabled(%+v) - got %v", tc.want, got)
+			}
+			if aws.BoolValue(got) != aws.BoolValue(tc.want) {
+				t.Errorf("MultiAZEnabled(%+v) - got %v", tc.want, got)
+			}
+		})
+	}
+}
+
+func TestDiffTags(t *testing.T) {
+	type args struct {
+		local  []v1beta1.Tag
+		remote []elasticachetypes.Tag
+	}
+	type want struct {
+		add    map[string]string
+		remove []string
+	}
+	cases := map[string]struct {
+		args args
+		want want
+	}{
+		"AllNew": {
+			args: args{
+				local: []v1beta1.Tag{
+					{Key: "key", Value: "val"},
+				},
+			},
+			want: want{
+				add:    map[string]string{"key": "val"},
+				remove: []string{},
+			},
+		},
+		"SomeNew": {
+			args: args{
+				local: []v1beta1.Tag{
+					{Key: "key", Value: "val"},
+					{Key: "key1", Value: "val1"},
+					{Key: "key2", Value: "val2"},
+				},
+				remote: []elasticachetypes.Tag{
+					{Key: aws.String("key"), Value: aws.String("val")},
+				},
+			},
+			want: want{
+				add: map[string]string{
+					"key1": "val1",
+					"key2": "val2",
+				},
+				remove: []string{},
+			},
+		},
+		"Update": {
+			args: args{
+				local: []v1beta1.Tag{
+					{Key: "key", Value: "different"},
+					{Key: "key1", Value: "val1"},
+					{Key: "key2", Value: "val2"},
+				},
+				remote: []elasticachetypes.Tag{
+					{Key: aws.String("key"), Value: aws.String("val")},
+					{Key: aws.String("key1"), Value: aws.String("val1")},
+					{Key: aws.String("key2"), Value: aws.String("val2")},
+				},
+			},
+			want: want{
+				add:    map[string]string{"key": "different"},
+				remove: []string{"key"},
+			},
+		},
+		"RemoveAll": {
+			args: args{
+				remote: []elasticachetypes.Tag{
+					{Key: aws.String("key"), Value: aws.String("val")},
+					{Key: aws.String("key1"), Value: aws.String("val1")},
+					{Key: aws.String("key2"), Value: aws.String("val2")},
+				},
+			},
+			want: want{
+				add:    map[string]string{},
+				remove: []string{"key", "key1", "key2"},
+			},
+		},
+	}
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			tagCmp := cmpopts.SortSlices(func(i, j elasticachetypes.Tag) bool {
+				return aws.StringValue(i.Key) < aws.StringValue(j.Key)
+			})
+			add, remove := DiffTags(tc.args.local, tc.args.remote)
+			if diff := cmp.Diff(tc.want.add, add, tagCmp, cmpopts.IgnoreTypes(document.NoSerde{})); diff != "" {
+				t.Errorf("add: -want, +got:\n%s", diff)
+			}
+			sort.Strings(tc.want.remove)
+			sort.Strings(remove)
+			if diff := cmp.Diff(tc.want.remove, remove, tagCmp); diff != "" {
+				t.Errorf("remove: -want, +got:\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestReplicationGroupTagsNeedsUpdate(t *testing.T) {
+	type args struct {
+		local  []v1beta1.Tag
+		remote []elasticachetypes.Tag
+	}
+	type want struct {
+		res bool
+	}
+	cases := map[string]struct {
+		args args
+		want want
+	}{
+		"AllNewRemoteNil": {
+			args: args{
+				local: []v1beta1.Tag{
+					{Key: "key", Value: "val"},
+				},
+			},
+			want: want{
+				res: true,
+			},
+		},
+		"AllNew": {
+			args: args{
+				local: []v1beta1.Tag{
+					{Key: "key", Value: "val"},
+				},
+				remote: []elasticachetypes.Tag{},
+			},
+			want: want{
+				res: true,
+			},
+		},
+		"SomeNew": {
+			args: args{
+				local: []v1beta1.Tag{
+					{Key: "key", Value: "val"},
+					{Key: "key1", Value: "val1"},
+					{Key: "key2", Value: "val2"},
+				},
+				remote: []elasticachetypes.Tag{
+					{Key: aws.String("key"), Value: aws.String("val")},
+				},
+			},
+			want: want{
+				res: true,
+			},
+		},
+		"Update": {
+			args: args{
+				local: []v1beta1.Tag{
+					{Key: "key", Value: "different"},
+					{Key: "key1", Value: "val1"},
+					{Key: "key2", Value: "val2"},
+				},
+				remote: []elasticachetypes.Tag{
+					{Key: aws.String("key"), Value: aws.String("val")},
+					{Key: aws.String("key1"), Value: aws.String("val1")},
+					{Key: aws.String("key2"), Value: aws.String("val2")},
+				},
+			},
+			want: want{
+				res: true,
+			},
+		},
+		"Equal": {
+			args: args{
+				local: []v1beta1.Tag{
+					{Key: "key1", Value: "val1"},
+					{Key: "key2", Value: "val2"},
+				},
+				remote: []elasticachetypes.Tag{
+					{Key: aws.String("key1"), Value: aws.String("val1")},
+					{Key: aws.String("key2"), Value: aws.String("val2")},
+				},
+			},
+			want: want{
+				res: false,
+			},
+		},
+		"EqualEmpty": {
+			args: args{
+				local:  []v1beta1.Tag{},
+				remote: []elasticachetypes.Tag{},
+			},
+			want: want{
+				res: false,
+			},
+		},
+		"RemoveAll": {
+			args: args{
+				remote: []elasticachetypes.Tag{
+					{Key: aws.String("key"), Value: aws.String("val")},
+					{Key: aws.String("key1"), Value: aws.String("val1")},
+					{Key: aws.String("key2"), Value: aws.String("val2")},
+				},
+			},
+			want: want{
+				res: true,
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			res := ReplicationGroupTagsNeedsUpdate(tc.args.local, tc.args.remote)
+			if diff := cmp.Diff(tc.want.res, res); diff != "" {
+				t.Errorf("r: -want, +got:\n%s", diff)
+			}
+		})
+	}
+}
diff --git a/pkg/controller/cache/managed_test.go b/pkg/controller/cache/managed_test.go
index 66e9f0e0f..a9b6dea9b 100644
--- a/pkg/controller/cache/managed_test.go
+++ b/pkg/controller/cache/managed_test.go
@@ -265,6 +265,14 @@ func TestObserve(t *testing.T) {
 						}},
 					}, nil
 				},
+				MockListTagsForResource: func(ctx context.Context, _ *elasticache.ListTagsForResourceInput, opts []func(*elasticache.Options)) (*elasticache.ListTagsForResourceOutput, error) {
+					return &elasticache.ListTagsForResourceOutput{
+						TagList: []types.Tag{
+							{Key: aws.String("key1"), Value: aws.String("val1")},
+							{Key: aws.String("key2"), Value: aws.String("val2")},
+						},
+					}, nil
+				},
 			}},
 			r: replicationGroup(
 				withReplicationGroupID(name),
diff --git a/pkg/controller/ec2/routetable/controller_test.go b/pkg/controller/ec2/routetable/controller_test.go
index 2d39f0027..277317d3c 100644
--- a/pkg/controller/ec2/routetable/controller_test.go
+++ b/pkg/controller/ec2/routetable/controller_test.go
@@ -39,18 +39,19 @@ import (
 )
 
 var (
-	rtID           = "some rt"
-	vpcID          = "some vpc"
-	igID           = "some ig"
-	instanceID     = "natID"
-	associationID  = "someAssociation"
-	subnetID       = "some subnet"
-	removeSubnetID = "removeMe"
-	testKey        = "testKey"
-	testValue      = "testValue"
-	CIDR           = "10.0.0.0/8"
-	instanceCIDR   = "10.1.1.1/32"
-	errBoom        = errors.New("boom")
+	rtID            = "some rt"
+	vpcID           = "some vpc"
+	igID            = "some ig"
+	destinationCIDR = "0.0.0.0/0"
+	instanceID      = "natID"
+	associationID   = "someAssociation"
+	subnetID        = "some subnet"
+	removeSubnetID  = "removeMe"
+	testKey         = "testKey"
+	testValue       = "testValue"
+	CIDR            = "10.0.0.0/8"
+	instanceCIDR    = "10.1.1.1/32"
+	errBoom         = errors.New("boom")
 )
 
 type args struct {
@@ -282,7 +283,8 @@ func TestUpdate(t *testing.T) {
 				},
 				cr: rt(withSpec(v1beta1.RouteTableParameters{
 					Routes: []v1beta1.RouteBeta{{
-						GatewayID: aws.String(igID),
+						GatewayID:            aws.String(igID),
+						DestinationCIDRBlock: aws.String(destinationCIDR),
 					}},
 					Associations: []v1beta1.Association{{
 						SubnetID: aws.String(subnetID),
@@ -295,7 +297,8 @@ func TestUpdate(t *testing.T) {
 			want: want{
 				cr: rt(withSpec(v1beta1.RouteTableParameters{
 					Routes: []v1beta1.RouteBeta{{
-						GatewayID: aws.String(igID),
+						GatewayID:            aws.String(igID),
+						DestinationCIDRBlock: aws.String(destinationCIDR),
 					}},
 					Associations: []v1beta1.Association{{
 						SubnetID: aws.String(subnetID),
@@ -685,7 +688,8 @@ func TestUpdate(t *testing.T) {
 				},
 				cr: rt(withSpec(v1beta1.RouteTableParameters{
 					Routes: []v1beta1.RouteBeta{{
-						GatewayID: aws.String(igID),
+						GatewayID:            aws.String(igID),
+						DestinationCIDRBlock: aws.String(destinationCIDR),
 					}},
 				}),
 					withStatus(v1beta1.RouteTableObservation{
@@ -695,7 +699,8 @@ func TestUpdate(t *testing.T) {
 			want: want{
 				cr: rt(withSpec(v1beta1.RouteTableParameters{
 					Routes: []v1beta1.RouteBeta{{
-						GatewayID: aws.String(igID),
+						GatewayID:            aws.String(igID),
+						DestinationCIDRBlock: aws.String(destinationCIDR),
 					}},
 				}),
 					withStatus(v1beta1.RouteTableObservation{
diff --git a/pkg/controller/ec2/securitygroup/controller_test.go b/pkg/controller/ec2/securitygroup/controller_test.go
index 03347f728..dd5b813e0 100644
--- a/pkg/controller/ec2/securitygroup/controller_test.go
+++ b/pkg/controller/ec2/securitygroup/controller_test.go
@@ -150,6 +150,73 @@ func TestObserve(t *testing.T) {
 				},
 			},
 		},
+		"EmptyExternalNameExistingSG": {
+			args: args{
+				kube: &test.MockClient{
+					MockUpdate: test.NewMockUpdateFn(nil),
+				},
+				sg: &fake.MockSecurityGroupClient{
+					MockDescribe: func(ctx context.Context, input *awsec2.DescribeSecurityGroupsInput, opts []func(*awsec2.Options)) (*awsec2.DescribeSecurityGroupsOutput, error) {
+						return &awsec2.DescribeSecurityGroupsOutput{
+							SecurityGroups: []awsec2types.SecurityGroup{{GroupId: aws.String(sgID)}},
+						}, nil
+					},
+				},
+				cr: sg(withExternalName("")),
+			},
+			want: want{
+				cr: sg(withStatus(v1beta1.SecurityGroupObservation{
+					SecurityGroupID: sgID,
+				}),
+					withExternalName(sgID),
+					withConditions(xpv1.Available())),
+				result: managed.ExternalObservation{
+					ResourceExists:   true,
+					ResourceUpToDate: true,
+				},
+			},
+		},
+		"EmptyExternalNameNonExistingSG": {
+			args: args{
+				kube: &test.MockClient{
+					MockUpdate: test.NewMockUpdateFn(nil),
+				},
+				sg: &fake.MockSecurityGroupClient{
+					MockDescribe: func(ctx context.Context, input *awsec2.DescribeSecurityGroupsInput, opts []func(*awsec2.Options)) (*awsec2.DescribeSecurityGroupsOutput, error) {
+						return &awsec2.DescribeSecurityGroupsOutput{
+							SecurityGroups: []awsec2types.SecurityGroup{},
+						}, nil
+					},
+				},
+				cr: sg(),
+			},
+			want: want{
+				cr: sg(),
+				result: managed.ExternalObservation{
+					ResourceExists: false,
+				},
+			},
+		},
+		"EmptyExternalNameClientError": {
+			args: args{
+				kube: &test.MockClient{
+					MockUpdate: test.NewMockUpdateFn(nil),
+				},
+				sg: &fake.MockSecurityGroupClient{
+					MockDescribe: func(ctx context.Context, input *awsec2.DescribeSecurityGroupsInput, opts []func(*awsec2.Options)) (*awsec2.DescribeSecurityGroupsOutput, error) {
+						return nil, errBoom
+					},
+				},
+				cr: sg(),
+			},
+			want: want{
+				cr: sg(),
+				result: managed.ExternalObservation{
+					ResourceExists: false,
+				},
+				err: awsclient.Wrap(errBoom, errGetSecurityGroup),
+			},
+		},
 		"MultipleSGs": {
 			args: args{
 				sg: &fake.MockSecurityGroupClient{