diff --git a/go.sum b/go.sum index ca04b4fd3a8..fb3fbfe5e9d 100644 --- a/go.sum +++ b/go.sum @@ -1217,6 +1217,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200502202811-ed308ab3e770/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/pkg/aws/cloudformation/cloudformation_test.go b/internal/pkg/aws/cloudformation/cloudformation_test.go index 1ceb037ce22..27bf0de3c90 100644 --- a/internal/pkg/aws/cloudformation/cloudformation_test.go +++ b/internal/pkg/aws/cloudformation/cloudformation_test.go @@ -844,7 +844,6 @@ func TestCloudFormation_Exists(t *testing.T) { require.True(t, exists) }) } - func TestCloudFormation_TemplateBody(t *testing.T) { testCases := map[string]struct { createMock func(ctrl *gomock.Controller) client diff --git a/internal/pkg/cli/interfaces.go b/internal/pkg/cli/interfaces.go index 5f44de99df6..3cf1d273089 100644 --- a/internal/pkg/cli/interfaces.go +++ b/internal/pkg/cli/interfaces.go @@ -363,6 +363,10 @@ type appResourcesGetter interface { GetRegionalAppResources(app *config.Application) ([]*stack.AppRegionalResources, error) } +type envCFDescriber interface { + EnvironmentUsesLegacySvcDiscovery(app, env string) (bool, error) +} + type taskDeployer interface { DeployTask(out termprogress.FileWriter, input *deploy.CreateTaskResourcesInput, opts ...awscloudformation.StackOption) error } diff --git a/internal/pkg/cli/job_deploy.go b/internal/pkg/cli/job_deploy.go index d07bcaa2e37..e5cd434d54c 100644 --- a/internal/pkg/cli/job_deploy.go +++ b/internal/pkg/cli/job_deploy.go @@ -43,6 +43,7 @@ type deployJobOpts struct { addons templater appCFN appResourcesGetter jobCFN cloudformation.CloudFormation + envCFN envCFDescriber imageBuilderPusher imageBuilderPusher sessProvider sessionProvider s3 artifactUploader @@ -207,6 +208,8 @@ func (o *deployJobOpts) configureClients() error { // CF client against env account profile AND target environment region o.jobCFN = cloudformation.New(envSession) + o.envCFN = cloudformation.New(envSession) + addonsSvc, err := addon.New(o.name) if err != nil { return fmt.Errorf("initiate addons service: %w", err) @@ -300,10 +303,15 @@ func (o *deployJobOpts) stackConfiguration(addonsURL string) (cloudformation.Sta } func (o *deployJobOpts) runtimeConfig(addonsURL string) (*stack.RuntimeConfig, error) { + svcDiscovery, err := envUsesLegacySvcDiscovery(o.envCFN, o.appName, o.envName) + if err != nil { + return nil, err + } if !o.buildRequired { return &stack.RuntimeConfig{ - AddonsTemplateURL: addonsURL, - AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags), + AddonsTemplateURL: addonsURL, + AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags), + LegacyServiceDiscovery: svcDiscovery, }, nil } resources, err := o.appCFN.GetAppResourcesByRegion(o.targetApp, o.targetEnvironment.Region) @@ -324,8 +332,9 @@ func (o *deployJobOpts) runtimeConfig(addonsURL string) (*stack.RuntimeConfig, e ImageTag: o.imageTag, Digest: o.imageDigest, }, - AddonsTemplateURL: addonsURL, - AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags), + AddonsTemplateURL: addonsURL, + AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags), + LegacyServiceDiscovery: svcDiscovery, }, nil } diff --git a/internal/pkg/cli/job_package.go b/internal/pkg/cli/job_package.go index 49bfea5c3c6..5f3be2b93bc 100644 --- a/internal/pkg/cli/job_package.go +++ b/internal/pkg/cli/job_package.go @@ -8,11 +8,10 @@ import ( "io/ioutil" "os" + "github.com/aws/copilot-cli/internal/pkg/aws/sessions" "github.com/aws/copilot-cli/internal/pkg/exec" - "github.com/aws/copilot-cli/internal/pkg/aws/sessions" "github.com/aws/copilot-cli/internal/pkg/config" - "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation" "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation/stack" "github.com/aws/copilot-cli/internal/pkg/manifest" "github.com/aws/copilot-cli/internal/pkg/term/prompt" @@ -60,11 +59,7 @@ func newPackageJobOpts(vars packageJobVars) (*packageJobOpts, error) { if err != nil { return nil, fmt.Errorf("connect to config store: %w", err) } - p := sessions.NewProvider() - sess, err := p.Default() - if err != nil { - return nil, fmt.Errorf("retrieve default session: %w", err) - } + prompter := prompt.New() opts := &packageJobOpts{ packageJobVars: vars, @@ -98,12 +93,15 @@ func newPackageJobOpts(vars packageJobVars) (*packageJobOpts, error) { initAddonsClient: initPackageAddonsClient, ws: ws, store: o.store, - appCFN: cloudformation.New(sess), stackWriter: os.Stdout, paramsWriter: ioutil.Discard, addonsWriter: ioutil.Discard, fs: &afero.Afero{Fs: afero.NewOsFs()}, stackSerializer: o.stackSerializer, + configure: func(o *packageSvcOpts) error { + return o.configureClients() + }, + sessProvider: sessions.NewProvider(), } } return opts, nil diff --git a/internal/pkg/cli/mocks/mock_interfaces.go b/internal/pkg/cli/mocks/mock_interfaces.go index fc87612ac78..7962f27b563 100644 --- a/internal/pkg/cli/mocks/mock_interfaces.go +++ b/internal/pkg/cli/mocks/mock_interfaces.go @@ -3613,6 +3613,44 @@ func (mr *MockappResourcesGetterMockRecorder) GetRegionalAppResources(app interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegionalAppResources", reflect.TypeOf((*MockappResourcesGetter)(nil).GetRegionalAppResources), app) } +// MockenvCFDescriber is a mock of envCFDescriber interface. +type MockenvCFDescriber struct { + ctrl *gomock.Controller + recorder *MockenvCFDescriberMockRecorder +} + +// MockenvCFDescriberMockRecorder is the mock recorder for MockenvCFDescriber. +type MockenvCFDescriberMockRecorder struct { + mock *MockenvCFDescriber +} + +// NewMockenvCFDescriber creates a new mock instance. +func NewMockenvCFDescriber(ctrl *gomock.Controller) *MockenvCFDescriber { + mock := &MockenvCFDescriber{ctrl: ctrl} + mock.recorder = &MockenvCFDescriberMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockenvCFDescriber) EXPECT() *MockenvCFDescriberMockRecorder { + return m.recorder +} + +// EnvironmentUsesLegacySvcDiscovery mocks base method. +func (m *MockenvCFDescriber) EnvironmentUsesLegacySvcDiscovery(app, env string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnvironmentUsesLegacySvcDiscovery", app, env) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnvironmentUsesLegacySvcDiscovery indicates an expected call of EnvironmentUsesLegacySvcDiscovery. +func (mr *MockenvCFDescriberMockRecorder) EnvironmentUsesLegacySvcDiscovery(app, env interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnvironmentUsesLegacySvcDiscovery", reflect.TypeOf((*MockenvCFDescriber)(nil).EnvironmentUsesLegacySvcDiscovery), app, env) +} + // MocktaskDeployer is a mock of taskDeployer interface. type MocktaskDeployer struct { ctrl *gomock.Controller diff --git a/internal/pkg/cli/svc_deploy.go b/internal/pkg/cli/svc_deploy.go index dfadd00a7a2..dc4a98002e3 100644 --- a/internal/pkg/cli/svc_deploy.go +++ b/internal/pkg/cli/svc_deploy.go @@ -57,6 +57,7 @@ type deploySvcOpts struct { addons templater appCFN appResourcesGetter svcCFN cloudformation.CloudFormation + envCFN envCFDescriber sessProvider sessionProvider envUpgradeCmd actionCommand appVersionGetter versionGetter @@ -259,6 +260,8 @@ func (o *deploySvcOpts) configureClients() error { // CF client against env account profile AND target environment region o.svcCFN = cloudformation.New(envSession) + o.envCFN = cloudformation.New(envSession) + addonsSvc, err := addon.New(o.name) if err != nil { return fmt.Errorf("initiate addons service: %w", err) @@ -378,11 +381,20 @@ func (o *deploySvcOpts) manifest() (interface{}, error) { return mft, nil } +func envUsesLegacySvcDiscovery(cf envCFDescriber, app, env string) (bool, error) { + return cf.EnvironmentUsesLegacySvcDiscovery(app, env) +} + func (o *deploySvcOpts) runtimeConfig(addonsURL string) (*stack.RuntimeConfig, error) { + svcDiscovery, err := envUsesLegacySvcDiscovery(o.envCFN, o.appName, o.envName) + if err != nil { + return nil, err + } if !o.buildRequired { return &stack.RuntimeConfig{ - AddonsTemplateURL: addonsURL, - AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags), + AddonsTemplateURL: addonsURL, + AdditionalTags: tags.Merge(o.targetApp.Tags, o.resourceTags), + LegacyServiceDiscovery: svcDiscovery, }, nil } resources, err := o.appCFN.GetAppResourcesByRegion(o.targetApp, o.targetEnvironment.Region) @@ -405,6 +417,7 @@ func (o *deploySvcOpts) runtimeConfig(addonsURL string) (*stack.RuntimeConfig, e ImageTag: o.imageTag, Digest: o.imageDigest, }, + LegacyServiceDiscovery: svcDiscovery, }, nil } diff --git a/internal/pkg/cli/svc_deploy_test.go b/internal/pkg/cli/svc_deploy_test.go index c76a95f6d6b..e2f0fb409db 100644 --- a/internal/pkg/cli/svc_deploy_test.go +++ b/internal/pkg/cli/svc_deploy_test.go @@ -357,6 +357,7 @@ func TestSvcDeployOpts_pushAddonsTemplateToS3Bucket(t *testing.T) { mockAppResourcesGetter func(m *mocks.MockappResourcesGetter) mockS3Svc func(m *mocks.MockartifactUploader) mockAddons func(m *mocks.Mocktemplater) + mockEnvCFDescriber func(m *mocks.MockenvCFDescriber) wantPath string wantErr error @@ -404,9 +405,9 @@ func TestSvcDeployOpts_pushAddonsTemplateToS3Bucket(t *testing.T) { mockAddons: func(m *mocks.Mocktemplater) { m.EXPECT().Template().Return("some data", nil) }, - mockS3Svc: func(m *mocks.MockartifactUploader) {}, - - wantErr: fmt.Errorf("get app resources: some error"), + mockS3Svc: func(m *mocks.MockartifactUploader) {}, + mockEnvCFDescriber: func(m *mocks.MockenvCFDescriber) {}, + wantErr: fmt.Errorf("get app resources: some error"), }, "should return error if fail to upload to S3 bucket": { inputSvc: "mockSvc", @@ -517,6 +518,7 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { mockWorkspace func(m *mocks.MockwsSvcDirReader) mockAppResourcesGetter func(m *mocks.MockappResourcesGetter) mockAppVersionGetter func(m *mocks.MockversionGetter) + mockEnvCFDescriber func(m *mocks.MockenvCFDescriber) wantErr error }{ @@ -526,6 +528,7 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { }, mockAppResourcesGetter: func(m *mocks.MockappResourcesGetter) {}, mockAppVersionGetter: func(m *mocks.MockversionGetter) {}, + mockEnvCFDescriber: func(m *mocks.MockenvCFDescriber) {}, wantErr: fmt.Errorf("read service %s manifest file: %w", mockSvcName, mockError), }, "fail to get app resources": { @@ -545,10 +548,32 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { Name: mockAppName, }, "us-west-2").Return(nil, mockError) }, + mockEnvCFDescriber: func(m *mocks.MockenvCFDescriber) { + m.EXPECT().EnvironmentUsesLegacySvcDiscovery(mockAppName, mockEnvName).Return(true, nil) + }, mockAppVersionGetter: func(m *mocks.MockversionGetter) {}, wantErr: fmt.Errorf("get application %s resources from region us-west-2: %w", mockAppName, mockError), }, - "cannot to find ECR repo": { + "fail to get environment": { + inBuildRequire: true, + inEnvironment: &config.Environment{ + Name: mockEnvName, + Region: "us-west-2", + }, + inApp: &config.Application{ + Name: mockAppName, + }, + mockWorkspace: func(m *mocks.MockwsSvcDirReader) { + m.EXPECT().ReadServiceManifest(mockSvcName).Return([]byte{}, nil) + }, + mockAppResourcesGetter: func(m *mocks.MockappResourcesGetter) {}, + mockEnvCFDescriber: func(m *mocks.MockenvCFDescriber) { + m.EXPECT().EnvironmentUsesLegacySvcDiscovery(mockAppName, mockEnvName).Return(false, errors.New("some error")) + }, + mockAppVersionGetter: func(m *mocks.MockversionGetter) {}, + wantErr: errors.New("some error"), + }, + "cannot find ECR repo": { inBuildRequire: true, inEnvironment: &config.Environment{ Name: mockEnvName, @@ -569,6 +594,9 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { RepositoryURLs: map[string]string{}, }, nil) }, + mockEnvCFDescriber: func(m *mocks.MockenvCFDescriber) { + m.EXPECT().EnvironmentUsesLegacySvcDiscovery(mockAppName, mockEnvName).Return(true, nil) + }, mockAppVersionGetter: func(m *mocks.MockversionGetter) {}, wantErr: fmt.Errorf("ECR repository not found for service mockSvc in region us-west-2 and account 1234567890"), }, @@ -589,6 +617,9 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { mockAppVersionGetter: func(m *mocks.MockversionGetter) { m.EXPECT().Version().Return("", mockError) }, + mockEnvCFDescriber: func(m *mocks.MockenvCFDescriber) { + m.EXPECT().EnvironmentUsesLegacySvcDiscovery(mockAppName, mockEnvName).Return(false, nil) + }, wantErr: fmt.Errorf("get version for app %s: %w", mockAppName, mockError), }, "fail to enable https alias because of incompatible app version": { @@ -608,6 +639,9 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { mockAppVersionGetter: func(m *mocks.MockversionGetter) { m.EXPECT().Version().Return("v0.0.0", nil) }, + mockEnvCFDescriber: func(m *mocks.MockenvCFDescriber) { + m.EXPECT().EnvironmentUsesLegacySvcDiscovery(mockAppName, mockEnvName).Return(false, nil) + }, wantErr: fmt.Errorf(`enable "http.alias": the application version should be at least %s`, deploy.AliasLeastAppTemplateVersion), }, "success": { @@ -622,6 +656,9 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { mockWorkspace: func(m *mocks.MockwsSvcDirReader) { m.EXPECT().ReadServiceManifest(mockSvcName).Return([]byte{}, nil) }, + mockEnvCFDescriber: func(m *mocks.MockenvCFDescriber) { + m.EXPECT().EnvironmentUsesLegacySvcDiscovery(mockAppName, mockEnvName).Return(false, nil) + }, mockAppResourcesGetter: func(m *mocks.MockappResourcesGetter) {}, mockAppVersionGetter: func(m *mocks.MockversionGetter) {}, }, @@ -634,10 +671,12 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { mockWorkspace := mocks.NewMockwsSvcDirReader(ctrl) mockAppResourcesGetter := mocks.NewMockappResourcesGetter(ctrl) + mockEnvCFDescriber := mocks.NewMockenvCFDescriber(ctrl) mockAppVersionGetter := mocks.NewMockversionGetter(ctrl) tc.mockWorkspace(mockWorkspace) tc.mockAppResourcesGetter(mockAppResourcesGetter) tc.mockAppVersionGetter(mockAppVersionGetter) + tc.mockEnvCFDescriber(mockEnvCFDescriber) opts := deploySvcOpts{ deployWkldVars: deployWkldVars{ @@ -648,6 +687,7 @@ func TestSvcDeployOpts_stackConfiguration(t *testing.T) { ws: mockWorkspace, buildRequired: tc.inBuildRequire, appCFN: mockAppResourcesGetter, + envCFN: mockEnvCFDescriber, appVersionGetter: mockAppVersionGetter, targetApp: tc.inApp, targetEnvironment: tc.inEnvironment, diff --git a/internal/pkg/cli/svc_package.go b/internal/pkg/cli/svc_package.go index 6d396aa3dd7..b63e8892d32 100644 --- a/internal/pkg/cli/svc_package.go +++ b/internal/pkg/cli/svc_package.go @@ -59,6 +59,7 @@ type packageSvcOpts struct { ws wsSvcReader store store appCFN appResourcesGetter + envCFN envCFDescriber stackWriter io.Writer paramsWriter io.Writer addonsWriter io.Writer @@ -67,6 +68,13 @@ type packageSvcOpts struct { sel wsSelector prompt prompter stackSerializer func(mft interface{}, env *config.Environment, app *config.Application, rc stack.RuntimeConfig) (stackSerializer, error) + sessProvider sessionProvider + // Cached data + targetEnvironment *config.Environment + runtimeConfig stack.RuntimeConfig + + // overridden in tests + configure func(*packageSvcOpts) error } func newPackageSvcOpts(vars packageSvcVars) (*packageSvcOpts, error) { @@ -78,18 +86,12 @@ func newPackageSvcOpts(vars packageSvcVars) (*packageSvcOpts, error) { if err != nil { return nil, fmt.Errorf("connect to config store: %w", err) } - p := sessions.NewProvider() - sess, err := p.Default() - if err != nil { - return nil, fmt.Errorf("retrieve default session: %w", err) - } prompter := prompt.New() opts := &packageSvcOpts{ packageSvcVars: vars, initAddonsClient: initPackageAddonsClient, ws: ws, store: store, - appCFN: cloudformation.New(sess), runner: exec.NewCmd(), sel: selector.NewWorkspaceSelect(prompter, store, ws), prompt: prompter, @@ -97,6 +99,7 @@ func newPackageSvcOpts(vars packageSvcVars) (*packageSvcOpts, error) { paramsWriter: ioutil.Discard, addonsWriter: ioutil.Discard, fs: &afero.Afero{Fs: afero.NewOsFs()}, + sessProvider: sessions.NewProvider(), } opts.stackSerializer = func(mft interface{}, env *config.Environment, app *config.Application, rc stack.RuntimeConfig) (stackSerializer, error) { @@ -129,6 +132,11 @@ func newPackageSvcOpts(vars packageSvcVars) (*packageSvcOpts, error) { } return serializer, nil } + + opts.configure = func(o *packageSvcOpts) error { + return o.configureClients() + } + return opts, nil } @@ -167,11 +175,18 @@ func (o *packageSvcOpts) Ask() error { // Execute prints the CloudFormation template of the application for the environment. func (o *packageSvcOpts) Execute() error { - o.tag = imageTagFromGit(o.runner, o.tag) // Best effort assign git tag. - env, err := o.store.GetEnvironment(o.appName, o.envName) + + env, err := targetEnv(o.store, o.appName, o.envName) if err != nil { return err } + o.targetEnvironment = env + + if err := o.configure(o); err != nil { + return err + } + + o.tag = imageTagFromGit(o.runner, o.tag) // Best effort assign git tag. if o.outputDir != "" { if err := o.setOutputFileWriters(); err != nil { @@ -179,14 +194,14 @@ func (o *packageSvcOpts) Execute() error { } } - appTemplates, err := o.getSvcTemplates(env) + svcTemplates, err := o.getSvcTemplates() if err != nil { return err } - if _, err = o.stackWriter.Write([]byte(appTemplates.stack)); err != nil { + if _, err = o.stackWriter.Write([]byte(svcTemplates.stack)); err != nil { return err } - if _, err = o.paramsWriter.Write([]byte(appTemplates.configuration)); err != nil { + if _, err = o.paramsWriter.Write([]byte(svcTemplates.configuration)); err != nil { return err } @@ -249,8 +264,24 @@ type svcCfnTemplates struct { configuration string } +func (o *packageSvcOpts) configureClients() error { + defaultSess, err := o.sessProvider.Default() + if err != nil { + return fmt.Errorf("create default session: %w", err) + } + o.appCFN = cloudformation.New(defaultSess) + + envSession, err := o.sessProvider.FromRole(o.targetEnvironment.ManagerRoleARN, o.targetEnvironment.Region) + if err != nil { + return fmt.Errorf("assuming environment manager role: %w", err) + } + o.envCFN = cloudformation.New(envSession) + + return nil +} + // getSvcTemplates returns the CloudFormation stack's template and its parameters for the service. -func (o *packageSvcOpts) getSvcTemplates(env *config.Environment) (*svcCfnTemplates, error) { +func (o *packageSvcOpts) getSvcTemplates() (*svcCfnTemplates, error) { raw, err := o.ws.ReadServiceManifest(o.name) if err != nil { return nil, err @@ -267,11 +298,18 @@ func (o *packageSvcOpts) getSvcTemplates(env *config.Environment) (*svcCfnTempla if err != nil { return nil, err } - rc := stack.RuntimeConfig{ - AdditionalTags: app.Tags, + legacySvcDiscovery, err := envUsesLegacySvcDiscovery(o.envCFN, o.appName, o.envName) + if err != nil { + return nil, err + } + + o.runtimeConfig = stack.RuntimeConfig{ + AdditionalTags: app.Tags, + LegacyServiceDiscovery: legacySvcDiscovery, } + if imgNeedsBuild { - resources, err := o.appCFN.GetAppResourcesByRegion(app, env.Region) + resources, err := o.appCFN.GetAppResourcesByRegion(app, o.targetEnvironment.Region) if err != nil { return nil, err } @@ -279,16 +317,16 @@ func (o *packageSvcOpts) getSvcTemplates(env *config.Environment) (*svcCfnTempla if !ok { return nil, &errRepoNotFound{ wlName: o.name, - envRegion: env.Region, + envRegion: o.targetEnvironment.Region, appAccountID: app.AccountID, } } - rc.Image = &stack.ECRImage{ + o.runtimeConfig.Image = &stack.ECRImage{ RepoURL: repoURL, ImageTag: o.tag, } } - serializer, err := o.stackSerializer(mft, env, app, rc) + serializer, err := o.stackSerializer(mft, o.targetEnvironment, app, o.runtimeConfig) if err != nil { return nil, err } diff --git a/internal/pkg/cli/svc_package_test.go b/internal/pkg/cli/svc_package_test.go index cd71a9f0d1e..4817b4d1b81 100644 --- a/internal/pkg/cli/svc_package_test.go +++ b/internal/pkg/cli/svc_package_test.go @@ -215,10 +215,11 @@ func TestPackageSvcOpts_Execute(t *testing.T) { mockDependencies func(*gomock.Controller, *packageSvcOpts) - wantedStack string - wantedParams string - wantedAddons string - wantedErr error + wantedStack string + wantedParams string + wantedAddons string + wantedRuntimeConfig stack.RuntimeConfig + wantedErr error }{ "writes service template without addons": { inVars: packageSvcVars{ @@ -271,6 +272,9 @@ count: 1`), nil) }, }, nil) + mockEnvCFN := mocks.NewMockenvCFDescriber(ctrl) + mockEnvCFN.EXPECT().EnvironmentUsesLegacySvcDiscovery("ecs-kudos", "test").Return(true, nil) + mockAddons := mocks.NewMocktemplater(ctrl) mockAddons.EXPECT().Template(). Return("", &addon.ErrAddonsDirNotExist{}) @@ -278,6 +282,7 @@ count: 1`), nil) opts.store = mockStore opts.ws = mockWs opts.appCFN = mockCfn + opts.envCFN = mockEnvCFN opts.initAddonsClient = func(opts *packageSvcOpts) error { opts.addonsClient = mockAddons return nil @@ -288,10 +293,21 @@ count: 1`), nil) mockStackSerializer.EXPECT().SerializedParameters().Return("myparams", nil) return mockStackSerializer, nil } + opts.configure = func(*packageSvcOpts) error { return nil } }, wantedStack: "mystack", wantedParams: "myparams", + wantedRuntimeConfig: stack.RuntimeConfig{ + Image: &stack.ECRImage{ + RepoURL: "some url", + ImageTag: "1234", + }, + AdditionalTags: map[string]string{ + "owner": "boss", + }, + LegacyServiceDiscovery: true, + }, }, } @@ -321,6 +337,7 @@ count: 1`), nil) require.Equal(t, tc.wantedStack, stackBuf.String()) require.Equal(t, tc.wantedParams, paramsBuf.String()) require.Equal(t, tc.wantedAddons, addonsBuf.String()) + require.Equal(t, tc.wantedRuntimeConfig, opts.runtimeConfig) }) } } diff --git a/internal/pkg/deploy/cloudformation/env.go b/internal/pkg/deploy/cloudformation/env.go index fba3d9b4397..f5bffd46b03 100644 --- a/internal/pkg/deploy/cloudformation/env.go +++ b/internal/pkg/deploy/cloudformation/env.go @@ -77,6 +77,24 @@ func (cf CloudFormation) EnvironmentTemplate(appName, envName string) (string, e return cf.cfnClient.TemplateBody(stackName) } +// EnvironmentUsesLegacyServiceDiscovery returns true if the environment has been upgraded and services should continue using the legacy namespace. +func (cf CloudFormation) EnvironmentUsesLegacySvcDiscovery(appName, envName string) (bool, error) { + stackName := stack.NameForEnv(appName, envName) + descr, err := cf.cfnClient.Describe(stackName) + if err != nil { + return false, err + } + stackOutputs := make(map[string]string) + for _, output := range descr.Outputs { + stackOutputs[*output.OutputKey] = *output.OutputValue + } + output, ok := stackOutputs[stack.EnvOutputLegacyServiceDiscovery] + if !ok || output != "false" { + return true, nil + } + return false, nil +} + // UpdateEnvironmentTemplate updates the cloudformation stack's template body while maintaining the parameters and tags. func (cf CloudFormation) UpdateEnvironmentTemplate(appName, envName, templateBody, cfnExecRoleARN string) error { stackName := stack.NameForEnv(appName, envName) diff --git a/internal/pkg/deploy/cloudformation/env_test.go b/internal/pkg/deploy/cloudformation/env_test.go index 97b38b9cf81..efa4c90df18 100644 --- a/internal/pkg/deploy/cloudformation/env_test.go +++ b/internal/pkg/deploy/cloudformation/env_test.go @@ -209,6 +209,68 @@ func TestCloudFormation_UpgradeLegacyEnvironment(t *testing.T) { } } +func TestCloudFormation_EnvironmentUsesLegacySvcDiscovery(t *testing.T) { + testCases := map[string]struct { + inAppName string + inEnvName string + inClient func(ctrl *gomock.Controller) *mocks.MockcfnClient + want bool + wantErr error + }{ + "calls Parameters": { + inAppName: "phonetool", + inEnvName: "test", + inClient: func(ctrl *gomock.Controller) *mocks.MockcfnClient { + m := mocks.NewMockcfnClient(ctrl) + m.EXPECT().Describe("phonetool-test").Return(&cloudformation.StackDescription{ + Outputs: []*awscfn.Output{ + { + OutputKey: aws.String("UseLegacyServiceDiscovery"), + OutputValue: aws.String("true"), + }, + }, + }, nil) + return m + }, + want: true, + }, + "false when environment is new": { + inAppName: "phonetool", + inEnvName: "test", + inClient: func(ctrl *gomock.Controller) *mocks.MockcfnClient { + m := mocks.NewMockcfnClient(ctrl) + m.EXPECT().Describe("phonetool-test").Return(&cloudformation.StackDescription{ + Outputs: []*awscfn.Output{ + { + OutputKey: aws.String("UseLegacyServiceDiscovery"), + OutputValue: aws.String("false"), + }, + }, + }, nil) + return m + }, + want: false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + // GIVEN + ctrl := gomock.NewController(t) + defer ctrl.Finish() + cf := &CloudFormation{ + cfnClient: tc.inClient(ctrl), + } + + // WHEN + got, err := cf.EnvironmentUsesLegacySvcDiscovery(tc.inAppName, tc.inEnvName) + + require.Equal(t, tc.want, got) + require.Equal(t, tc.wantErr, err) + }) + } +} + func TestCloudFormation_EnvironmentTemplate(t *testing.T) { testCases := map[string]struct { inAppName string diff --git a/internal/pkg/deploy/cloudformation/stack/backend_svc.go b/internal/pkg/deploy/cloudformation/stack/backend_svc.go index dedfbdb59b6..3a5dd187d8a 100644 --- a/internal/pkg/deploy/cloudformation/stack/backend_svc.go +++ b/internal/pkg/deploy/cloudformation/stack/backend_svc.go @@ -114,24 +114,25 @@ func (s *BackendService) Template() (string, error) { return "", fmt.Errorf(`convert 'command' to string slice: %w`, err) } content, err := s.parser.ParseBackendService(template.WorkloadOpts{ - Variables: s.manifest.BackendServiceConfig.Variables, - Secrets: s.manifest.BackendServiceConfig.Secrets, - NestedStack: outputs, - Sidecars: sidecars, - Autoscaling: autoscaling, - CapacityProviders: capacityProviders, - DesiredCountOnSpot: desiredCountOnSpot, - ExecuteCommand: convertExecuteCommand(&s.manifest.ExecuteCommand), - WorkloadType: manifest.BackendServiceType, - HealthCheck: s.manifest.BackendServiceConfig.ImageConfig.HealthCheckOpts(), - LogConfig: convertLogging(s.manifest.Logging), - DockerLabels: s.manifest.ImageConfig.DockerLabels, - DesiredCountLambda: desiredCountLambda.String(), - EnvControllerLambda: envControllerLambda.String(), - Storage: storage, - Network: convertNetworkConfig(s.manifest.Network), - EntryPoint: entrypoint, - Command: command, + Variables: s.manifest.BackendServiceConfig.Variables, + Secrets: s.manifest.BackendServiceConfig.Secrets, + NestedStack: outputs, + Sidecars: sidecars, + Autoscaling: autoscaling, + CapacityProviders: capacityProviders, + DesiredCountOnSpot: desiredCountOnSpot, + ExecuteCommand: convertExecuteCommand(&s.manifest.ExecuteCommand), + WorkloadType: manifest.BackendServiceType, + HealthCheck: s.manifest.BackendServiceConfig.ImageConfig.HealthCheckOpts(), + LogConfig: convertLogging(s.manifest.Logging), + DockerLabels: s.manifest.ImageConfig.DockerLabels, + DesiredCountLambda: desiredCountLambda.String(), + EnvControllerLambda: envControllerLambda.String(), + Storage: storage, + Network: convertNetworkConfig(s.manifest.Network), + EntryPoint: entrypoint, + Command: command, + LegacyServiceDiscovery: s.rc.LegacyServiceDiscovery, }) if err != nil { return "", fmt.Errorf("parse backend service template: %w", err) diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index 9e7bb374cb6..7ab8fe6ec6e 100644 --- a/internal/pkg/deploy/cloudformation/stack/env.go +++ b/internal/pkg/deploy/cloudformation/stack/env.go @@ -36,6 +36,9 @@ const ( envParamAppDNSKey = "AppDNSName" envParamAppDNSDelegationRoleKey = "AppDNSDelegationRole" EnvParamAliasesKey = "Aliases" + EnvParamLegacyServiceDiscovery = "UseLegacyServiceDiscoveryIfBlank" + + EnvOutputLegacyServiceDiscovery = "UseLegacyServiceDiscovery" // Output keys. EnvOutputVPCID = "VpcId" @@ -48,6 +51,9 @@ const ( DefaultVPCCIDR = "10.0.0.0/16" DefaultPublicSubnetCIDRs = "10.0.0.0/24,10.0.1.0/24" DefaultPrivateSubnetCIDRs = "10.0.2.0/24,10.0.3.0/24" + + // Placeholder parameter value to keep legacy svc discovery from being created + DoNotCreateLegacySvcDiscovery = "doNotCreate" ) // NewEnvStackConfig sets up a struct which can provide values to CloudFormation for @@ -128,6 +134,10 @@ func (e *EnvStackConfig) Parameters() ([]*cloudformation.Parameter, error) { ParameterKey: aws.String(envParamAppDNSDelegationRoleKey), ParameterValue: aws.String(e.dnsDelegationRole()), }, + { + ParameterKey: aws.String(EnvParamLegacyServiceDiscovery), + ParameterValue: aws.String(DoNotCreateLegacySvcDiscovery), + }, }, nil } @@ -163,12 +173,10 @@ func (e *EnvStackConfig) ToEnv(stack *cloudformation.Stack) (*config.Environment if err != nil { return nil, fmt.Errorf("couldn't extract region and account from stack ID %s: %w", *stack.StackId, err) } - stackOutputs := make(map[string]string) for _, output := range stack.Outputs { stackOutputs[*output.OutputKey] = *output.OutputValue } - return &config.Environment{ Name: e.in.Name, App: e.in.AppName, diff --git a/internal/pkg/deploy/cloudformation/stack/env_test.go b/internal/pkg/deploy/cloudformation/stack/env_test.go index a880808163e..d2dd5e93fc4 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_test.go @@ -102,6 +102,10 @@ func TestEnv_Parameters(t *testing.T) { ParameterKey: aws.String(envParamAppDNSDelegationRoleKey), ParameterValue: aws.String(""), }, + { + ParameterKey: aws.String(EnvParamLegacyServiceDiscovery), + ParameterValue: aws.String(DoNotCreateLegacySvcDiscovery), + }, }, }, "with DNS": { @@ -127,6 +131,10 @@ func TestEnv_Parameters(t *testing.T) { ParameterKey: aws.String(envParamAppDNSDelegationRoleKey), ParameterValue: aws.String("arn:aws:iam::000000000:role/project-DNSDelegationRole"), }, + { + ParameterKey: aws.String(EnvParamLegacyServiceDiscovery), + ParameterValue: aws.String(DoNotCreateLegacySvcDiscovery), + }, }, }, } @@ -254,6 +262,21 @@ func TestToEnv(t *testing.T) { ExecutionRoleARN: "arn:aws:iam::902697171733:role/phonetool-test-CFNExecutionRole", }, }, + "should return a well formed legacy environment": { + mockStack: mockLegacyEnvironmentStack( + "arn:aws:cloudformation:eu-west-3:902697171733:stack/project-env", + "arn:aws:iam::902697171733:role/phonetool-test-EnvManagerRole", + "arn:aws:iam::902697171733:role/phonetool-test-CFNExecutionRole"), + expectedEnv: config.Environment{ + Name: mockDeployInput.Name, + App: mockDeployInput.AppName, + Prod: mockDeployInput.Prod, + AccountID: "902697171733", + Region: "eu-west-3", + ManagerRoleARN: "arn:aws:iam::902697171733:role/phonetool-test-EnvManagerRole", + ExecutionRoleARN: "arn:aws:iam::902697171733:role/phonetool-test-CFNExecutionRole", + }, + }, } for name, tc := range testCases { @@ -286,9 +309,21 @@ func mockEnvironmentStack(stackArn, managerRoleARN, executionRoleARN string) *cl OutputValue: aws.String(executionRoleARN), }, }, + Parameters: []*cloudformation.Parameter{ + { + ParameterKey: aws.String(EnvParamLegacyServiceDiscovery), + ParameterValue: aws.String(DoNotCreateLegacySvcDiscovery), + }, + }, } } +func mockLegacyEnvironmentStack(stackArn, managerRoleARN, executionRoleARN string) *cloudformation.Stack { + stack := mockEnvironmentStack(stackArn, managerRoleARN, executionRoleARN) + stack.Parameters = nil + return stack +} + func mockDeployEnvironmentInput() *deploy.CreateEnvironmentInput { return &deploy.CreateEnvironmentInput{ Name: "env", diff --git a/internal/pkg/deploy/cloudformation/stack/lb_web_service_integration_test.go b/internal/pkg/deploy/cloudformation/stack/lb_web_service_integration_test.go index 0218272c5f5..5db7f409414 100644 --- a/internal/pkg/deploy/cloudformation/stack/lb_web_service_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/lb_web_service_integration_test.go @@ -32,11 +32,13 @@ func TestLoadBalancedWebService_Template(t *testing.T) { envName string svcStackPath string svcParamsPath string + legacySvcDisc bool }{ "default env": { envName: "test", svcStackPath: "svc-test.stack.yml", svcParamsPath: "svc-test.params.json", + legacySvcDisc: true, }, "staging env": { envName: "staging", @@ -59,7 +61,7 @@ func TestLoadBalancedWebService_Template(t *testing.T) { for name, tc := range testCases { - serializer, err := stack.NewHTTPSLoadBalancedWebService(v, tc.envName, appName, stack.RuntimeConfig{}) + serializer, err := stack.NewHTTPSLoadBalancedWebService(v, tc.envName, appName, stack.RuntimeConfig{LegacyServiceDiscovery: tc.legacySvcDisc}) tpl, err := serializer.Template() require.NoError(t, err, "template should render") diff --git a/internal/pkg/deploy/cloudformation/stack/lb_web_svc.go b/internal/pkg/deploy/cloudformation/stack/lb_web_svc.go index 4d306fc7e39..772a3285605 100644 --- a/internal/pkg/deploy/cloudformation/stack/lb_web_svc.go +++ b/internal/pkg/deploy/cloudformation/stack/lb_web_svc.go @@ -147,27 +147,28 @@ func (s *LoadBalancedWebService) Template() (string, error) { } } content, err := s.parser.ParseLoadBalancedWebService(template.WorkloadOpts{ - Variables: s.manifest.Variables, - Secrets: s.manifest.Secrets, - Aliases: aliases, - NestedStack: outputs, - Sidecars: sidecars, - LogConfig: convertLogging(s.manifest.Logging), - DockerLabels: s.manifest.ImageConfig.DockerLabels, - Autoscaling: autoscaling, - CapacityProviders: capacityProviders, - DesiredCountOnSpot: desiredCountOnSpot, - ExecuteCommand: convertExecuteCommand(&s.manifest.ExecuteCommand), - WorkloadType: manifest.LoadBalancedWebServiceType, - HTTPHealthCheck: convertHTTPHealthCheck(&s.manifest.HealthCheck), - AllowedSourceIps: s.manifest.AllowedSourceIps, - RulePriorityLambda: rulePriorityLambda.String(), - DesiredCountLambda: desiredCountLambda.String(), - EnvControllerLambda: envControllerLambda.String(), - Storage: storage, - Network: convertNetworkConfig(s.manifest.Network), - EntryPoint: entrypoint, - Command: command, + Variables: s.manifest.Variables, + Secrets: s.manifest.Secrets, + Aliases: aliases, + NestedStack: outputs, + Sidecars: sidecars, + LogConfig: convertLogging(s.manifest.Logging), + DockerLabels: s.manifest.ImageConfig.DockerLabels, + Autoscaling: autoscaling, + CapacityProviders: capacityProviders, + DesiredCountOnSpot: desiredCountOnSpot, + ExecuteCommand: convertExecuteCommand(&s.manifest.ExecuteCommand), + WorkloadType: manifest.LoadBalancedWebServiceType, + HTTPHealthCheck: convertHTTPHealthCheck(&s.manifest.HealthCheck), + AllowedSourceIps: s.manifest.AllowedSourceIps, + RulePriorityLambda: rulePriorityLambda.String(), + DesiredCountLambda: desiredCountLambda.String(), + EnvControllerLambda: envControllerLambda.String(), + Storage: storage, + Network: convertNetworkConfig(s.manifest.Network), + EntryPoint: entrypoint, + Command: command, + LegacyServiceDiscovery: s.rc.LegacyServiceDiscovery, }) if err != nil { return "", err diff --git a/internal/pkg/deploy/cloudformation/stack/scheduled_job.go b/internal/pkg/deploy/cloudformation/stack/scheduled_job.go index d7ebb153f6a..51591fe1af1 100644 --- a/internal/pkg/deploy/cloudformation/stack/scheduled_job.go +++ b/internal/pkg/deploy/cloudformation/stack/scheduled_job.go @@ -159,18 +159,19 @@ func (j *ScheduledJob) Template() (string, error) { return "", fmt.Errorf(`convert 'command' to string slice: %w`, err) } content, err := j.parser.ParseScheduledJob(template.WorkloadOpts{ - Variables: j.manifest.Variables, - Secrets: j.manifest.Secrets, - NestedStack: outputs, - Sidecars: sidecars, - ScheduleExpression: schedule, - StateMachine: stateMachine, - LogConfig: convertLogging(j.manifest.Logging), - DockerLabels: j.manifest.ImageConfig.DockerLabels, - Storage: storage, - Network: convertNetworkConfig(j.manifest.Network), - EntryPoint: entrypoint, - Command: command, + Variables: j.manifest.Variables, + Secrets: j.manifest.Secrets, + NestedStack: outputs, + Sidecars: sidecars, + ScheduleExpression: schedule, + StateMachine: stateMachine, + LogConfig: convertLogging(j.manifest.Logging), + DockerLabels: j.manifest.ImageConfig.DockerLabels, + Storage: storage, + Network: convertNetworkConfig(j.manifest.Network), + EntryPoint: entrypoint, + Command: command, + LegacyServiceDiscovery: j.rc.LegacyServiceDiscovery, EnvControllerLambda: envControllerLambda.String(), }) diff --git a/internal/pkg/deploy/cloudformation/stack/scheduled_job_integration_test.go b/internal/pkg/deploy/cloudformation/stack/scheduled_job_integration_test.go index c3c3ed6877b..b94f1f2d6e9 100644 --- a/internal/pkg/deploy/cloudformation/stack/scheduled_job_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/scheduled_job_integration_test.go @@ -36,7 +36,7 @@ func TestScheduledJob_Template(t *testing.T) { require.NoError(t, err) v, ok := mft.(*manifest.ScheduledJob) require.True(t, ok) - serializer, err := stack.NewScheduledJob(v, envName, appName, stack.RuntimeConfig{}) + serializer, err := stack.NewScheduledJob(v, envName, appName, stack.RuntimeConfig{LegacyServiceDiscovery: false}) tpl, err := serializer.Template() require.NoError(t, err, "template should render") diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/workloads/job-test.stack.yml b/internal/pkg/deploy/cloudformation/stack/testdata/workloads/job-test.stack.yml index 6d064b3db11..d7ea79a6d09 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/workloads/job-test.stack.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/workloads/job-test.stack.yml @@ -129,7 +129,7 @@ Resources: - Name: COPILOT_APPLICATION_NAME Value: !Sub '${AppName}' - Name: COPILOT_SERVICE_DISCOVERY_ENDPOINT - Value: !Sub '${AppName}.local' + Value: !Sub '${EnvName}.${AppName}.local' - Name: COPILOT_ENVIRONMENT_NAME Value: !Sub '${EnvName}' - Name: COPILOT_SERVICE_NAME diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/workloads/svc-prod.stack.yml b/internal/pkg/deploy/cloudformation/stack/testdata/workloads/svc-prod.stack.yml index a8f3d050227..7960ce938e1 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/workloads/svc-prod.stack.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/workloads/svc-prod.stack.yml @@ -81,7 +81,7 @@ Resources: - Name: COPILOT_APPLICATION_NAME Value: !Sub '${AppName}' - Name: COPILOT_SERVICE_DISCOVERY_ENDPOINT - Value: !Sub '${AppName}.local' + Value: !Sub '${EnvName}.${AppName}.local' - Name: COPILOT_ENVIRONMENT_NAME Value: !Sub '${EnvName}' - Name: COPILOT_SERVICE_NAME @@ -197,7 +197,7 @@ Resources: Name: !Ref WorkloadName NamespaceId: Fn::ImportValue: - !Sub '${AppName}-${EnvName}-ServiceDiscoveryNamespaceID' + !Sub '${AppName}-${EnvName}-ServiceDiscoveryNamespaceIDWithEnv' DynamicDesiredCountAction: Type: Custom::DynamicDesiredCountFunction diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/workloads/svc-staging.stack.yml b/internal/pkg/deploy/cloudformation/stack/testdata/workloads/svc-staging.stack.yml index f0636baab57..47adbeefdd2 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/workloads/svc-staging.stack.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/workloads/svc-staging.stack.yml @@ -81,7 +81,7 @@ Resources: - Name: COPILOT_APPLICATION_NAME Value: !Sub '${AppName}' - Name: COPILOT_SERVICE_DISCOVERY_ENDPOINT - Value: !Sub '${AppName}.local' + Value: !Sub '${EnvName}.${AppName}.local' - Name: COPILOT_ENVIRONMENT_NAME Value: !Sub '${EnvName}' - Name: COPILOT_SERVICE_NAME @@ -197,7 +197,7 @@ Resources: Name: !Ref WorkloadName NamespaceId: Fn::ImportValue: - !Sub '${AppName}-${EnvName}-ServiceDiscoveryNamespaceID' + !Sub '${AppName}-${EnvName}-ServiceDiscoveryNamespaceIDWithEnv' EnvControllerAction: diff --git a/internal/pkg/deploy/cloudformation/stack/workload.go b/internal/pkg/deploy/cloudformation/stack/workload.go index ebff17a678c..5b06d93f881 100644 --- a/internal/pkg/deploy/cloudformation/stack/workload.go +++ b/internal/pkg/deploy/cloudformation/stack/workload.go @@ -68,9 +68,10 @@ const maxDockerContainerPathLength = 242 // RuntimeConfig represents configuration that's defined outside of the manifest file // that is needed to create a CloudFormation stack. type RuntimeConfig struct { - Image *ECRImage // Optional. Image location in an ECR repository. - AddonsTemplateURL string // Optional. S3 object URL for the addons template. - AdditionalTags map[string]string // AdditionalTags are labels applied to resources in the workload stack. + Image *ECRImage // Optional. Image location in an ECR repository. + AddonsTemplateURL string // Optional. S3 object URL for the addons template. + AdditionalTags map[string]string // AdditionalTags are labels applied to resources in the workload stack. + LegacyServiceDiscovery bool // LegacyServiceDiscovery determines whether to register both legacy and new cloudmap namespaces. } // ECRImage represents configuration about the pushed ECR image that is needed to diff --git a/internal/pkg/deploy/env.go b/internal/pkg/deploy/env.go index d27c9be09f2..9cb472446f0 100644 --- a/internal/pkg/deploy/env.go +++ b/internal/pkg/deploy/env.go @@ -13,7 +13,7 @@ const ( // LegacyEnvTemplateVersion is the version associated with the environment template before we started versioning. LegacyEnvTemplateVersion = "v0.0.0" // LatestEnvTemplateVersion is the latest version number available for environment templates. - LatestEnvTemplateVersion = "v1.4.0" + LatestEnvTemplateVersion = "v1.5.0" ) // CreateEnvironmentInput holds the fields required to deploy an environment. diff --git a/internal/pkg/describe/mocks/mock_describe.go b/internal/pkg/describe/mocks/mock_describe.go index d09164ad7df..18e5d5321c9 100644 --- a/internal/pkg/describe/mocks/mock_describe.go +++ b/internal/pkg/describe/mocks/mock_describe.go @@ -7,6 +7,7 @@ package mocks import ( reflect "reflect" + stack "github.com/aws/copilot-cli/internal/pkg/describe/stack" gomock "github.com/golang/mock/gomock" ) @@ -61,3 +62,86 @@ func (mr *MockHumanJSONStringerMockRecorder) JSONString() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONString", reflect.TypeOf((*MockHumanJSONStringer)(nil).JSONString)) } + +// MockstackDescriber is a mock of stackDescriber interface. +type MockstackDescriber struct { + ctrl *gomock.Controller + recorder *MockstackDescriberMockRecorder +} + +// MockstackDescriberMockRecorder is the mock recorder for MockstackDescriber. +type MockstackDescriberMockRecorder struct { + mock *MockstackDescriber +} + +// NewMockstackDescriber creates a new mock instance. +func NewMockstackDescriber(ctrl *gomock.Controller) *MockstackDescriber { + mock := &MockstackDescriber{ctrl: ctrl} + mock.recorder = &MockstackDescriberMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockstackDescriber) EXPECT() *MockstackDescriberMockRecorder { + return m.recorder +} + +// Describe mocks base method. +func (m *MockstackDescriber) Describe() (stack.StackDescription, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Describe") + ret0, _ := ret[0].(stack.StackDescription) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Describe indicates an expected call of Describe. +func (mr *MockstackDescriberMockRecorder) Describe() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockstackDescriber)(nil).Describe)) +} + +// Resources mocks base method. +func (m *MockstackDescriber) Resources() ([]*stack.Resource, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Resources") + ret0, _ := ret[0].([]*stack.Resource) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Resources indicates an expected call of Resources. +func (mr *MockstackDescriberMockRecorder) Resources() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resources", reflect.TypeOf((*MockstackDescriber)(nil).Resources)) +} + +// StackMetadata mocks base method. +func (m *MockstackDescriber) StackMetadata() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StackMetadata") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StackMetadata indicates an expected call of StackMetadata. +func (mr *MockstackDescriberMockRecorder) StackMetadata() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackMetadata", reflect.TypeOf((*MockstackDescriber)(nil).StackMetadata)) +} + +// StackSetMetadata mocks base method. +func (m *MockstackDescriber) StackSetMetadata() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StackSetMetadata") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StackSetMetadata indicates an expected call of StackSetMetadata. +func (mr *MockstackDescriberMockRecorder) StackSetMetadata() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackSetMetadata", reflect.TypeOf((*MockstackDescriber)(nil).StackSetMetadata)) +} diff --git a/internal/pkg/describe/mocks/mock_service.go b/internal/pkg/describe/mocks/mock_service.go index 48cf19ed941..9d2d63d05e1 100644 --- a/internal/pkg/describe/mocks/mock_service.go +++ b/internal/pkg/describe/mocks/mock_service.go @@ -286,89 +286,6 @@ func (mr *MockecsSvcDescriberMockRecorder) ServiceStackResources() *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceStackResources", reflect.TypeOf((*MockecsSvcDescriber)(nil).ServiceStackResources)) } -// MockstackDescriber is a mock of stackDescriber interface. -type MockstackDescriber struct { - ctrl *gomock.Controller - recorder *MockstackDescriberMockRecorder -} - -// MockstackDescriberMockRecorder is the mock recorder for MockstackDescriber. -type MockstackDescriberMockRecorder struct { - mock *MockstackDescriber -} - -// NewMockstackDescriber creates a new mock instance. -func NewMockstackDescriber(ctrl *gomock.Controller) *MockstackDescriber { - mock := &MockstackDescriber{ctrl: ctrl} - mock.recorder = &MockstackDescriberMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockstackDescriber) EXPECT() *MockstackDescriberMockRecorder { - return m.recorder -} - -// Describe mocks base method. -func (m *MockstackDescriber) Describe() (stack.StackDescription, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Describe") - ret0, _ := ret[0].(stack.StackDescription) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Describe indicates an expected call of Describe. -func (mr *MockstackDescriberMockRecorder) Describe() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockstackDescriber)(nil).Describe)) -} - -// Resources mocks base method. -func (m *MockstackDescriber) Resources() ([]*stack.Resource, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Resources") - ret0, _ := ret[0].([]*stack.Resource) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Resources indicates an expected call of Resources. -func (mr *MockstackDescriberMockRecorder) Resources() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resources", reflect.TypeOf((*MockstackDescriber)(nil).Resources)) -} - -// StackMetadata mocks base method. -func (m *MockstackDescriber) StackMetadata() (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StackMetadata") - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// StackMetadata indicates an expected call of StackMetadata. -func (mr *MockstackDescriberMockRecorder) StackMetadata() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackMetadata", reflect.TypeOf((*MockstackDescriber)(nil).StackMetadata)) -} - -// StackSetMetadata mocks base method. -func (m *MockstackDescriber) StackSetMetadata() (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StackSetMetadata") - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// StackSetMetadata indicates an expected call of StackSetMetadata. -func (mr *MockstackDescriberMockRecorder) StackSetMetadata() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackSetMetadata", reflect.TypeOf((*MockstackDescriber)(nil).StackSetMetadata)) -} - // MockConfigStoreSvc is a mock of ConfigStoreSvc interface. type MockConfigStoreSvc struct { ctrl *gomock.Controller diff --git a/internal/pkg/template/workload.go b/internal/pkg/template/workload.go index 632c710cce5..5d5bdb5ff27 100644 --- a/internal/pkg/template/workload.go +++ b/internal/pkg/template/workload.go @@ -239,13 +239,14 @@ type WorkloadOpts struct { DependsOn map[string]string // Additional options for service templates. - WorkloadType string - HealthCheck *ecs.HealthCheck - HTTPHealthCheck HTTPHealthCheckOpts - AllowedSourceIps []string - RulePriorityLambda string - DesiredCountLambda string - EnvControllerLambda string + WorkloadType string + HealthCheck *ecs.HealthCheck + HTTPHealthCheck HTTPHealthCheckOpts + AllowedSourceIps []string + RulePriorityLambda string + DesiredCountLambda string + EnvControllerLambda string + LegacyServiceDiscovery bool // Additional options for job templates. ScheduleExpression string diff --git a/site/content/docs/concepts/services.en.md b/site/content/docs/concepts/services.en.md index b70415f9387..58c99f54f84 100644 --- a/site/content/docs/concepts/services.en.md +++ b/site/content/docs/concepts/services.en.md @@ -134,7 +134,7 @@ Routes Service Discovery Environment Namespace - test front-end.my-app.local:8080 + test front-end.test.my-app.local:8080 Variables @@ -142,7 +142,7 @@ Variables COPILOT_APPLICATION_NAME test my-app COPILOT_ENVIRONMENT_NAME test test COPILOT_LB_DNS test my-ap-Publi-1RV8QEBNTEQCW-1762184596.ca-central-1.elb.amazonaws.com - COPILOT_SERVICE_DISCOVERY_ENDPOINT test my-app.local + COPILOT_SERVICE_DISCOVERY_ENDPOINT test test.my-app.local COPILOT_SERVICE_NAME test front-end ``` diff --git a/site/content/docs/concepts/services.ja.md b/site/content/docs/concepts/services.ja.md index 9df60944898..95bacfb79e8 100644 --- a/site/content/docs/concepts/services.ja.md +++ b/site/content/docs/concepts/services.ja.md @@ -137,7 +137,7 @@ Routes Service Discovery Environment Namespace - test front-end.my-app.local:8080 + test front-end.test.my-app.local:8080 Variables @@ -145,7 +145,7 @@ Variables COPILOT_APPLICATION_NAME test my-app COPILOT_ENVIRONMENT_NAME test test COPILOT_LB_DNS test my-ap-Publi-1RV8QEBNTEQCW-1762184596.ca-central-1.elb.amazonaws.com - COPILOT_SERVICE_DISCOVERY_ENDPOINT test my-app.local + COPILOT_SERVICE_DISCOVERY_ENDPOINT test test.my-app.local COPILOT_SERVICE_NAME test front-end ``` diff --git a/site/content/docs/developing/environment-variables.en.md b/site/content/docs/developing/environment-variables.en.md index 0ea9dc7441b..92c63eea5c3 100644 --- a/site/content/docs/developing/environment-variables.en.md +++ b/site/content/docs/developing/environment-variables.en.md @@ -27,7 +27,7 @@ By default, the AWS Copilot CLI passes in some default environment variables for * `COPILOT_ENVIRONMENT_NAME` - this is the name of the environment the service is running in (test vs prod, for example) * `COPILOT_SERVICE_NAME` - this is the name of the current service. * `COPILOT_LB_DNS` - this is the DNS name of the Load Balancer (if it exists) such as _kudos-Publi-MC2WNHAIOAVS-588300247.us-west-2.elb.amazonaws.com_. Note: if you're using a custom domain name, this value will still be the Load Balancer's DNS name. -* `COPILOT_SERVICE_DISCOVERY_ENDPOINT` - this is the endpoint to add after a service name to talk to another service in your environment via service discovery. The value is `{app name}.local`. For more information about service discovery, check out our [Service Discovery guide](../developing/service-discovery.en.md). +* `COPILOT_SERVICE_DISCOVERY_ENDPOINT` - this is the endpoint to add after a service name to talk to another service in your environment via service discovery. The value is `{env name}.{app name}.local`. For more information about service discovery, check out our [Service Discovery guide](../developing/service-discovery.en.md). ## How do I add my own Environment Variables? diff --git a/site/content/docs/developing/environment-variables.ja.md b/site/content/docs/developing/environment-variables.ja.md index ed51641c712..fb1276604f7 100644 --- a/site/content/docs/developing/environment-variables.ja.md +++ b/site/content/docs/developing/environment-variables.ja.md @@ -26,7 +26,7 @@ database_name = os.getenv('DATABASE_NAME') * `COPILOT_ENVIRONMENT_NAME` - Service が実行されている Environment 名(例: test、prod) * `COPILOT_SERVICE_NAME` - 現在の Service 名 * `COPILOT_LB_DNS` - (存在する場合)ロードバランサー名。例: _kudos-Publi-MC2WNHAIOAVS-588300247.us-west-2.elb.amazonaws.com_ 注: カスタムドメイン名を利用している場合でも、この値は ロードバランサーの DNS 名を保持します -* `COPILOT_SERVICE_DISCOVERY_ENDPOINT` - サービス検出を介して、Environment の中で他の Service と通信するために Service 名の後に追加されるエンドポイント。値は `{app name}.local` となります。サービスディスカバリについてのより詳しい情報は[サービス検出のガイド](../developing/service-discovery.ja.md) を参照してください +* `COPILOT_SERVICE_DISCOVERY_ENDPOINT` - サービス検出を介して、Environment の中で他の Service と通信するために Service 名の後に追加されるエンドポイント。値は `{env name}.{app name}.local` となります。サービスディスカバリについてのより詳しい情報は[サービス検出のガイド](../developing/service-discovery.ja.md) を参照してください ## 環境変数を追加する方法 環境変数を追加するのは簡単です。[Manifest](../manifest/overview.ja.md) の `variables` セクションに直接追加できます。 下記のスニペットでは、`LOG_LEVEL` という変数を `debug` という値で Service に渡しています。 diff --git a/site/content/docs/developing/service-discovery.en.md b/site/content/docs/developing/service-discovery.en.md index 3f5809cb427..be50e5fc0b9 100644 --- a/site/content/docs/developing/service-discovery.en.md +++ b/site/content/docs/developing/service-discovery.en.md @@ -9,13 +9,13 @@ Service Discovery is enabled for all services set up using the Copilot CLI. We'l !!! Attention Service Discovery is not supported for Request-Driven Web Services. -In this example we'll imagine our `front-end` service has a public endpoint and wants to call our `api` service using its service discovery endpoint. +In this example we'll imagine our `front-end` service is deployed in the `test` environment, has a public endpoint and wants to call our `api` service using its service discovery endpoint. ```go // Calling our api service from the front-end service using Service Discovery func ServiceDiscoveryGet(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { endpoint := fmt.Sprintf("http://api.%s/some-request", os.Getenv("COPILOT_SERVICE_DISCOVERY_ENDPOINT")) - resp, err := http.Get(endpoint /* http://api.kudos.local/some-request */) + resp, err := http.Get(endpoint /* http://api.test.kudos.local/some-request */) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -33,6 +33,12 @@ The important part is that our `front-end` service is making a request to our `a endpoint := fmt.Sprintf("http://api.%s/some-request", os.Getenv("COPILOT_SERVICE_DISCOVERY_ENDPOINT")) ``` -`COPILOT_SERVICE_DISCOVERY_ENDPOINT` is a special environment variable that the Copilot CLI sets for you when it creates your service. It's of the format _{app name}.local_ - so in this case in our _kudos_ app, the request would be to `http://api.kudos.local/some-request`. Since our _api_ service is running on port 80, we're not specifying the port in the URL. However, if it was running on another port, say 8080, we'd need to include the port in the request, as well `http://api.kudos.local:8080/some-request`. +`COPILOT_SERVICE_DISCOVERY_ENDPOINT` is a special environment variable that the Copilot CLI sets for you when it creates your service. It's of the format _{env name}.{app name}.local_ - so in this case in our _kudos_ app, when deployed in the _test_ environment, the request would be to `http://api.test.kudos.local/some-request`. Since our _api_ service is running on port 80, we're not specifying the port in the URL. However, if it was running on another port, say 8080, we'd need to include the port in the request, as well `http://api.kudos.local:8080/some-request`. -When our front-end makes this request, the endpoint `api.kudos.local` resolves to a private IP address and is routed privately within your VPC. +When our front-end makes this request, the endpoint `api.test.kudos.local` resolves to a private IP address and is routed privately within your VPC. + +## Legacy Environments and Service Discovery + +Prior to Copilot v1.8.0 and environment version 1.5.0, the service discovery namespace used the format _{app name}.local_, without including the environment. This limitation made it impossible to deploy multiple environments in the same VPC. Any environments created with Copilot v1.8.0 and newer can share a VPC with any other environment. + +When your environments are upgraded, Copilot will honor the service discovery namespace that the environment was created with. That means that the endpoint your services are reachable at will not change. Any new environments created with Copilot v1.8.0 and above will use the _{env name}.{app name}.local_ format for service discovery, and can share VPCs with older environments. \ No newline at end of file diff --git a/site/content/docs/developing/service-discovery.ja.md b/site/content/docs/developing/service-discovery.ja.md index 0605be9c98a..7875d5a4682 100644 --- a/site/content/docs/developing/service-discovery.ja.md +++ b/site/content/docs/developing/service-discovery.ja.md @@ -16,7 +16,7 @@ // サービス検出を使って front-end Service から api Service を呼び出す func ServiceDiscoveryGet(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { endpoint := fmt.Sprintf("http://api.%s/some-request", os.Getenv("COPILOT_SERVICE_DISCOVERY_ENDPOINT")) - resp, err := http.Get(endpoint /* http://api.kudos.local/some-request */) + resp, err := http.Get(endpoint /* http://api.test.kudos.local/some-request */) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -34,6 +34,6 @@ func ServiceDiscoveryGet(w http.ResponseWriter, req *http.Request, ps httprouter endpoint := fmt.Sprintf("http://api.%s/some-request", os.Getenv("COPILOT_SERVICE_DISCOVERY_ENDPOINT")) ``` -`COPILOT_SERVICE_DISCOVERY_ENDPOINT` は特別な環境変数で Copilot CLI は Service 作成時にこの環境変数を設定します。これは _{app name}.local_ というフォーマットで登録されており、今回の例だと _kudos_ Application の場合、リクエストは `http://api.kudos.local/some-request` に送信されます。 _api_ Service は 80 番ポートで動いているので、 URL のなかでポートを指定していません。しかし Service が例えば 8080 番のような別のポートで動いている場合はリクエストの中にポート番号を含める必要があります。今回の例だと `http://api.kudos.local:8080/some-request` のようになります。 +`COPILOT_SERVICE_DISCOVERY_ENDPOINT` は特別な環境変数で Copilot CLI は Service 作成時にこの環境変数を設定します。これは _{env name}.{app name}.local_ というフォーマットで登録されており、今回の例だと _kudos_ Application の場合、リクエストは `http://api.test.kudos.local/some-request` に送信されます。 _api_ Service は 80 番ポートで動いているので、 URL のなかでポートを指定していません。しかし Service が例えば 8080 番のような別のポートで動いている場合はリクエストの中にポート番号を含める必要があります。今回の例だと `http://api.test.kudos.local:8080/some-request` のようになります。 -`front-end` Service がリクエストを送信するとき `api.kudos.local` というエンドポイントはプライベート IP アドレスに変換され VPC のなかでプライベートにルーティングされます。 +`front-end` Service がリクエストを送信するとき `api.test.kudos.local` というエンドポイントはプライベート IP アドレスに変換され VPC のなかでプライベートにルーティングされます。 diff --git a/templates/environment/versions/cf-v1.5.0.yml b/templates/environment/versions/cf-v1.5.0.yml new file mode 100644 index 00000000000..bf3e8f62805 --- /dev/null +++ b/templates/environment/versions/cf-v1.5.0.yml @@ -0,0 +1,404 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +Description: CloudFormation environment template for infrastructure shared among Copilot workloads. +Metadata: + Version: 'v1.5.0' +Parameters: + AppName: + Type: String + EnvironmentName: + Type: String + ALBWorkloads: + Type: String + Default: "" + EFSWorkloads: + Type: String + Default: "" + NATWorkloads: + Type: String + Default: "" + ToolsAccountPrincipalARN: + Type: String + AppDNSName: + Type: String + Default: "" + AppDNSDelegationRole: + Type: String + Default: "" + Aliases: + Type: String + Default: "" + UseLegacyServiceDiscoveryIfBlank: + Type: String + Default: "" +Conditions: + CreateALB: + !Not [!Equals [ !Ref ALBWorkloads, "" ]] + DelegateDNS: + !Not [!Equals [ !Ref AppDNSName, "" ]] + ExportHTTPSListener: !And + - !Condition DelegateDNS + - !Condition CreateALB + CreateEFS: + !Not [!Equals [ !Ref EFSWorkloads, ""]] + CreateNATGateways: + !Not [!Equals [ !Ref NATWorkloads, ""]] + HasAliases: + !Not [!Equals [ !Ref Aliases, "" ]] + UseLegacyServiceDiscovery: + !Equals [ !Ref UseLegacyServiceDiscoveryIfBlank, "" ] +Resources: +{{- if not .ImportVPC}} +{{include "vpc-resources" .VPCConfig | indent 2}} +{{include "nat-gateways" .VPCConfig | indent 2}} +{{- end}} + # Creates a service discovery namespace with the form: + # {svc}.{appname}.local + # This is a legacy construct and only used in environments created before v1.5.0. + ServiceDiscoveryNamespace: + Condition: UseLegacyServiceDiscovery + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Properties: + Name: !Sub ${AppName}.local +{{- if .ImportVPC}} + Vpc: {{.ImportVPC.ID}} +{{- else}} + Vpc: !Ref VPC +{{- end}} + # Creates a service discovery namespace with the form: + # {svc}.{env}.{appname}.local + ServiceDiscoveryNamespaceWithEnv: + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Properties: + Name: !Sub ${EnvironmentName}.${AppName}.local +{{- if .ImportVPC}} + Vpc: {{.ImportVPC.ID}} +{{- else}} + Vpc: !Ref VPC +{{- end}} + Cluster: + Metadata: + 'aws:copilot:description': 'An ECS cluster to group your services' + Type: AWS::ECS::Cluster + Properties: + CapacityProviders: ['FARGATE', 'FARGATE_SPOT'] + Configuration: + ExecuteCommandConfiguration: + Logging: DEFAULT + PublicLoadBalancerSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your load balancer allowing HTTP and HTTPS traffic' + Condition: CreateALB + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to the public facing load balancer + SecurityGroupIngress: + - CidrIp: 0.0.0.0/0 + Description: Allow from anyone on port 80 + FromPort: 80 + IpProtocol: tcp + ToPort: 80 + - CidrIp: 0.0.0.0/0 + Description: Allow from anyone on port 443 + FromPort: 443 + IpProtocol: tcp + ToPort: 443 +{{- if .ImportVPC}} + VpcId: {{.ImportVPC.ID}} +{{- else}} + VpcId: !Ref VPC +{{- end}} + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-lb' + # Only accept requests coming from the public ALB or other containers in the same security group. + EnvironmentSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group to allow your containers to talk to each other' + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: !Join ['', [!Ref AppName, '-', !Ref EnvironmentName, EnvironmentSecurityGroup]] +{{- if .ImportVPC}} + VpcId: {{.ImportVPC.ID}} +{{- else}} + VpcId: !Ref VPC +{{- end}} + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-env' + EnvironmentSecurityGroupIngressFromPublicALB: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateALB + Properties: + Description: Ingress from the public ALB + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref PublicLoadBalancerSecurityGroup + EnvironmentSecurityGroupIngressFromSelf: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from other containers in the same security group + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + PublicLoadBalancer: + Metadata: + 'aws:copilot:description': 'An Application Load Balancer to distribute public traffic to your services' + Condition: CreateALB + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internet-facing + SecurityGroups: [ !GetAtt PublicLoadBalancerSecurityGroup.GroupId ] +{{- if .ImportVPC}} + Subnets: [ {{range $id := .ImportVPC.PublicSubnetIDs}}{{$id}}, {{end}} ] +{{- else}} + Subnets: [ {{range $ind, $cidr := .VPCConfig.PublicSubnetCIDRs}}!Ref PublicSubnet{{inc $ind}}, {{end}} ] +{{- end}} + Type: application + # Assign a dummy target group that with no real services as targets, so that we can create + # the listeners for the services. + DefaultHTTPTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Condition: CreateALB + Properties: + # Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds. + HealthCheckIntervalSeconds: 10 # Default is 30. + HealthyThresholdCount: 2 # Default is 5. + HealthCheckTimeoutSeconds: 5 + Port: 80 + Protocol: HTTP + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 # Default is 300. + TargetType: ip +{{- if .ImportVPC}} + VpcId: {{.ImportVPC.ID}} +{{- else}} + VpcId: !Ref VPC +{{- end}} + HTTPListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: CreateALB + Properties: + DefaultActions: + - TargetGroupArn: !Ref DefaultHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref PublicLoadBalancer + Port: 80 + Protocol: HTTP + HTTPSListener: + Type: AWS::ElasticLoadBalancingV2::Listener + DependsOn: HTTPSCert + Condition: ExportHTTPSListener + Properties: + Certificates: + - CertificateArn: !Ref HTTPSCert + DefaultActions: + - TargetGroupArn: !Ref DefaultHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref PublicLoadBalancer + Port: 443 + Protocol: HTTPS + FileSystem: + Condition: CreateEFS + Type: AWS::EFS::FileSystem + Metadata: + 'aws:copilot:description': 'An EFS filesystem for persistent task storage' + Properties: + BackupPolicy: + Status: ENABLED + Encrypted: true + FileSystemPolicy: + Version: 2012-10-17 + Id: CopilotEFSPolicy + Statement: + - Sid: AllowIAMFromTaggedRoles + Effect: Allow + Principal: + AWS: '*' + Action: + - elasticfilesystem:ClientWrite + - elasticfilesystem:ClientMount + Condition: + Bool: + 'elasticfilesystem:AccessedViaMountTarget': true + StringEquals: + 'iam:ResourceTag/copilot-application': !Sub '${AppName}' + 'iam:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' + - Sid: DenyUnencryptedAccess + Effect: Deny + Principal: '*' + Action: 'elasticfilesystem:*' + Condition: + Bool: + 'aws:SecureTransport': false + LifecyclePolicies: + - TransitionToIA: AFTER_30_DAYS + PerformanceMode: generalPurpose + ThroughputMode: bursting + EFSSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group to allow your containers to talk to EFS storage' + Type: AWS::EC2::SecurityGroup + Condition: CreateEFS + Properties: + GroupDescription: !Join ['', [!Ref AppName, '-', !Ref EnvironmentName, EFSSecurityGroup]] +{{- if .ImportVPC}} + VpcId: {{.ImportVPC.ID}} +{{- else}} + VpcId: !Ref VPC +{{- end}} + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-efs' + EFSSecurityGroupIngressFromEnvironment: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateEFS + Properties: + Description: Ingress from containers in the Environment Security Group. + GroupId: !Ref EFSSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup +{{- if .ImportVPC}} +{{- range $ind, $id := .ImportVPC.PrivateSubnetIDs}} + MountTarget{{inc $ind}}: + Type: AWS::EFS::MountTarget + Condition: CreateEFS + Properties: + FileSystemId: !Ref FileSystem + SubnetId: {{$id}} + SecurityGroups: + - !Ref EFSSecurityGroup +{{- end}} +{{- else}} +{{- range $ind, $cidr := .VPCConfig.PrivateSubnetCIDRs}} + MountTarget{{inc $ind}}: + Type: AWS::EFS::MountTarget + Condition: CreateEFS + Properties: + FileSystemId: !Ref FileSystem + SubnetId: !Ref PrivateSubnet{{inc $ind}} + SecurityGroups: + - !Ref EFSSecurityGroup +{{- end}} +{{- end}} +{{include "cfn-execution-role" . | indent 2}} +{{include "environment-manager-role" . | indent 2}} +{{include "custom-resources-role" . | indent 2}} + EnvironmentHostedZone: + Type: "AWS::Route53::HostedZone" + Condition: DelegateDNS + Properties: + HostedZoneConfig: + Comment: !Sub "HostedZone for environment ${EnvironmentName} - ${EnvironmentName}.${AppName}.${AppDNSName}" + Name: !Sub ${EnvironmentName}.${AppName}.${AppDNSName} +{{include "lambdas" . | indent 2}} +{{include "custom-resources" . | indent 2}} +Outputs: + VpcId: +{{- if .ImportVPC}} + Value: {{.ImportVPC.ID}} +{{- else}} + Value: !Ref VPC +{{- end}} + Export: + Name: !Sub ${AWS::StackName}-VpcId + PublicSubnets: +{{- if .ImportVPC}} + Value: !Join [ ',', [ {{range $id := .ImportVPC.PublicSubnetIDs}}{{$id}}, {{end}}] ] +{{- else}} + Value: !Join [ ',', [ {{range $ind, $cidr := .VPCConfig.PublicSubnetCIDRs}}!Ref PublicSubnet{{inc $ind}}, {{end}}] ] +{{- end}} + Export: + Name: !Sub ${AWS::StackName}-PublicSubnets + PrivateSubnets: +{{- if .ImportVPC}} + Value: !Join [ ',', [ {{range $id := .ImportVPC.PrivateSubnetIDs}}{{$id}}, {{end}}] ] +{{- else}} + Value: !Join [ ',', [ {{range $ind, $cidr := .VPCConfig.PrivateSubnetCIDRs}}!Ref PrivateSubnet{{inc $ind}}, {{end}}] ] +{{- end}} + Export: + Name: !Sub ${AWS::StackName}-PrivateSubnets + ServiceDiscoveryNamespaceID: + Condition: UseLegacyServiceDiscovery + Value: !GetAtt ServiceDiscoveryNamespace.Id + Export: + Name: !Sub ${AWS::StackName}-ServiceDiscoveryNamespaceID + ServiceDiscoveryNamespaceIDWithEnv: + Value: !GetAtt ServiceDiscoveryNamespaceWithEnv.Id + Export: + Name: !Sub ${AWS::StackName}-ServiceDiscoveryNamespaceIDWithEnv + UseLegacyServiceDiscovery: + Value: !If [ UseLegacyServiceDiscovery, true, false ] + Export: + Name: !Sub ${AWS::StackName}-UseLegacyServiceDiscovery + EnvironmentSecurityGroup: + Value: !Ref EnvironmentSecurityGroup + Export: + Name: !Sub ${AWS::StackName}-EnvironmentSecurityGroup + PublicLoadBalancerDNSName: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.DNSName + Export: + Name: !Sub ${AWS::StackName}-PublicLoadBalancerDNS + PublicLoadBalancerFullName: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.LoadBalancerFullName + Export: + Name: !Sub ${AWS::StackName}-PublicLoadBalancerFullName + PublicLoadBalancerHostedZone: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.CanonicalHostedZoneID + Export: + Name: !Sub ${AWS::StackName}-CanonicalHostedZoneID + HTTPListenerArn: + Condition: CreateALB + Value: !Ref HTTPListener + Export: + Name: !Sub ${AWS::StackName}-HTTPListenerArn + HTTPSListenerArn: + Condition: ExportHTTPSListener + Value: !Ref HTTPSListener + Export: + Name: !Sub ${AWS::StackName}-HTTPSListenerArn + DefaultHTTPTargetGroupArn: + Condition: CreateALB + Value: !Ref DefaultHTTPTargetGroup + Export: + Name: !Sub ${AWS::StackName}-DefaultHTTPTargetGroup + ClusterId: + Value: !Ref Cluster + Export: + Name: !Sub ${AWS::StackName}-ClusterId + EnvironmentManagerRoleARN: + Value: !GetAtt EnvironmentManagerRole.Arn + Description: The role to be assumed by the ecs-cli to manage environments. + Export: + Name: !Sub ${AWS::StackName}-EnvironmentManagerRoleARN + CFNExecutionRoleARN: + Value: !GetAtt CloudformationExecutionRole.Arn + Description: The role to be assumed by the Cloudformation service when it deploys application infrastructure. + Export: + Name: !Sub ${AWS::StackName}-CFNExecutionRoleARN + EnvironmentHostedZone: + Condition: DelegateDNS + Value: !Ref EnvironmentHostedZone + Description: The HostedZone for this environment's private DNS. + Export: + Name: !Sub ${AWS::StackName}-HostedZone + EnvironmentSubdomain: + Condition: DelegateDNS + Value: !Sub ${EnvironmentName}.${AppName}.${AppDNSName} + Description: The domain name of this environment. + Export: + Name: !Sub ${AWS::StackName}-SubDomain + EnabledFeatures: + # We don't need to include Aliases because updating it always results in the CustomDomain action to update. + Value: !Sub '${ALBWorkloads},${EFSWorkloads},${NATWorkloads}' + Description: Required output to force the stack to update if mutating feature params, like ALBWorkloads, does not change the template. + ManagedFileSystemID: + Condition: CreateEFS + Value: !Ref FileSystem + Description: The ID of the Copilot-managed EFS filesystem. + Export: + Name: !Sub ${AWS::StackName}-FilesystemID diff --git a/templates/workloads/partials/cf/envvars.yml b/templates/workloads/partials/cf/envvars.yml index c12a151b603..e6925b80700 100644 --- a/templates/workloads/partials/cf/envvars.yml +++ b/templates/workloads/partials/cf/envvars.yml @@ -5,7 +5,11 @@ Environment: - Name: COPILOT_APPLICATION_NAME Value: !Sub '${AppName}' - Name: COPILOT_SERVICE_DISCOVERY_ENDPOINT +{{- if .LegacyServiceDiscovery}} Value: !Sub '${AppName}.local' +{{- else}} + Value: !Sub '${EnvName}.${AppName}.local' +{{- end}} - Name: COPILOT_ENVIRONMENT_NAME Value: !Sub '${EnvName}' - Name: COPILOT_SERVICE_NAME diff --git a/templates/workloads/partials/cf/servicediscovery.yml b/templates/workloads/partials/cf/servicediscovery.yml index b6987d09f9f..2ace8dc5449 100644 --- a/templates/workloads/partials/cf/servicediscovery.yml +++ b/templates/workloads/partials/cf/servicediscovery.yml @@ -16,4 +16,8 @@ DiscoveryService: Name: !Ref WorkloadName NamespaceId: Fn::ImportValue: + {{- if .LegacyServiceDiscovery}} !Sub '${AppName}-${EnvName}-ServiceDiscoveryNamespaceID' + {{- else}} + !Sub '${AppName}-${EnvName}-ServiceDiscoveryNamespaceIDWithEnv' + {{- end}} \ No newline at end of file diff --git a/templates/workloads/services/backend/cf.yml b/templates/workloads/services/backend/cf.yml index c49279519c5..23681e45216 100644 --- a/templates/workloads/services/backend/cf.yml +++ b/templates/workloads/services/backend/cf.yml @@ -105,7 +105,12 @@ Resources: Type: AWS::ECS::Service Properties: {{include "service-base-properties" . | indent 6}} - ServiceRegistries: !If [ExposePort, [{RegistryArn: !GetAtt DiscoveryService.Arn, Port: !Ref ContainerPort}], !Ref "AWS::NoValue"] + ServiceRegistries: + Fn::If: + - ExposePort + - - RegistryArn: !GetAtt DiscoveryService.Arn + Port: !Ref ContainerPort + - !Ref "AWS::NoValue" {{include "efs-access-point" . | indent 2}} diff --git a/templates/workloads/services/lb-web/cf.yml b/templates/workloads/services/lb-web/cf.yml index 94c9a6c2126..88d7af551ba 100644 --- a/templates/workloads/services/lb-web/cf.yml +++ b/templates/workloads/services/lb-web/cf.yml @@ -424,4 +424,4 @@ Outputs: Description: ARN of the Discovery Service. Value: !GetAtt DiscoveryService.Arn Export: - Name: !Sub ${AWS::StackName}-DiscoveryServiceARN + Name: !Sub ${AWS::StackName}-DiscoveryServiceARN \ No newline at end of file