Skip to content

Commit 28e656c

Browse files
authored
feat: support importing internal ALBs for backend services (#5490)
Related: #5438. Integ test changes in #5483. #5483 adds the Load Balancer DNS name to the service as an env var (and therefore appears in `svc show` output. However, it is not (yet) included in URI output as a recommended action in `svc deploy` output. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
1 parent 83a0be3 commit 28e656c

File tree

11 files changed

+339
-11
lines changed

11 files changed

+339
-11
lines changed

internal/pkg/cli/deploy/backend.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package deploy
55

66
import (
77
"fmt"
8+
"github.com/aws/aws-sdk-go/aws"
9+
"github.com/aws/copilot-cli/internal/pkg/aws/elbv2"
810

911
"github.com/aws/aws-sdk-go/aws/session"
1012
"github.com/aws/copilot-cli/internal/pkg/aws/acm"
@@ -21,6 +23,7 @@ import (
2123

2224
type backendSvcDeployer struct {
2325
*svcDeployer
26+
elbGetter elbGetter
2427
backendMft *manifest.BackendService
2528

2629
// Overriden in tests.
@@ -41,6 +44,7 @@ func NewBackendDeployer(in *WorkloadDeployerInput) (*backendSvcDeployer, error)
4144
}
4245
return &backendSvcDeployer{
4346
svcDeployer: svcDeployer,
47+
elbGetter: elbv2.New(svcDeployer.envSess),
4448
backendMft: bsMft,
4549
aliasCertValidator: acm.New(svcDeployer.envSess),
4650
}, nil
@@ -94,6 +98,14 @@ func (d *backendSvcDeployer) stackConfiguration(in *StackRuntimeConfiguration) (
9498
if err := d.validateALBRuntime(); err != nil {
9599
return nil, err
96100
}
101+
var opts []stack.BackendServiceOption
102+
if d.backendMft.HTTP.ImportedALB != nil {
103+
lb, err := d.elbGetter.LoadBalancer(aws.StringValue(d.backendMft.HTTP.ImportedALB))
104+
if err != nil {
105+
return nil, err
106+
}
107+
opts = append(opts, stack.WithImportedInternalALB(lb))
108+
}
97109

98110
var conf cloudformation.StackConfiguration
99111
switch {
@@ -109,7 +121,7 @@ func (d *backendSvcDeployer) stackConfiguration(in *StackRuntimeConfiguration) (
109121
ArtifactKey: d.resources.KMSKeyARN,
110122
RuntimeConfig: *rc,
111123
Addons: d.addons,
112-
})
124+
}, opts...)
113125
if err != nil {
114126
return nil, fmt.Errorf("create stack configuration: %w", err)
115127
}
@@ -127,6 +139,9 @@ func (d *backendSvcDeployer) validateALBRuntime() error {
127139
if d.backendMft.HTTP.IsEmpty() {
128140
return nil
129141
}
142+
if err := d.validateImportedALBConfig(); err != nil {
143+
return fmt.Errorf(`validate imported ALB configuration for "http": %w`, err)
144+
}
130145
if err := d.validateRuntimeRoutingRule(d.backendMft.HTTP.Main); err != nil {
131146
return fmt.Errorf(`validate ALB runtime configuration for "http": %w`, err)
132147
}
@@ -138,6 +153,37 @@ func (d *backendSvcDeployer) validateALBRuntime() error {
138153
return nil
139154
}
140155

156+
func (d *backendSvcDeployer) validateImportedALBConfig() error {
157+
if d.backendMft.HTTP.ImportedALB == nil {
158+
return nil
159+
}
160+
alb, err := d.elbGetter.LoadBalancer(aws.StringValue(d.backendMft.HTTP.ImportedALB))
161+
if err != nil {
162+
return fmt.Errorf(`retrieve load balancer %q: %w`, aws.StringValue(d.backendMft.HTTP.ImportedALB), err)
163+
}
164+
if alb.Scheme != "internal" {
165+
return fmt.Errorf(`imported ALB %q for Backend Service %q should have "internal" Scheme value`, alb.ARN, aws.StringValue(d.backendMft.Name))
166+
}
167+
if len(alb.Listeners) == 0 {
168+
return fmt.Errorf(`imported ALB %q must have at least one listener. For two listeners, one must be of protocol HTTP and the other of protocol HTTPS`, alb.ARN)
169+
}
170+
if len(alb.Listeners) == 1 {
171+
return nil
172+
}
173+
var quantHTTP, quantHTTPS int
174+
for _, listener := range alb.Listeners {
175+
if listener.Protocol == "HTTP" {
176+
quantHTTP += 1
177+
} else if listener.Protocol == "HTTPS" {
178+
quantHTTPS += 1
179+
}
180+
}
181+
if quantHTTP != 1 || quantHTTPS != 1 {
182+
return fmt.Errorf("imported ALB %q must have exactly one listener of protocol HTTP and exactly one listener of protocol HTTPS", alb.ARN)
183+
}
184+
return nil
185+
}
186+
141187
func (d *backendSvcDeployer) validateRuntimeRoutingRule(rule manifest.RoutingRule) error {
142188
if rule.IsEmpty() {
143189
return nil

internal/pkg/cli/deploy/backend_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package deploy
55

66
import (
77
"errors"
8+
"github.com/aws/copilot-cli/internal/pkg/aws/elbv2"
89
"testing"
910
"time"
1011

@@ -254,6 +255,213 @@ func TestBackendSvcDeployer_stackConfiguration(t *testing.T) {
254255
},
255256
expectedErr: `validate ALB runtime configuration for "http.additional_rules[0]": cannot deploy service mock-svc without "alias" to environment mock-env with certificate imported`,
256257
},
258+
"failure if can't retrieve imported ALB": {
259+
App: &config.Application{
260+
Name: mockAppName,
261+
},
262+
Env: &config.Environment{
263+
Name: mockEnvName,
264+
},
265+
Manifest: &manifest.BackendService{
266+
BackendServiceConfig: manifest.BackendServiceConfig{
267+
HTTP: manifest.HTTP{
268+
Main: manifest.RoutingRule{
269+
Path: aws.String("/"),
270+
},
271+
ImportedALB: aws.String("mockALB"),
272+
},
273+
},
274+
},
275+
setupMocks: func(m *deployMocks) {
276+
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
277+
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
278+
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(nil, errors.New("some error"))
279+
},
280+
expectedErr: `validate imported ALB configuration for "http": retrieve load balancer "mockALB": some error`,
281+
},
282+
"failure if imported ALB has 'internet-facing' not 'internal' scheme": {
283+
App: &config.Application{
284+
Name: mockAppName,
285+
},
286+
Env: &config.Environment{
287+
Name: mockEnvName,
288+
},
289+
Manifest: &manifest.BackendService{
290+
Workload: manifest.Workload{
291+
Name: aws.String("be"),
292+
},
293+
BackendServiceConfig: manifest.BackendServiceConfig{
294+
HTTP: manifest.HTTP{
295+
Main: manifest.RoutingRule{
296+
Path: aws.String("/"),
297+
},
298+
ImportedALB: aws.String("mockALB"),
299+
},
300+
},
301+
},
302+
setupMocks: func(m *deployMocks) {
303+
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
304+
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
305+
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
306+
ARN: "mockALBARN",
307+
Name: "mockALB",
308+
Scheme: "internet-facing",
309+
}, nil)
310+
},
311+
expectedErr: `validate imported ALB configuration for "http": imported ALB "mockALBARN" for Backend Service "be" should have "internal" Scheme value`,
312+
},
313+
"failure if imported ALB has no listeners": {
314+
App: &config.Application{
315+
Name: mockAppName,
316+
},
317+
Env: &config.Environment{
318+
Name: mockEnvName,
319+
},
320+
Manifest: &manifest.BackendService{
321+
BackendServiceConfig: manifest.BackendServiceConfig{
322+
HTTP: manifest.HTTP{
323+
Main: manifest.RoutingRule{
324+
Path: aws.String("/"),
325+
},
326+
ImportedALB: aws.String("mockALB"),
327+
},
328+
},
329+
},
330+
setupMocks: func(m *deployMocks) {
331+
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
332+
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
333+
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
334+
ARN: "mockALBARN",
335+
Name: "mockALB",
336+
Scheme: "internal",
337+
Listeners: []elbv2.Listener{},
338+
}, nil)
339+
},
340+
expectedErr: `validate imported ALB configuration for "http": imported ALB "mockALBARN" must have at least one listener. For two listeners, one must be of protocol HTTP and the other of protocol HTTPS`,
341+
},
342+
"failure if imported ALB has more than 2 listeners": {
343+
App: &config.Application{
344+
Name: mockAppName,
345+
},
346+
Env: &config.Environment{
347+
Name: mockEnvName,
348+
},
349+
Manifest: &manifest.BackendService{
350+
BackendServiceConfig: manifest.BackendServiceConfig{
351+
HTTP: manifest.HTTP{
352+
Main: manifest.RoutingRule{
353+
Path: aws.String("/"),
354+
},
355+
ImportedALB: aws.String("mockALB"),
356+
},
357+
},
358+
},
359+
setupMocks: func(m *deployMocks) {
360+
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
361+
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
362+
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
363+
ARN: "mockALBARN",
364+
Name: "mockALB",
365+
Scheme: "internal",
366+
Listeners: []elbv2.Listener{
367+
{
368+
ARN: "default",
369+
Port: 0,
370+
Protocol: "something",
371+
},
372+
{
373+
ARN: "second",
374+
Port: 80,
375+
Protocol: "http",
376+
},
377+
{
378+
ARN: "third",
379+
Port: 443,
380+
Protocol: "https",
381+
}},
382+
}, nil)
383+
},
384+
expectedErr: `validate imported ALB configuration for "http": imported ALB "mockALBARN" must have exactly one listener of protocol HTTP and exactly one listener of protocol HTTPS`,
385+
},
386+
"failure if imported ALB has two listeners but they don't have HTTP and HTTPS protocols": {
387+
App: &config.Application{
388+
Name: mockAppName,
389+
},
390+
Env: &config.Environment{
391+
Name: mockEnvName,
392+
},
393+
Manifest: &manifest.BackendService{
394+
Workload: manifest.Workload{
395+
Name: aws.String("be"),
396+
},
397+
BackendServiceConfig: manifest.BackendServiceConfig{
398+
HTTP: manifest.HTTP{
399+
Main: manifest.RoutingRule{
400+
Path: aws.String("/"),
401+
},
402+
ImportedALB: aws.String("mockALB"),
403+
},
404+
},
405+
},
406+
setupMocks: func(m *deployMocks) {
407+
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
408+
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
409+
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
410+
ARN: "mockALBARN",
411+
Name: "mockALB",
412+
Scheme: "internal",
413+
Listeners: []elbv2.Listener{
414+
{
415+
ARN: "default",
416+
Port: 0,
417+
Protocol: "something",
418+
},
419+
{
420+
ARN: "second",
421+
Port: 80,
422+
Protocol: "boop",
423+
}},
424+
}, nil)
425+
},
426+
expectedErr: `validate imported ALB configuration for "http": imported ALB "mockALBARN" must have exactly one listener of protocol HTTP and exactly one listener of protocol HTTPS`,
427+
},
428+
"success imported ALB": {
429+
App: &config.Application{
430+
Name: mockAppName,
431+
},
432+
Env: &config.Environment{
433+
Name: mockEnvName,
434+
},
435+
Manifest: &manifest.BackendService{
436+
Workload: manifest.Workload{
437+
Name: aws.String("be"),
438+
},
439+
BackendServiceConfig: manifest.BackendServiceConfig{
440+
HTTP: manifest.HTTP{
441+
Main: manifest.RoutingRule{
442+
Path: aws.String("/"),
443+
},
444+
ImportedALB: aws.String("mockALB"),
445+
},
446+
},
447+
},
448+
setupMocks: func(m *deployMocks) {
449+
m.mockEndpointGetter.EXPECT().ServiceDiscoveryEndpoint().Return(mockAppName+".local", nil)
450+
m.mockEnvVersionGetter.EXPECT().Version().Return("v1.42.0", nil)
451+
m.mockELBGetter.EXPECT().LoadBalancer("mockALB").Return(&elbv2.LoadBalancer{
452+
ARN: "mockALBARN",
453+
Name: "mockALB",
454+
Scheme: "internal",
455+
Listeners: []elbv2.Listener{
456+
{
457+
ARN: "yarn",
458+
Port: 80,
459+
Protocol: "HTTP",
460+
},
461+
},
462+
}, nil).Times(2)
463+
},
464+
},
257465
"success if env has imported certs but alb not configured": {
258466
App: &config.Application{
259467
Name: mockAppName,
@@ -281,6 +489,7 @@ func TestBackendSvcDeployer_stackConfiguration(t *testing.T) {
281489
mockEndpointGetter: mocks.NewMockendpointGetter(ctrl),
282490
mockValidator: mocks.NewMockaliasCertValidator(ctrl),
283491
mockEnvVersionGetter: mocks.NewMockversionGetter(ctrl),
492+
mockELBGetter: mocks.NewMockelbGetter(ctrl),
284493
}
285494
if tc.setupMocks != nil {
286495
tc.setupMocks(m)
@@ -305,6 +514,7 @@ func TestBackendSvcDeployer_stackConfiguration(t *testing.T) {
305514
return nil
306515
},
307516
},
517+
elbGetter: m.mockELBGetter,
308518
backendMft: tc.Manifest,
309519
aliasCertValidator: m.mockValidator,
310520
newStack: func() cloudformation.StackConfiguration {

internal/pkg/cli/deploy/workload_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type deployMocks struct {
6161
mockValidator *mocks.MockaliasCertValidator
6262
mockLabeledTermPrinter *mocks.MockLabeledTermPrinter
6363
mockdockerEngineRunChecker *mocks.MockdockerEngineRunChecker
64+
mockELBGetter *mocks.MockelbGetter
6465
}
6566

6667
type mockTemplateFS struct {

internal/pkg/cli/run_local.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1102,7 +1102,7 @@ func (h *hostDiscoverer) Hosts(ctx context.Context) ([]orchestrator.Host, error)
11021102

11031103
var hosts []orchestrator.Host
11041104
for _, svc := range svcs {
1105-
// find the primary deployment with service connect enabled
1105+
// find the primary deployment with Service Connect enabled
11061106
idx := slices.IndexFunc(svc.Deployments, func(dep *sdkecs.Deployment) bool {
11071107
return aws.StringValue(dep.Status) == "PRIMARY" && aws.BoolValue(dep.ServiceConnectConfiguration.Enabled)
11081108
})

internal/pkg/cli/svc_deploy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ func (o *deploySvcOpts) uriRecommendedActions() ([]string, error) {
596596
case describe.URIAccessTypeServiceDiscovery:
597597
network = "with service discovery."
598598
case describe.URIAccessTypeServiceConnect:
599-
network = "with service connect."
599+
network = "with Service Connect."
600600
case describe.URIAccessTypeNone:
601601
return []string{}, nil
602602
}

0 commit comments

Comments
 (0)