Skip to content

Commit 64920a0

Browse files
authored
feat: allow multiple environments per VPC via new service discovery namespace (#2515)
This PR modifies the default service discovery namespace for new environments to the form {env}.{app}.local. This change allows customers to place multiple environments in the same VPC. The PR also creates an upgrade path for older environments. Those upgraded environments will retain the default app.local namespace to avoid a backward breaking change for services deployed there, but new environments will use env.app.local. <img width="761" alt="Screen Shot 2021-06-24 at 3 56 02 PM" src="https://user-images.githubusercontent.com/25392995/123342814-50146a00-d505-11eb-8749-41e409150999.png"> In the image above, I have deployed the same service to 3 environments. legacy was created at environment version 1.4.0. and upgraded to 1.5.0. new was created at version 1.5.0 shared-with-legacy was created at 1.5.0 and shares a vpc and subnets with legacy. There currently is not an upgrade path for existing environments to switch to new service discovery. This should not be an issue in most cases, however, as the environment variable points to the correct namespace in all cases and new+old environments can coexist in the same VPC. This change does not disrupt any existing environments. If we eventually add an "environment v2.0.0," we can force migration to the new discovery service by removing the UseLegacyServiceDiscoveryIfBlank parameter and updating our workloads to remove the LegacyServiceDiscovery bool from the template package. Updated integ tests to reflect new endpoints. Manually tested the above by running `copilot svc exec -n fe -e $ENV` for each of the three environments, then running `curl fe.$COPILOT_SVC_DISCOVERY_ENDPOINT` from within the container. Each time, I received the expected HTML content. Related #2377 By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 07eaba8 commit 64920a0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+494
-171
lines changed

e2e/customized-env/customized_env_test.go

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ var _ = Describe("Customized Env", func() {
7676

7777
Context("when adding cross account environments", func() {
7878
var (
79-
testEnvInitErr error
80-
prodEnvInitErr error
79+
testEnvInitErr error
80+
prodEnvInitErr error
81+
sharedEnvInitErr error
8182
)
8283
BeforeAll(func() {
8384
_, testEnvInitErr = cli.EnvInit(&client.EnvInitRequest{
@@ -96,17 +97,26 @@ var _ = Describe("Customized Env", func() {
9697
VPCConfig: vpcConfig,
9798
CustomizedEnv: true,
9899
})
100+
_, sharedEnvInitErr = cli.EnvInit(&client.EnvInitRequest{
101+
AppName: appName,
102+
EnvName: "shared",
103+
Profile: "default",
104+
Prod: false,
105+
VPCImport: vpcImport,
106+
CustomizedEnv: true,
107+
})
99108
})
100109

101-
It("env init should succeed for test and prod envs", func() {
110+
It("env init should succeed for test, prod and shared envs", func() {
102111
Expect(testEnvInitErr).NotTo(HaveOccurred())
103112
Expect(prodEnvInitErr).NotTo(HaveOccurred())
113+
Expect(sharedEnvInitErr).NotTo(HaveOccurred())
104114
})
105115

106-
It("env ls should list both envs", func() {
116+
It("env ls should list all three envs", func() {
107117
envListOutput, err := cli.EnvList(appName)
108118
Expect(err).NotTo(HaveOccurred())
109-
Expect(len(envListOutput.Envs)).To(Equal(2))
119+
Expect(len(envListOutput.Envs)).To(Equal(3))
110120
envs := map[string]client.EnvDescription{}
111121
for _, env := range envListOutput.Envs {
112122
envs[env.Name] = env
@@ -117,6 +127,9 @@ var _ = Describe("Customized Env", func() {
117127
Expect(envs["test"]).NotTo(BeNil())
118128
Expect(envs["test"].Prod).To(BeFalse())
119129

130+
Expect(envs["shared"]).NotTo(BeNil())
131+
Expect(envs["shared"].Prod).To(BeFalse())
132+
120133
Expect(envs["prod"]).NotTo(BeNil())
121134
Expect(envs["prod"].Prod).To(BeTrue())
122135
})
@@ -169,10 +182,11 @@ environments:
169182
})
170183
})
171184

172-
Context("when deploying a svc to test and prod envs", func() {
185+
Context("when deploying a svc to test, prod, and shared envs", func() {
173186
var (
174187
testDeployErr error
175188
prodEndDeployErr error
189+
sharedDeployErr error
176190
svcName string
177191
)
178192
BeforeAll(func() {
@@ -188,32 +202,44 @@ environments:
188202
EnvName: "prod",
189203
ImageTag: "gallopinggurdey",
190204
})
205+
_, sharedDeployErr = cli.SvcDeploy(&client.SvcDeployInput{
206+
Name: svcName,
207+
EnvName: "shared",
208+
ImageTag: "gallopinggurdey",
209+
})
191210
})
192211

193-
It("svc deploy should succeed to both environments", func() {
212+
It("svc deploy should succeed to all three environments", func() {
194213
Expect(testDeployErr).NotTo(HaveOccurred())
195214
Expect(prodEndDeployErr).NotTo(HaveOccurred())
215+
Expect(sharedDeployErr).NotTo(HaveOccurred())
196216
})
197217

198-
It("svc show should include a valid URL and description for test and prod envs", func() {
218+
It("svc show should include a valid URL and description for test, prod and shared envs", func() {
199219
svc, svcShowErr := cli.SvcShow(&client.SvcShowRequest{
200220
AppName: appName,
201221
Name: svcName,
202222
})
203223
Expect(svcShowErr).NotTo(HaveOccurred())
204-
Expect(len(svc.Routes)).To(Equal(2))
224+
Expect(len(svc.Routes)).To(Equal(3))
205225
// Group routes by environment
206226
envRoutes := map[string]client.SvcShowRoutes{}
207227
for _, route := range svc.Routes {
208228
envRoutes[route.Environment] = route
209229
}
210230

211-
Expect(len(svc.ServiceDiscoveries)).To(Equal(1))
212-
Expect(svc.ServiceDiscoveries[0].Environment).To(ConsistOf("test", "prod"))
213-
Expect(svc.ServiceDiscoveries[0].Namespace).To(Equal(fmt.Sprintf("%s.%s.local:80", svc.SvcName, appName)))
231+
Expect(len(svc.ServiceDiscoveries)).To(Equal(3))
232+
var envs, namespaces, wantedNamespaces []string
233+
for _, sd := range svc.ServiceDiscoveries {
234+
envs = append(envs, sd.Environment[0])
235+
namespaces = append(namespaces, sd.Namespace)
236+
wantedNamespaces = append(wantedNamespaces, fmt.Sprintf("%s.%s.%s.local:80", svc.SvcName, sd.Environment, appName))
237+
}
238+
Expect(envs).To(ConsistOf("test", "prod", "shared"))
239+
Expect(namespaces).To(ConsistOf(wantedNamespaces))
214240

215241
// Call each environment's endpoint and ensure it returns a 200
216-
for _, env := range []string{"test", "prod"} {
242+
for _, env := range []string{"test", "prod", "shared"} {
217243
route := envRoutes[env]
218244
Expect(route.Environment).To(Equal(env))
219245
Eventually(func() (int, error) {
@@ -224,7 +250,7 @@ environments:
224250
})
225251

226252
It("svc logs should display logs", func() {
227-
for _, envName := range []string{"test", "prod"} {
253+
for _, envName := range []string{"test", "prod", "shared"} {
228254
var svcLogs []client.SvcLogsOutput
229255
var svcLogsErr error
230256
Eventually(func() ([]client.SvcLogsOutput, error) {
@@ -246,9 +272,9 @@ environments:
246272
}
247273
})
248274

249-
It("env show should display info for test and prod envs", func() {
275+
It("env show should display info for test, prod, and shared envs", func() {
250276
envs := map[string]client.EnvDescription{}
251-
for _, envName := range []string{"test", "prod"} {
277+
for _, envName := range []string{"test", "prod", "shared"} {
252278
envShowOutput, envShowErr := cli.EnvShow(&client.EnvShowRequest{
253279
AppName: appName,
254280
EnvName: envName,
@@ -272,6 +298,8 @@ environments:
272298
Expect(envs["test"].Prod).To(BeFalse())
273299
Expect(envs["prod"]).NotTo(BeNil())
274300
Expect(envs["prod"].Prod).To(BeTrue())
301+
Expect(envs["shared"]).NotTo(BeNil())
302+
Expect(envs["shared"].Prod).To(BeFalse())
275303
})
276304
})
277305
})

e2e/init/init_suite_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
var cli *client.CLI
1717
var appName string
18+
var envName string
1819

1920
/**
2021
The Init Suite runs through the copilot init workflow for a brand new
@@ -31,6 +32,7 @@ var _ = BeforeSuite(func() {
3132
cli = ecsCli
3233
Expect(err).NotTo(HaveOccurred())
3334
appName = fmt.Sprintf("e2e-init-%d", time.Now().Unix())
35+
envName = "test"
3436
})
3537

3638
var _ = AfterSuite(func() {

e2e/init/init_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ var _ = Describe("init flow", func() {
133133
It("should return a valid service discovery namespace", func() {
134134
Expect(len(svc.ServiceDiscoveries)).To(Equal(1))
135135
Expect(svc.ServiceDiscoveries[0].Environment).To(Equal([]string{"test"}))
136-
Expect(svc.ServiceDiscoveries[0].Namespace).To(Equal(fmt.Sprintf("%s.%s.local:80", svcName, appName)))
136+
Expect(svc.ServiceDiscoveries[0].Namespace).To(Equal(fmt.Sprintf("%s.%s.%s.local:80", svcName, envName, appName)))
137137
})
138138

139139
It("should return the correct environment variables", func() {
@@ -143,7 +143,7 @@ var _ = Describe("init flow", func() {
143143
"COPILOT_ENVIRONMENT_NAME": "test",
144144
"COPILOT_LB_DNS": strings.TrimPrefix(svc.Routes[0].URL, "http://"),
145145
"COPILOT_SERVICE_NAME": svcName,
146-
"COPILOT_SERVICE_DISCOVERY_ENDPOINT": fmt.Sprintf("%s.local", appName),
146+
"COPILOT_SERVICE_DISCOVERY_ENDPOINT": fmt.Sprintf("%s.%s.local", envName, appName),
147147
}
148148
for _, variable := range svc.Variables {
149149
Expect(variable.Value).To(Equal(expectedVars[variable.Name]))

internal/pkg/cli/env_upgrade.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func newEnvUpgradeOpts(vars envUpgradeVars) (*envUpgradeOpts, error) {
7979
sel: selector.NewSelect(prompt.New(), store),
8080
legacyEnvTemplater: stack.NewEnvStackConfig(&deploy.CreateEnvironmentInput{
8181
Version: deploy.LegacyEnvTemplateVersion,
82+
AppName: vars.appName,
8283
}),
8384
prog: termprogress.NewSpinner(log.DiagnosticWriter),
8485
uploader: template.New(),

internal/pkg/cli/interfaces.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ type versionGetter interface {
408408
Version() (string, error)
409409
}
410410

411+
type endpointGetter interface {
412+
ServiceDiscoveryEndpoint() (string, error)
413+
}
414+
411415
type envTemplater interface {
412416
EnvironmentTemplate(appName, envName string) (string, error)
413417
}

internal/pkg/cli/job_deploy.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111

1212
"github.com/aws/copilot-cli/internal/pkg/deploy"
13+
"github.com/aws/copilot-cli/internal/pkg/describe"
1314

1415
"github.com/aws/copilot-cli/internal/pkg/addon"
1516
"github.com/aws/copilot-cli/internal/pkg/exec"
@@ -47,6 +48,7 @@ type deployJobOpts struct {
4748
sessProvider sessionProvider
4849
s3 artifactUploader
4950
envUpgradeCmd actionCommand
51+
endpointGetter endpointGetter
5052

5153
spinner progress
5254
sel wsSelector
@@ -206,6 +208,14 @@ func (o *deployJobOpts) configureClients() error {
206208

207209
// CF client against env account profile AND target environment region
208210
o.jobCFN = cloudformation.New(envSession)
211+
o.endpointGetter, err = describe.NewEnvDescriber(describe.NewEnvDescriberConfig{
212+
App: o.appName,
213+
Env: o.envName,
214+
ConfigStore: o.store,
215+
})
216+
if err != nil {
217+
return fmt.Errorf("initiate environment describer: %w", err)
218+
}
209219

210220
addonsSvc, err := addon.New(o.name)
211221
if err != nil {
@@ -300,10 +310,15 @@ func (o *deployJobOpts) stackConfiguration(addonsURL string) (cloudformation.Sta
300310
}
301311

302312
func (o *deployJobOpts) runtimeConfig(addonsURL string) (*stack.RuntimeConfig, error) {
313+
endpoint, err := o.endpointGetter.ServiceDiscoveryEndpoint()
314+
if err != nil {
315+
return nil, err
316+
}
303317
if !o.buildRequired {
304318
return &stack.RuntimeConfig{
305-
AddonsTemplateURL: addonsURL,
306-
AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags),
319+
AddonsTemplateURL: addonsURL,
320+
AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags),
321+
ServiceDiscoveryEndpoint: endpoint,
307322
}, nil
308323
}
309324
resources, err := o.appCFN.GetAppResourcesByRegion(o.targetApp, o.targetEnvironment.Region)
@@ -324,8 +339,9 @@ func (o *deployJobOpts) runtimeConfig(addonsURL string) (*stack.RuntimeConfig, e
324339
ImageTag: o.imageTag,
325340
Digest: o.imageDigest,
326341
},
327-
AddonsTemplateURL: addonsURL,
328-
AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags),
342+
AddonsTemplateURL: addonsURL,
343+
AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags),
344+
ServiceDiscoveryEndpoint: endpoint,
329345
}, nil
330346
}
331347

internal/pkg/cli/job_package.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io/ioutil"
99
"os"
1010

11+
"github.com/aws/copilot-cli/internal/pkg/describe"
1112
"github.com/aws/copilot-cli/internal/pkg/exec"
1213

1314
"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
@@ -104,6 +105,17 @@ func newPackageJobOpts(vars packageJobVars) (*packageJobOpts, error) {
104105
addonsWriter: ioutil.Discard,
105106
fs: &afero.Afero{Fs: afero.NewOsFs()},
106107
stackSerializer: o.stackSerializer,
108+
newEndpointGetter: func(app, env string) (endpointGetter, error) {
109+
d, err := describe.NewEnvDescriber(describe.NewEnvDescriberConfig{
110+
App: app,
111+
Env: env,
112+
ConfigStore: store,
113+
})
114+
if err != nil {
115+
return nil, fmt.Errorf("new env describer for environment %s in app %s: %v", env, app, err)
116+
}
117+
return d, nil
118+
},
107119
}
108120
}
109121
return opts, nil

internal/pkg/cli/mocks/mock_interfaces.go

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)