diff --git a/internal/pkg/cli/svc_deploy.go b/internal/pkg/cli/svc_deploy.go index 711354064c4..85bee3233bd 100644 --- a/internal/pkg/cli/svc_deploy.go +++ b/internal/pkg/cli/svc_deploy.go @@ -488,7 +488,11 @@ func (o *deploySvcOpts) showSvcURI() error { } switch o.targetSvc.Type { case manifest.BackendServiceType: - log.Successf("Deployed %s, its service discovery endpoint is %s.\n", color.HighlightUserInput(o.name), color.HighlightResource(uri)) + msg := fmt.Sprintf("Deployed %s.\n", color.HighlightUserInput(o.name)) + if uri != describe.BlankServiceDiscoveryURI { + msg = fmt.Sprintf("Deployed %s, its service discovery endpoint is %s.\n", color.HighlightUserInput(o.name), color.HighlightResource(uri)) + } + log.Success(msg) default: log.Successf("Deployed %s, you can access it at %s.\n", color.HighlightUserInput(o.name), color.HighlightResource(uri)) } diff --git a/internal/pkg/cli/svc_init.go b/internal/pkg/cli/svc_init.go index 2d4e9507c6c..e3c900cd5c7 100644 --- a/internal/pkg/cli/svc_init.go +++ b/internal/pkg/cli/svc_init.go @@ -222,7 +222,7 @@ func (o *initSvcOpts) createManifest() (string, error) { manifestMsgFmt = "Manifest file for service %s already exists at %s, skipping writing it.\n" } log.Successf(manifestMsgFmt, color.HighlightUserInput(o.name), color.HighlightResource(manifestPath)) - log.Infoln(color.Help(fmt.Sprintf("Your manifest contains configurations like your container size and port (:%d).", o.port))) + log.Infoln(color.Help("Your manifest contains configurations like your container size and port.")) log.Infoln() return manifestPath, nil @@ -343,7 +343,6 @@ func (o *initSvcOpts) askDockerfile() error { } func (o *initSvcOpts) askSvcPort() error { - // Use flag before anything else if o.port != 0 { return nil } @@ -365,6 +364,10 @@ func (o *initSvcOpts) askSvcPort() error { default: defaultPort = strconv.Itoa(int(ports[0])) } + // Skip asking if it is a backend service. + if o.serviceType == manifest.BackendServiceType { + return nil + } port, err := o.prompt.Get( fmt.Sprintf(svcInitSvcPortPrompt, color.Emphasize("port")), diff --git a/internal/pkg/cli/svc_init_test.go b/internal/pkg/cli/svc_init_test.go index 93b43bed086..b621b239dec 100644 --- a/internal/pkg/cli/svc_init_test.go +++ b/internal/pkg/cli/svc_init_test.go @@ -201,6 +201,18 @@ func TestSvcInitOpts_Ask(t *testing.T) { mockDockerfile: func(m *mocks.MockdockerfileParser) {}, wantedErr: fmt.Errorf("some error"), }, + "skip asking for port for backend service": { + inSvcType: "Backend Service", + inSvcName: wantedSvcName, + inDockerfilePath: wantedDockerfilePath, + + mockPrompt: func(m *mocks.Mockprompter) {}, + mockDockerfile: func(m *mocks.MockdockerfileParser) { + m.EXPECT().GetExposedPorts().Return([]uint16{}, errors.New("no expose")) + }, + mockSel: func(m *mocks.MockdockerfileSelector) {}, + wantedErr: nil, + }, "asks for port if not specified": { inSvcType: wantedSvcType, inSvcName: wantedSvcName, @@ -309,7 +321,6 @@ func TestSvcInitOpts_Ask(t *testing.T) { require.EqualError(t, err, tc.wantedErr.Error()) } else { require.NoError(t, err) - require.Equal(t, wantedSvcType, opts.serviceType) require.Equal(t, wantedSvcName, opts.name) require.Equal(t, wantedDockerfilePath, opts.dockerfilePath) } diff --git a/internal/pkg/deploy/cloudformation/stack/backend_svc.go b/internal/pkg/deploy/cloudformation/stack/backend_svc.go index 4e045b29c26..f032aaf5883 100644 --- a/internal/pkg/deploy/cloudformation/stack/backend_svc.go +++ b/internal/pkg/deploy/cloudformation/stack/backend_svc.go @@ -19,6 +19,11 @@ const ( BackendServiceContainerPortParamKey = "ContainerPort" ) +const ( + // BlankBackendSvcContainerPort indicates no port is exposed for service container. + BlankBackendSvcContainerPort = "-1" +) + type backendSvcReadParser interface { template.ReadParser ParseBackendService(template.WorkloadOpts) (*template.Content, error) @@ -100,10 +105,14 @@ func (s *BackendService) Parameters() ([]*cloudformation.Parameter, error) { if err != nil { return nil, err } + containerPort := BlankBackendSvcContainerPort + if s.manifest.BackendServiceConfig.ImageConfig.Port != nil { + containerPort = strconv.FormatUint(uint64(aws.Uint16Value(s.manifest.BackendServiceConfig.ImageConfig.Port)), 10) + } return append(svcParams, []*cloudformation.Parameter{ { ParameterKey: aws.String(BackendServiceContainerPortParamKey), - ParameterValue: aws.String(strconv.FormatUint(uint64(aws.Uint16Value(s.manifest.BackendServiceConfig.ImageConfig.Port)), 10)), + ParameterValue: aws.String(containerPort), }, }...), nil } diff --git a/internal/pkg/describe/backend_service.go b/internal/pkg/describe/backend_service.go index 4b9e8965ea8..db73663e608 100644 --- a/internal/pkg/describe/backend_service.go +++ b/internal/pkg/describe/backend_service.go @@ -15,6 +15,12 @@ import ( "github.com/aws/copilot-cli/internal/pkg/term/color" ) +const ( + // BlankServiceDiscoveryURI is the blank service discovery URI. + BlankServiceDiscoveryURI = "-" + blankContainerPort = "-" +) + // BackendServiceDescriber retrieves information about a backend service. type BackendServiceDescriber struct { app string @@ -71,9 +77,13 @@ func (d *BackendServiceDescriber) URI(envName string) (string, error) { if err != nil { return "", fmt.Errorf("retrieve service deployment configuration: %w", err) } + port := svcParams[stack.LBWebServiceContainerPortParamKey] + if port == stack.BlankBackendSvcContainerPort { + return BlankServiceDiscoveryURI, nil + } s := serviceDiscovery{ Service: d.svc, - Port: svcParams[stack.LBWebServiceContainerPortParamKey], + Port: port, App: d.app, } return s.String(), nil @@ -98,14 +108,19 @@ func (d *BackendServiceDescriber) Describe() (HumanJSONStringer, error) { if err != nil { return nil, fmt.Errorf("retrieve service deployment configuration: %w", err) } - services = appendServiceDiscovery(services, serviceDiscovery{ - Service: d.svc, - Port: svcParams[stack.LBWebServiceContainerPortParamKey], - App: d.app, - }, env) + port := svcParams[stack.LBWebServiceContainerPortParamKey] + if port != stack.BlankBackendSvcContainerPort { + services = appendServiceDiscovery(services, serviceDiscovery{ + Service: d.svc, + Port: port, + App: d.app, + }, env) + } else { + port = blankContainerPort + } configs = append(configs, &ServiceConfig{ Environment: env, - Port: svcParams[stack.LBWebServiceContainerPortParamKey], + Port: port, Tasks: svcParams[stack.WorkloadTaskCountParamKey], CPU: svcParams[stack.WorkloadTaskCPUParamKey], Memory: svcParams[stack.WorkloadTaskMemoryParamKey], diff --git a/internal/pkg/describe/backend_service_test.go b/internal/pkg/describe/backend_service_test.go index ebd6d9d5b05..c6624eeb3b6 100644 --- a/internal/pkg/describe/backend_service_test.go +++ b/internal/pkg/describe/backend_service_test.go @@ -23,12 +23,11 @@ type backendSvcDescriberMocks struct { func TestBackendServiceDescriber_Describe(t *testing.T) { const ( - testApp = "phonetool" - testEnv = "test" - testSvc = "jobs" - testSvcPath = "*" - prodEnv = "prod" - prodSvcPath = "*" + testApp = "phonetool" + testEnv = "test" + testSvc = "jobs" + prodEnv = "prod" + mockEnv = "mockEnv" ) mockErr := errors.New("some error") testCases := map[string]struct { @@ -75,7 +74,7 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { shouldOutputResources: true, setupMocks: func(m backendSvcDescriberMocks) { gomock.InOrder( - m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv, prodEnv}, nil), + m.storeSvc.EXPECT().ListEnvironmentsDeployedTo(testApp, testSvc).Return([]string{testEnv, prodEnv, mockEnv}, nil), m.svcDescriber.EXPECT().Params().Return(map[string]string{ stack.LBWebServiceContainerPortParamKey: "5000", @@ -99,6 +98,17 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { "COPILOT_ENVIRONMENT_NAME": prodEnv, }, nil), + m.svcDescriber.EXPECT().Params().Return(map[string]string{ + stack.LBWebServiceContainerPortParamKey: "-1", + stack.WorkloadTaskCountParamKey: "2", + stack.WorkloadTaskCPUParamKey: "512", + stack.WorkloadTaskMemoryParamKey: "1024", + }, nil), + m.svcDescriber.EXPECT().EnvVars().Return( + map[string]string{ + "COPILOT_ENVIRONMENT_NAME": mockEnv, + }, nil), + m.svcDescriber.EXPECT().ServiceStackResources().Return([]*cloudformation.StackResource{ { ResourceType: aws.String("AWS::EC2::SecurityGroupIngress"), @@ -111,6 +121,12 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { PhysicalResourceId: aws.String("sg-0758ed6b233743530"), }, }, nil), + m.svcDescriber.EXPECT().ServiceStackResources().Return([]*cloudformation.StackResource{ + { + ResourceType: aws.String("AWS::EC2::SecurityGroup"), + PhysicalResourceId: aws.String("sg-2337435300758ed6b"), + }, + }, nil), ) }, wantedBackendSvc: &backendSvcDesc{ @@ -132,6 +148,13 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { Port: "5000", Tasks: "2", }, + { + CPU: "512", + Environment: "mockEnv", + Memory: "1024", + Port: "-", + Tasks: "2", + }, }, ServiceDiscovery: []*ServiceDiscovery{ { @@ -140,6 +163,11 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { }, }, Variables: []*EnvVars{ + { + Environment: "mockEnv", + Name: "COPILOT_ENVIRONMENT_NAME", + Value: "mockEnv", + }, { Environment: "prod", Name: "COPILOT_ENVIRONMENT_NAME", @@ -164,6 +192,12 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { PhysicalID: "sg-0758ed6b233743530", }, }, + "mockEnv": { + { + Type: "AWS::EC2::SecurityGroup", + PhysicalID: "sg-2337435300758ed6b", + }, + }, }, }, }, @@ -189,8 +223,9 @@ func TestBackendServiceDescriber_Describe(t *testing.T) { enableResources: tc.shouldOutputResources, store: mockStore, svcDescriber: map[string]svcDescriber{ - "test": mockSvcDescriber, - "prod": mockSvcDescriber, + "test": mockSvcDescriber, + "prod": mockSvcDescriber, + "mockEnv": mockSvcDescriber, }, initServiceDescriber: func(string) error { return nil }, } diff --git a/internal/pkg/manifest/backend_svc.go b/internal/pkg/manifest/backend_svc.go index 55cfaa9aa23..1a0f137f4ec 100644 --- a/internal/pkg/manifest/backend_svc.go +++ b/internal/pkg/manifest/backend_svc.go @@ -77,7 +77,7 @@ func NewBackendService(props BackendServiceProps) *BackendService { // Apply overrides. svc.Name = aws.String(props.Name) svc.BackendServiceConfig.ImageConfig.Build.BuildArgs.Dockerfile = aws.String(props.Dockerfile) - svc.BackendServiceConfig.ImageConfig.Port = aws.Uint16(props.Port) + svc.BackendServiceConfig.ImageConfig.Port = uint16P(props.Port) svc.BackendServiceConfig.ImageConfig.HealthCheck = healthCheck svc.parser = template.New() return svc diff --git a/internal/pkg/manifest/backend_svc_test.go b/internal/pkg/manifest/backend_svc_test.go index 17ec956c46a..2d7e9510ab4 100644 --- a/internal/pkg/manifest/backend_svc_test.go +++ b/internal/pkg/manifest/backend_svc_test.go @@ -20,13 +20,12 @@ func TestNewBackendSvc(t *testing.T) { wantedManifest *BackendService }{ - "without healthcheck": { + "without healthcheck and port": { inProps: BackendServiceProps{ WorkloadProps: WorkloadProps{ Name: "subscribers", Dockerfile: "./subscribers/Dockerfile", }, - Port: 8080, }, wantedManifest: &BackendService{ Workload: Workload{ @@ -43,7 +42,6 @@ func TestNewBackendSvc(t *testing.T) { }, }, }, - Port: aws.Uint16(8080), }, }, TaskConfig: TaskConfig{ @@ -125,13 +123,12 @@ func TestBackendSvc_MarshalBinary(t *testing.T) { wantedTestdata string }{ - "without healthcheck": { + "without healthcheck and port": { inProps: BackendServiceProps{ WorkloadProps: WorkloadProps{ Name: "subscribers", Dockerfile: "./subscribers/Dockerfile", }, - Port: 8080, }, wantedTestdata: "backend-svc-nohealthcheck.yml", }, diff --git a/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml b/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml index 24901128f81..7640b9bdcaa 100644 --- a/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml +++ b/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml @@ -4,7 +4,6 @@ # Your service name will be used in naming your resources like log groups, ECS services, etc. name: subscribers - # Your service is reachable at "http://subscribers.${COPILOT_SERVICE_DISCOVERY_ENDPOINT}:8080" but is not public. type: Backend Service diff --git a/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml b/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml index 373e72687fc..f2aae93a998 100644 --- a/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml +++ b/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml @@ -4,15 +4,12 @@ # Your service name will be used in naming your resources like log groups, ECS services, etc. name: subscribers - -# Your service is reachable at "http://subscribers.${COPILOT_SERVICE_DISCOVERY_ENDPOINT}:8080" but is not public. +# Your service does not allow any traffic. type: Backend Service image: # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args build: ./subscribers/Dockerfile - # Port exposed through your container to route traffic to it. - port: 8080 # Number of CPU units for the task. cpu: 256 diff --git a/internal/pkg/manifest/workload.go b/internal/pkg/manifest/workload.go index 451d120d049..b419c17fb1a 100644 --- a/internal/pkg/manifest/workload.go +++ b/internal/pkg/manifest/workload.go @@ -328,3 +328,10 @@ func dockerfileBuildRequired(workloadType string, svc interface{}) (bool, error) } return required, nil } + +func uint16P(n uint16) *uint16 { + if n == 0 { + return nil + } + return &n +} diff --git a/templates/workloads/services/backend/cf.yml b/templates/workloads/services/backend/cf.yml index 6ad710806c5..e08de1d3419 100644 --- a/templates/workloads/services/backend/cf.yml +++ b/templates/workloads/services/backend/cf.yml @@ -29,6 +29,8 @@ Parameters: Conditions: HasAddons: !Not [!Equals [!Ref AddonsTemplateURL, ""]] + ExposePort: + !Not [!Equals [!Ref ContainerPort, -1]] Resources: {{include "loggroup" . | indent 2}} @@ -40,8 +42,7 @@ Resources: ContainerDefinitions: - Name: !Ref WorkloadName Image: !Ref ContainerImage - PortMappings: - - ContainerPort: !Ref ContainerPort + PortMappings: !If [ExposePort, [{ContainerPort: !Ref ContainerPort}], !Ref "AWS::NoValue"] {{include "envvars" . | indent 10}} {{include "logconfig" . | indent 10}} {{- if .HealthCheck}} @@ -110,8 +111,6 @@ Resources: DeploymentConfiguration: MinimumHealthyPercent: 100 MaximumPercent: 200 - ServiceRegistries: - - RegistryArn: !GetAtt DiscoveryService.Arn - Port: !Ref ContainerPort + ServiceRegistries: !If [ExposePort, [{RegistryArn: !GetAtt DiscoveryService.Arn, Port: !Ref ContainerPort}], !Ref "AWS::NoValue"] {{include "addons" . | indent 2}} \ No newline at end of file diff --git a/templates/workloads/services/backend/manifest.yml b/templates/workloads/services/backend/manifest.yml index cdb46ac707b..ca0b4c6f6ea 100644 --- a/templates/workloads/services/backend/manifest.yml +++ b/templates/workloads/services/backend/manifest.yml @@ -4,22 +4,29 @@ # Your service name will be used in naming your resources like log groups, ECS services, etc. name: {{.Name}} - +{{- if .ImageConfig.Port}} # Your service is reachable at "http://{{.Name}}.${COPILOT_SERVICE_DISCOVERY_ENDPOINT}:{{.ImageConfig.Port}}" but is not public. +{{- else}} +# Your service does not allow any traffic. +{{- end}} type: {{.Type}} image: # Docker build arguments. You can specify additional overrides here. Supported: dockerfile, context, args build: {{.ImageConfig.Build.BuildArgs.Dockerfile}} +{{- if .ImageConfig.Port}} # Port exposed through your container to route traffic to it. - port: {{.ImageConfig.Port}}{{if .ImageConfig.HealthCheck}} + port: {{.ImageConfig.Port}} +{{- end}} +{{- if .ImageConfig.HealthCheck}} healthcheck: # The command the container runs to determine if it's healthy. command: {{fmtSlice (quoteSlice .ImageConfig.HealthCheck.Command)}} interval: {{.ImageConfig.HealthCheck.Interval}} # Time period between healthchecks. Default is 10s. retries: {{.ImageConfig.HealthCheck.Retries}} # Number of times to retry before container is deemed unhealthy. Default is 2. timeout: {{.ImageConfig.HealthCheck.Timeout}} # How long to wait before considering the healthcheck failed. Default is 5s. - start_period: {{.ImageConfig.HealthCheck.StartPeriod}} # Grace period within which to provide containers time to bootstrap before failed health checks count towards the maximum number of retries. Default is 0s.{{end}} + start_period: {{.ImageConfig.HealthCheck.StartPeriod}} # Grace period within which to provide containers time to bootstrap before failed health checks count towards the maximum number of retries. Default is 0s. +{{- end}} # Number of CPU units for the task. cpu: {{.CPU}}