Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Introduce edge subnets to support AWS Local Zones #4882

Merged
merged 3 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions api/v1beta1/awscluster_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,19 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error {
dst.Spec.NetworkSpec.VPC.EmptyRoutesDefaultVPCSecurityGroup = restored.Spec.NetworkSpec.VPC.EmptyRoutesDefaultVPCSecurityGroup
dst.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch = restored.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch

// Restore SubnetSpec.ResourceID field, if any.
// Restore SubnetSpec.ResourceID, SubnetSpec.ParentZoneName, and SubnetSpec.ZoneType fields, if any.
for _, subnet := range restored.Spec.NetworkSpec.Subnets {
if len(subnet.ResourceID) == 0 {
continue
}
for i, dstSubnet := range dst.Spec.NetworkSpec.Subnets {
if dstSubnet.ID == subnet.ID {
dstSubnet.ResourceID = subnet.ResourceID
if len(subnet.ResourceID) > 0 {
dstSubnet.ResourceID = subnet.ResourceID
}
if subnet.ParentZoneName != nil {
dstSubnet.ParentZoneName = subnet.ParentZoneName
}
if subnet.ZoneType != nil {
dstSubnet.ZoneType = subnet.ZoneType
}
dstSubnet.DeepCopyInto(&dst.Spec.NetworkSpec.Subnets[i])
}
}
Expand Down
2 changes: 2 additions & 0 deletions api/v1beta1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/v1beta2/awscluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ func (r *AWSCluster) validateNetwork() field.ErrorList {
if subnet.IsIPv6 || subnet.IPv6CidrBlock != "" {
allErrs = append(allErrs, field.Invalid(field.NewPath("subnets"), r.Spec.NetworkSpec.Subnets, "IPv6 cannot be used with unmanaged clusters at this time."))
}
if subnet.ZoneType != nil && subnet.IsEdge() {
if subnet.ParentZoneName == nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("subnets"), r.Spec.NetworkSpec.Subnets, "ParentZoneName must be set when ZoneType is 'local-zone'."))
}
}
Comment on lines +251 to +255
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for the reviewer:

// IsEdge returns the true when the subnet is created in the edge zone,
// Local Zones.

}

if r.Spec.NetworkSpec.VPC.CidrBlock != "" && r.Spec.NetworkSpec.VPC.IPAMPool != nil {
Expand Down
125 changes: 121 additions & 4 deletions api/v1beta2/network_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import (
"fmt"
"sort"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"k8s.io/utils/ptr"
)

const (
Expand All @@ -37,6 +41,11 @@ const (
DefaultAPIServerHealthThresholdCount = 5
// DefaultAPIServerUnhealthThresholdCount the API server unhealthy check threshold count.
DefaultAPIServerUnhealthThresholdCount = 3

// ZoneTypeAvailabilityZone defines the regular AWS zones in the Region.
ZoneTypeAvailabilityZone ZoneType = "availability-zone"
// ZoneTypeLocalZone defines the AWS zone type in Local Zone infrastructure.
ZoneTypeLocalZone ZoneType = "local-zone"
)

// NetworkStatus encapsulates AWS networking resources.
Expand Down Expand Up @@ -508,6 +517,39 @@ type SubnetSpec struct {

// Tags is a collection of tags describing the resource.
Tags Tags `json:"tags,omitempty"`

// ZoneType defines the type of the zone where the subnet is created.
//
// The valid values are availability-zone, and local-zone.
//
// Subnet with zone type availability-zone (regular) is always selected to create cluster
// resources, like Load Balancers, NAT Gateways, Contol Plane nodes, etc.
//
// Subnet with zone type local-zone is not eligible to automatically create
// regular cluster resources.
//
// The public subnet in availability-zone or local-zone is associated with regular public
// route table with default route entry to a Internet Gateway.
//
// The private subnet in the availability-zone is associated with a private route table with
// the default route entry to a NAT Gateway created in that zone.
//
// The private subnet in the local-zone is associated with a private route table with
// the default route entry re-using the NAT Gateway in the Region (preferred from the
// parent zone, the zone type availability-zone in the region, or first table available).
//
// +kubebuilder:validation:Enum=availability-zone;local-zone
// +optional
ZoneType *ZoneType `json:"zoneType,omitempty"`

// ParentZoneName is the zone name where the current subnet's zone is tied when
// the zone is a Local Zone.
//
// The subnets in Local Zone locations consume the ParentZoneName to determine the correct
// private route table to egress traffic to the internet.
//
// +optional
nrb marked this conversation as resolved.
Show resolved Hide resolved
ParentZoneName *string `json:"parentZoneName,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have CEL to enforce the cross field validation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or use validation in the webhook.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@richardcase I am thiking the best strategy here. I think we need to prevent the ParentZoneName when not local-zone for ZoneType, so something like this:

 // SubnetSpec configures an AWS Subnet.
+// +kubebuilder:validation:XValidation:rule="has(self.zoneType) && self.zoneType == 'local-zone' ? (has(self.parentZoneName) && size(self.parentZoneName) > 0)
 : !has(self.parentZoneName)",message="parentZoneName must be set if zoneType is 'local-zone'"
 type SubnetSpec struct {
        // ID defines a unique identifier to reference this resource.
....

Do you think it makes sense? is it enough?

Copy link
Contributor Author

@mtulio mtulio Apr 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or use validation in the webhook.

yeah, when thinking in the follow up PRs (Wavelength), it seems to be better to create a validation in the webhook. Let me do it instead of CEL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Webhook added

}

// GetResourceID returns the identifier for this subnet,
Expand All @@ -524,6 +566,39 @@ func (s *SubnetSpec) String() string {
return fmt.Sprintf("id=%s/az=%s/public=%v", s.GetResourceID(), s.AvailabilityZone, s.IsPublic)
}

// IsEdge returns the true when the subnet is created in the edge zone,
// Local Zones.
func (s *SubnetSpec) IsEdge() bool {
return s.ZoneType != nil && *s.ZoneType == ZoneTypeLocalZone
}

// SetZoneInfo updates the subnets with zone information.
func (s *SubnetSpec) SetZoneInfo(zones []*ec2.AvailabilityZone) error {
zoneInfo := func(zoneName string) *ec2.AvailabilityZone {
for _, zone := range zones {
if aws.StringValue(zone.ZoneName) == zoneName {
return zone
}
}
return nil
}

zone := zoneInfo(s.AvailabilityZone)
if zone == nil {
if len(s.AvailabilityZone) > 0 {
return fmt.Errorf("unable to update zone information for subnet '%v' and zone '%v'", s.ID, s.AvailabilityZone)
}
return fmt.Errorf("unable to update zone information for subnet '%v'", s.ID)
}
if zone.ZoneType != nil {
s.ZoneType = ptr.To(ZoneType(*zone.ZoneType))
Copy link
Contributor

@r4f4 r4f4 Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm just being paranoid, but I feel like it's safer to save the value to a local var before taking its address:

Suggested change
s.ZoneType = ptr.To(ZoneType(*zone.ZoneType))
zt := ZoneType(*zone.ZoneType)
s.ZoneType = ptr.To(zt)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are not, or I am being too! =]
Good question. I was thinking about that. But considering i am checking it's not null, I am unsure if that change would make safer. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't mean safer in the sense of the value being nil but more in the sense of saving the addr of a temporary variable. I guess that doesn't make sense since it's basically doing &(*zonetype) which is zonetype.

}
if zone.ParentZoneName != nil {
s.ParentZoneName = zone.ParentZoneName
}
return nil
}

// Subnets is a slice of Subnet.
// +listType=map
// +listMapKey=id
Expand All @@ -541,6 +616,22 @@ func (s Subnets) ToMap() map[string]*SubnetSpec {

// IDs returns a slice of the subnet ids.
func (s Subnets) IDs() []string {
res := []string{}
for _, subnet := range s {
// Prevent returning edge zones (Local Zone) to regular Subnet IDs.
// Edge zones should not deploy control plane nodes, and does not support Nat Gateway and
// Network Load Balancers. Any resource for the core infrastructure should not consume edge
// zones.
if subnet.IsEdge() {
continue
}
res = append(res, subnet.GetResourceID())
}
return res
}

// IDsWithEdge returns a slice of the subnet ids.
func (s Subnets) IDsWithEdge() []string {
res := []string{}
for _, subnet := range s {
res = append(res, subnet.GetResourceID())
Expand Down Expand Up @@ -581,21 +672,29 @@ func (s Subnets) FindEqual(spec *SubnetSpec) *SubnetSpec {
// FilterPrivate returns a slice containing all subnets marked as private.
func (s Subnets) FilterPrivate() (res Subnets) {
for _, x := range s {
// Subnets in AWS Local Zones or Wavelength should not be used by core infrastructure.
if x.IsEdge() {
continue
}
if !x.IsPublic {
res = append(res, x)
}
}
return
return res
}

// FilterPublic returns a slice containing all subnets marked as public.
func (s Subnets) FilterPublic() (res Subnets) {
for _, x := range s {
// Subnets in AWS Local Zones or Wavelength should not be used by core infrastructure.
if x.IsEdge() {
continue
}
if x.IsPublic {
res = append(res, x)
}
}
return
return res
}

// FilterByZone returns a slice containing all subnets that live in the availability zone specified.
Expand All @@ -605,22 +704,32 @@ func (s Subnets) FilterByZone(zone string) (res Subnets) {
res = append(res, x)
}
}
return
return res
}

// GetUniqueZones returns a slice containing the unique zones of the subnets.
func (s Subnets) GetUniqueZones() []string {
keys := make(map[string]bool)
zones := []string{}
for _, x := range s {
if _, value := keys[x.AvailabilityZone]; !value {
if _, value := keys[x.AvailabilityZone]; len(x.AvailabilityZone) > 0 && !value {
keys[x.AvailabilityZone] = true
zones = append(zones, x.AvailabilityZone)
}
}
return zones
}

// SetZoneInfo updates the subnets with zone information.
func (s Subnets) SetZoneInfo(zones []*ec2.AvailabilityZone) error {
for i := range s {
if err := s[i].SetZoneInfo(zones); err != nil {
return err
}
}
return nil
}

// CNISpec defines configuration for CNI.
type CNISpec struct {
// CNIIngressRules specify rules to apply to control plane and worker node security groups.
Expand Down Expand Up @@ -835,3 +944,11 @@ func (i *IngressRule) Equals(o *IngressRule) bool {

return true
}

// ZoneType defines listener AWS Availability Zone type.
type ZoneType string

// String returns the string representation for the zone type.
func (z ZoneType) String() string {
return string(z)
}
Loading
Loading