Skip to content

Commit b604e54

Browse files
committed
feat: add support for global accelerator endpoint groups
Signed-off-by: Tobias Kässer <[email protected]>
1 parent 12124ad commit b604e54

File tree

11 files changed

+439
-0
lines changed

11 files changed

+439
-0
lines changed

docs/examples/globalaccelerator.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
kind: Ingress
2+
metadata:
3+
name: echoserver
4+
annotations:
5+
kubernetes.io/ingress.class: alb
6+
alb.ingress.kubernetes.io/scheme: internet-facing
7+
alb.ingress.kubernetes.io/ga-epg-arn: arn:aws:globalaccelerator::12345678912:accelerator/d60128f1-4134-4e03-bed9-edd00f77b3e6/listener/a309af4a/endpoint-group/ed7bf648f700
8+
alb.ingress.kubernetes.io/ga-ep-create: "true"
9+
spec:
10+
rules:
11+
- http:
12+
paths:
13+
- backend:
14+
service:
15+
name: my-release-nginx
16+
port:
17+
number: 80
18+
path: /
19+
pathType: Prefix
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Create Endpoint on exisitng Endpointgroup
2+
3+
In order to create an endpoint for the ingress-group, the user
4+
needs to specify two annotations:
5+
6+
`alb.ingress.kubernetes.io/ga-epg-arn: arn:aws:globalaccelerator::12345678912:accelerator/d60128f1-4134-4e03-bed9-edd00f77b3e6/listener/a309af4a/endpoint-group/ed7bf648f700`
7+
`alb.ingress.kubernetes.io/ga-ep-create: "true"`
8+
9+
This second annotation exists because of the fact that endpoints don't support tags
10+
and with the current stateless logic it is not possible to identify the correct
11+
endpoint for deletion. This means that:
12+
*Deletion is only supported by setting `ga-ep-create: "false"`*

pkg/annotations/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const (
1919
IngressSuffixWAFACLID = "waf-acl-id"
2020
IngressSuffixWebACLID = "web-acl-id" // deprecated, use "waf-acl-id" instead.
2121
IngressSuffixShieldAdvancedProtection = "shield-advanced-protection"
22+
IngressSuffixGAEndpointGroup = "ga-epg-arn"
23+
IngressSuffixGAEndpointCreate = "ga-ep-create"
2224
IngressSuffixSecurityGroups = "security-groups"
2325
IngressSuffixListenPorts = "listen-ports"
2426
IngressSuffixSSLRedirect = "ssl-redirect"

pkg/aws/cloud.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ type Cloud interface {
4141
// RGT provides API to AWS RGT
4242
RGT() services.RGT
4343

44+
// GA provides API to AWS Global Accelerator
45+
GA() services.GlobalAccelerator
46+
4447
// Region for the kubernetes cluster
4548
Region() string
4649

@@ -125,6 +128,7 @@ func NewCloud(cfg CloudConfig, metricsRegisterer prometheus.Registerer) (Cloud,
125128
ec2: ec2Service,
126129
elbv2: services.NewELBV2(sess),
127130
acm: services.NewACM(sess),
131+
ga: services.NewGlobalAccelerator(sess),
128132
wafv2: services.NewWAFv2(sess),
129133
wafRegional: services.NewWAFRegional(sess, cfg.Region),
130134
shield: services.NewShield(sess),
@@ -177,6 +181,7 @@ type defaultCloud struct {
177181
elbv2 services.ELBV2
178182

179183
acm services.ACM
184+
ga services.GlobalAccelerator
180185
wafv2 services.WAFv2
181186
wafRegional services.WAFRegional
182187
shield services.Shield
@@ -211,6 +216,10 @@ func (c *defaultCloud) RGT() services.RGT {
211216
return c.rgt
212217
}
213218

219+
func (c *defaultCloud) GA() services.GlobalAccelerator {
220+
return c.ga
221+
}
222+
214223
func (c *defaultCloud) Region() string {
215224
return c.cfg.Region
216225
}

pkg/aws/services/globalaccelerator.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package services
2+
3+
import (
4+
"github.com/aws/aws-sdk-go/aws"
5+
"github.com/aws/aws-sdk-go/aws/session"
6+
"github.com/aws/aws-sdk-go/service/globalaccelerator"
7+
"github.com/aws/aws-sdk-go/service/globalaccelerator/globalacceleratoriface"
8+
)
9+
10+
type GlobalAccelerator interface {
11+
globalacceleratoriface.GlobalAcceleratorAPI
12+
}
13+
14+
// NewGlobalAccelerator constructs new GlobalAccelerator implementation.
15+
func NewGlobalAccelerator(session *session.Session) GlobalAccelerator {
16+
return &defaultGlobalAcceleratorAPI{
17+
// global accelerator always needs `us-west-2`-region
18+
GlobalAcceleratorAPI: globalaccelerator.New(session, aws.NewConfig().WithRegion("us-west-2")),
19+
}
20+
}
21+
22+
// default implementation for Global Accelerator.
23+
type defaultGlobalAcceleratorAPI struct {
24+
globalacceleratoriface.GlobalAcceleratorAPI
25+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package globalaccelerator
2+
3+
import (
4+
"context"
5+
6+
awssdk "github.com/aws/aws-sdk-go/aws"
7+
gasdk "github.com/aws/aws-sdk-go/service/globalaccelerator"
8+
9+
"github.com/go-logr/logr"
10+
"sigs.k8s.io/aws-load-balancer-controller/pkg/aws/services"
11+
)
12+
13+
type EndpointManager interface {
14+
// AddEndpoint add an endpoint to a globalaccelerator endpoint group
15+
AddEndpoint(ctx context.Context, endpointGroupARN string, lbArn string) error
16+
17+
// GetEndpoint gets the existing endpoint for an endpointgroup and load balancer
18+
GetEndpoint(ctx context.Context, endpointGroupARN string, lbArn string) (string, error)
19+
20+
// DeleteEndpoints deletes an existing endpoint for an endpointgroup and load balancer
21+
DeleteEndpoint(ctx context.Context, endpointGroupARN string, lbArn string) error
22+
}
23+
24+
func NewDefaultEndpointManager(gaClient services.GlobalAccelerator, logger logr.Logger) *defaultEndpointManager {
25+
return &defaultEndpointManager{
26+
gaClient: gaClient,
27+
}
28+
}
29+
30+
var _ EndpointManager = &defaultEndpointManager{}
31+
32+
type defaultEndpointManager struct {
33+
gaClient services.GlobalAccelerator
34+
}
35+
36+
func (m *defaultEndpointManager) AddEndpoint(ctx context.Context, endpointGroupARN string, lbArn string) error {
37+
_, err := m.gaClient.AddEndpoints(&gasdk.AddEndpointsInput{
38+
EndpointGroupArn: &endpointGroupARN,
39+
EndpointConfigurations: []*gasdk.EndpointConfiguration{
40+
{
41+
EndpointId: awssdk.String(lbArn),
42+
},
43+
},
44+
})
45+
46+
if err != nil {
47+
return err
48+
}
49+
50+
return nil
51+
}
52+
53+
func (m *defaultEndpointManager) GetEndpoint(ctx context.Context, endpointGroupARN string, lbArn string) (string, error) {
54+
epResponse, err := m.gaClient.DescribeEndpointGroup(&gasdk.DescribeEndpointGroupInput{
55+
EndpointGroupArn: &endpointGroupARN,
56+
})
57+
58+
if err != nil {
59+
return "", err
60+
}
61+
62+
for _, endpoint := range epResponse.EndpointGroup.EndpointDescriptions {
63+
if *endpoint.EndpointId == lbArn {
64+
return lbArn, nil
65+
}
66+
}
67+
68+
return "", nil
69+
}
70+
71+
func (m *defaultEndpointManager) DeleteEndpoint(ctx context.Context, endpointGroupARN string, lbArn string) error {
72+
_, err := m.gaClient.RemoveEndpoints(&gasdk.RemoveEndpointsInput{
73+
EndpointGroupArn: &endpointGroupARN,
74+
EndpointIdentifiers: []*gasdk.EndpointIdentifier{
75+
{
76+
EndpointId: awssdk.String(lbArn),
77+
},
78+
},
79+
})
80+
if err != nil {
81+
return err
82+
}
83+
84+
return nil
85+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package globalaccelerator
2+
3+
import (
4+
"context"
5+
"github.com/go-logr/logr"
6+
"github.com/pkg/errors"
7+
"sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
8+
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
9+
gamodel "sigs.k8s.io/aws-load-balancer-controller/pkg/model/globalaccelerator"
10+
)
11+
12+
// NewEndpointSynthesizer constructs new endpointSynthesizer
13+
func NewEndpointSynthesizer(epManager EndpointManager, logger logr.Logger, stack core.Stack) *endpointSynthesizer {
14+
return &endpointSynthesizer{
15+
endpointManager: epManager,
16+
logger: logger,
17+
stack: stack,
18+
}
19+
}
20+
21+
type endpointSynthesizer struct {
22+
endpointManager EndpointManager
23+
logger logr.Logger
24+
stack core.Stack
25+
}
26+
27+
func (s *endpointSynthesizer) Synthesize(ctx context.Context) error {
28+
var resEndpoints []*gamodel.Endpoint
29+
s.stack.ListResources(&resEndpoints)
30+
resEndpointsByARN, err := mapResEndpointByResourceARN(resEndpoints)
31+
if err != nil {
32+
return err
33+
}
34+
35+
var resLBs []*elbv2model.LoadBalancer
36+
s.stack.ListResources(&resLBs)
37+
for _, resLB := range resLBs {
38+
// Global Accelerator can only be created for ALB for now.
39+
if resLB.Spec.Type != elbv2model.LoadBalancerTypeApplication {
40+
continue
41+
}
42+
lbARN, err := resLB.LoadBalancerARN().Resolve(ctx)
43+
if err != nil {
44+
return err
45+
}
46+
resEndpoints := resEndpointsByARN[lbARN]
47+
48+
if err := s.synthesizeGAEndpoints(ctx, lbARN, resEndpoints); err != nil {
49+
return err
50+
}
51+
}
52+
53+
return nil
54+
}
55+
56+
func (*endpointSynthesizer) PostSynthesize(ctx context.Context) error {
57+
// nothing to do here.
58+
return nil
59+
}
60+
61+
func (s *endpointSynthesizer) synthesizeGAEndpoints(ctx context.Context, lbARN string, resEndpoints []*gamodel.Endpoint) error {
62+
if len(resEndpoints) > 1 {
63+
return fmt.Errorf("[should never happen] multiple Global Accelerator Endpoints desired on LoadBalancer: %v", lbARN)
64+
}
65+
66+
var desiredEndpointGroupARN string
67+
var desiredEndpointCreate bool
68+
if len(resEndpoints) == 1 {
69+
desiredEndpointGroupARN = resEndpoints[0].Spec.EndpointGroupARN
70+
desiredEndpointCreate = resEndpoints[0].Spec.Create
71+
}
72+
73+
if desiredEndpointGroupARN != "" {
74+
// no lbARN means delete
75+
if !desiredEndpointCreate {
76+
s.endpointManager.DeleteEndpoint(ctx, desiredEndpointGroupARN, lbARN)
77+
return nil
78+
}
79+
80+
existingEndpoint, err := s.endpointManager.GetEndpoint(ctx, desiredEndpointGroupARN, lbARN)
81+
if err != nil {
82+
return err
83+
}
84+
if existingEndpoint == "" {
85+
s.endpointManager.AddEndpoint(ctx, desiredEndpointGroupARN, lbARN)
86+
}
87+
}
88+
89+
return nil
90+
}
91+
92+
func mapResEndpointByResourceARN(resEndpoints []*gamodel.Endpoint) (map[string][]*gamodel.Endpoint, error) {
93+
resEndpointsByARN := make(map[string][]*gamodel.Endpoint, len(resEndpoints))
94+
ctx := context.Background()
95+
for _, resEndpoint := range resEndpoints {
96+
resARN, err := resEndpoint.Spec.ResourceARN.Resolve(ctx)
97+
if err != nil {
98+
return nil, err
99+
}
100+
resEndpointsByARN[resARN] = append(resEndpointsByARN[resARN], resEndpoint)
101+
}
102+
return resEndpointsByARN, nil
103+
}

pkg/deploy/stack_deployer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"sigs.k8s.io/aws-load-balancer-controller/pkg/config"
88
"sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/ec2"
99
"sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/elbv2"
10+
"sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/globalaccelerator"
1011
"sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/shield"
1112
"sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/tracking"
1213
"sigs.k8s.io/aws-load-balancer-controller/pkg/deploy/wafregional"
@@ -47,6 +48,7 @@ func NewDefaultStackDeployer(cloud aws.Cloud, k8sClient client.Client,
4748
wafv2WebACLAssociationManager: wafv2.NewDefaultWebACLAssociationManager(cloud.WAFv2(), logger),
4849
wafRegionalWebACLAssociationManager: wafregional.NewDefaultWebACLAssociationManager(cloud.WAFRegional(), logger),
4950
shieldProtectionManager: shield.NewDefaultProtectionManager(cloud.Shield(), logger),
51+
gaEndpointManager: globalaccelerator.NewDefaultEndpointManager(cloud.GA(), logger),
5052
featureGates: config.FeatureGates,
5153
vpcID: cloud.VpcID(),
5254
logger: logger,
@@ -69,6 +71,7 @@ type defaultStackDeployer struct {
6971
elbv2LRManager elbv2.ListenerRuleManager
7072
elbv2TGManager elbv2.TargetGroupManager
7173
elbv2TGBManager elbv2.TargetGroupBindingManager
74+
gaEndpointManager globalaccelerator.EndpointManager
7275
wafv2WebACLAssociationManager wafv2.WebACLAssociationManager
7376
wafRegionalWebACLAssociationManager wafregional.WebACLAssociationManager
7477
shieldProtectionManager shield.ProtectionManager
@@ -92,6 +95,7 @@ func (d *defaultStackDeployer) Deploy(ctx context.Context, stack core.Stack) err
9295
elbv2.NewListenerSynthesizer(d.cloud.ELBV2(), d.elbv2TaggingManager, d.elbv2LSManager, d.logger, stack),
9396
elbv2.NewListenerRuleSynthesizer(d.cloud.ELBV2(), d.elbv2TaggingManager, d.elbv2LRManager, d.logger, stack),
9497
elbv2.NewTargetGroupBindingSynthesizer(d.k8sClient, d.trackingProvider, d.elbv2TGBManager, d.logger, stack),
98+
globalaccelerator.NewEndpointSynthesizer(d.gaEndpointManager, d.logger, stack),
9599
}
96100

97101
if d.addonsConfig.WAFV2Enabled {

pkg/ingress/model_build_load_balancer_addons.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"k8s.io/apimachinery/pkg/util/sets"
77
"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
88
"sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
9+
gamodel "sigs.k8s.io/aws-load-balancer-controller/pkg/model/globalaccelerator"
910
shieldmodel "sigs.k8s.io/aws-load-balancer-controller/pkg/model/shield"
1011
wafregionalmodel "sigs.k8s.io/aws-load-balancer-controller/pkg/model/wafregional"
1112
wafv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/wafv2"
1213
)
1314

1415
func (t *defaultModelBuildTask) buildLoadBalancerAddOns(ctx context.Context, lbARN core.StringToken) error {
16+
1517
if _, err := t.buildWAFv2WebACLAssociation(ctx, lbARN); err != nil {
1618
return err
1719
}
@@ -21,6 +23,10 @@ func (t *defaultModelBuildTask) buildLoadBalancerAddOns(ctx context.Context, lbA
2123
if _, err := t.buildShieldProtection(ctx, lbARN); err != nil {
2224
return err
2325
}
26+
if _, err := t.buildGAEndpoint(ctx, lbARN); err != nil {
27+
return err
28+
}
29+
2430
return nil
2531
}
2632

@@ -102,3 +108,42 @@ func (t *defaultModelBuildTask) buildShieldProtection(_ context.Context, lbARN c
102108
}
103109
return nil, nil
104110
}
111+
112+
func (t *defaultModelBuildTask) buildGAEndpoint(_ context.Context, lbARN core.StringToken) (*gamodel.Endpoint, error) {
113+
explicitEPGARNs := sets.NewString()
114+
epCreateByARN := make(map[string]string)
115+
rawEPGARN := ""
116+
117+
for _, member := range t.ingGroup.Members {
118+
// Unfortunately we can't support deletion of an endpoint just by removing
119+
// the `ga-epg-arn` annotation, because cleaning up afterwards would require
120+
// us to scan all accelerators and endpointgroups for the desired endpoint.
121+
// To still enable a way to delete the endpoint, users can set `ga-epg-create=false`
122+
epCreate := "false"
123+
if exists := t.annotationParser.ParseStringAnnotation(annotations.IngressSuffixGAEndpointGroup, &rawEPGARN, member.Ing.Annotations); exists {
124+
explicitEPGARNs.Insert(rawEPGARN)
125+
t.annotationParser.ParseStringAnnotation(annotations.IngressSuffixGAEndpointCreate, &epCreate, member.Ing.Annotations)
126+
epCreateByARN[rawEPGARN] = epCreate
127+
}
128+
}
129+
if len(explicitEPGARNs) > 1 {
130+
return nil, errors.Errorf("conflicting Global Accelerator EndpointGroup ARNs: %v", explicitEPGARNs.List())
131+
}
132+
if len(explicitEPGARNs) == 1 {
133+
epgARN, _ := explicitEPGARNs.PopAny()
134+
if epgARN != "" {
135+
create := false
136+
if epCreateByARN[epgARN] == "true" {
137+
create = true
138+
}
139+
endpoint := gamodel.NewEndpoint(t.stack, resourceIDLoadBalancer, gamodel.EndpointSpec{
140+
EndpointGroupARN: epgARN,
141+
ResourceARN: lbARN,
142+
Create: create,
143+
})
144+
return endpoint, nil
145+
}
146+
}
147+
148+
return nil, nil
149+
}

0 commit comments

Comments
 (0)