package config import ( "encoding/json" "fmt" "log" "os" "path/filepath" "strconv" "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/applicationautoscaling" "github.com/aws/aws-sdk-go/service/ecs" "github.com/iancoleman/strcase" "github.com/pkg/errors" "gitlab.com/geeks-accelerator/oss/devops/pkg/devdeploy" "gopkg.in/go-playground/validator.v9" ) const ( // EnableServiceElb will enable all services to be deployed with an ELB (Elastic Load Balancer). // This will only be applied to the prod env, but the logic can be changed in the code below. // // When enabled each service will require it's own ELB and therefore will add $20~ month per service when // this is enabled. The hostnames defined for the service will be updated in Route53 to resolve to the ELB. // If HTTPS is enabled, the ELB will be created with an AWS ACM certificate that will support SSL termination on // the ELB, all traffic will be sent to the container as HTTP. // This can be configured on a by service basis. // // When not enabled, tasks will be auto assigned a public IP. As ECS tasks for the service are launched/terminated, // the task will update the hostnames defined for the service in Route53 to either add/remove its public IP. This // option is good for services that only need one container running. EnableServiceElb = false // EnableServiceAutoscaling will enable all services to be deployed with an application scaling policy. This should // typically be enabled for front end services that have an ELB enabled. EnableServiceAutoscaling = false ) // Service define the name of a service. type Service = string var ( ServiceWebApi Service = "web-api" ServiceWebApp Service = "web-app" ) // List of service names used by main.go for help. var ServiceNames = []Service{ ServiceWebApi, ServiceWebApp, } // ServiceContext defines the settings for a service. type ServiceContext struct { // Required flags. Name string `validate:"required" example:"web-api"` ServiceHostPrimary string `validate:"required" example:"example-project.com"` DesiredCount int64 `validate:"required" example:"2"` ServiceDir string `validate:"required"` Dockerfile string `validate:"required" example:"./cmd/web-api/Dockerfile"` ReleaseTag string `validate:"required"` // Optional flags. ServiceHostNames []string `validate:"omitempty" example:"subdomain.example-project.com"` EnableHTTPS bool `validate:"omitempty" example:"false"` StaticFilesS3Enable bool `validate:"omitempty" example:"false"` DockerBuildDir string `validate:"omitempty"` DockerBuildContext string `validate:"omitempty" example:"."` } // NewServiceContext returns the Service for a service that is configured for the target deployment env. func NewServiceContext(serviceName string, cfg *devdeploy.Config) (ServiceContext, error) { // ========================================================================= // New service context. srv := ServiceContext{ Name: serviceName, DesiredCount: 1, DockerBuildContext: ".", ServiceDir: filepath.Join(cfg.ProjectRoot, "cmd", serviceName), // Set the release tag for the image to use include env + service name + commit hash/tag. ReleaseTag: devdeploy.GitLabCiReleaseTag(cfg.Env, serviceName), } // ========================================================================= // Context settings based on target env. if cfg.Env == EnvStage || cfg.Env == EnvProd { srv.EnableHTTPS = true srv.StaticFilesS3Enable = true } else { srv.EnableHTTPS = false srv.StaticFilesS3Enable = false } // ========================================================================= // Service dependant settings. switch serviceName { case ServiceWebApp: // Set the hostnames for the service. if cfg.Env == EnvProd { srv.ServiceHostPrimary = "example.saasstartupkit.com" // Any hostname listed here that doesn't match the primary hostname will be updated in Route 53 but the // service itself will redirect any requests back to the primary hostname. srv.ServiceHostNames = []string{ fmt.Sprintf("%s.example.saasstartupkit.com", cfg.Env), } } else { srv.ServiceHostPrimary = fmt.Sprintf("%s.example.saasstartupkit.com", cfg.Env) } case ServiceWebApi: // Set the hostnames for the service. if cfg.Env == EnvProd { srv.ServiceHostPrimary = "api.example.saasstartupkit.com" } else { srv.ServiceHostPrimary = fmt.Sprintf("api.%s.example.saasstartupkit.com", cfg.Env) } default: return ServiceContext{}, errors.Wrapf(devdeploy.ErrInvalidService, "No service config defined for service '%s'", serviceName) } // Set the docker file if no custom one has been defined for the service. if srv.Dockerfile == "" { srv.Dockerfile = filepath.Join(srv.ServiceDir, "Dockerfile") } // Ensure the config is valid. errs := validator.New().Struct(cfg) if errs != nil { return srv, errs } return srv, nil } // BaseUrl returns the base url for a specific service. func (c ServiceContext) BaseUrl() string { var schema string if c.EnableHTTPS { schema = "https" } else { schema = "http" } return fmt.Sprintf("%s://%s/", schema, c.ServiceHostPrimary) } // NewService returns the ProjectService for a service that is configured for the target deployment env. func NewService(log *log.Logger, serviceName string, cfg *devdeploy.Config) (*devdeploy.ProjectService, error) { ctx, err := NewServiceContext(serviceName, cfg) if err != nil { return nil, err } // ========================================================================= // New project service. srv := &devdeploy.ProjectService{ Name: serviceName, CodeDir: filepath.Join(cfg.ProjectRoot, "cmd", serviceName), DockerBuildDir: ctx.DockerBuildDir, DockerBuildContext: ".", EnableHTTPS: ctx.EnableHTTPS, ServiceHostPrimary: ctx.ServiceHostPrimary, ServiceHostNames: ctx.ServiceHostNames, ReleaseTag: ctx.ReleaseTag, DockerBuildArgs: make(map[string]string), } if srv.DockerBuildDir == "" { srv.DockerBuildDir = cfg.ProjectRoot } // Sync static files to S3 will be enabled when the S3 prefix is defined. if ctx.StaticFilesS3Enable { srv.StaticFilesS3Prefix = filepath.Join(cfg.AwsS3BucketPublicKeyPrefix, ctx.ReleaseTag, "static") } // ========================================================================= // Service settings based on target env. var enableElb bool if cfg.Env == EnvStage || cfg.Env == EnvProd { if cfg.Env == EnvProd && EnableServiceElb { enableElb = true } } // ========================================================================= // Shared details that could be applied to all task definitions. // Define the ECS Cluster used to host the serverless fargate tasks. srv.AwsEcsCluster = &devdeploy.AwsEcsCluster{ ClusterName: cfg.ProjectName + "-" + cfg.Env, Tags: []devdeploy.Tag{ {Key: devdeploy.AwsTagNameProject, Value: cfg.ProjectName}, {Key: devdeploy.AwsTagNameEnv, Value: cfg.Env}, }, } // Define the ECS task execution role. This role executes ECS actions such as pulling the image and storing the // application logs in cloudwatch. srv.AwsEcsExecutionRole = &devdeploy.AwsIamRole{ RoleName: fmt.Sprintf("ecsExecutionRole%s%s", cfg.ProjectNameCamel(), strcase.ToCamel(cfg.Env)), Description: fmt.Sprintf("Provides access to other AWS service resources that are required to run Amazon ECS tasks for %s. ", cfg.ProjectName), AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ecs-tasks.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}", Tags: []devdeploy.Tag{ {Key: devdeploy.AwsTagNameProject, Value: cfg.ProjectName}, {Key: devdeploy.AwsTagNameEnv, Value: cfg.Env}, }, AttachRolePolicyArns: []string{"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"}, } log.Printf("\t\tSet ECS Execution Role Name to '%s'.", srv.AwsEcsExecutionRole.RoleName) // Define the ECS task role. This role is used by the task itself for calling other AWS services. srv.AwsEcsTaskRole = &devdeploy.AwsIamRole{ RoleName: fmt.Sprintf("ecsTaskRole%s%s", cfg.ProjectNameCamel(), strcase.ToCamel(cfg.Env)), Description: fmt.Sprintf("Allows ECS tasks for %s to call AWS services on your behalf.", cfg.ProjectName), AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ecs-tasks.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}", Tags: []devdeploy.Tag{ {Key: devdeploy.AwsTagNameProject, Value: cfg.ProjectName}, {Key: devdeploy.AwsTagNameEnv, Value: cfg.Env}, }, } log.Printf("\t\tSet ECS Task Role Name to '%s'.", srv.AwsEcsTaskRole.RoleName) // AwsCloudWatchLogGroup defines the name of the cloudwatch log group that will be used to store logs for the ECS tasks. srv.AwsCloudWatchLogGroup = &devdeploy.AwsCloudWatchLogGroup{ LogGroupName: fmt.Sprintf("logs/env_%s/aws/ecs/cluster_%s/service_%s", cfg.Env, srv.AwsEcsCluster.ClusterName, srv.Name), Tags: []devdeploy.Tag{ {Key: devdeploy.AwsTagNameProject, Value: cfg.ProjectName}, {Key: devdeploy.AwsTagNameEnv, Value: cfg.Env}, }, } log.Printf("\t\tSet AWS Log Group Name to '%s'.", srv.AwsCloudWatchLogGroup.LogGroupName) // AwsSdPrivateDnsNamespace defines the service discovery group. srv.AwsSdPrivateDnsNamespace = &devdeploy.AwsSdPrivateDnsNamespace{ Name: srv.AwsEcsCluster.ClusterName, Description: fmt.Sprintf("Private DNS namespace used for services running on the ECS Cluster %s", srv.AwsEcsCluster.ClusterName), Service: &devdeploy.AwsSdService{ Name: ctx.Name, Description: fmt.Sprintf("Service %s running on the ECS Cluster %s", ctx.Name, srv.AwsEcsCluster.ClusterName), DnsRecordTTL: 300, HealthCheckFailureThreshold: 3, }, } log.Printf("\t\tSet AWS Service Discovery Namespace to '%s'.", srv.AwsSdPrivateDnsNamespace.Name) // If the service is requested to use an elastic load balancer then define. if enableElb { // AwsElbLoadBalancer defines if the service should use an elastic load balancer. srv.AwsElbLoadBalancer = &devdeploy.AwsElbLoadBalancer{ Name: fmt.Sprintf("%s-%s", cfg.Env, ctx.Name), IpAddressType: "ipv4", Scheme: "internet-facing", Type: "application", Tags: []devdeploy.Tag{ {Key: devdeploy.AwsTagNameProject, Value: cfg.ProjectName}, {Key: devdeploy.AwsTagNameEnv, Value: cfg.Env}, }, } log.Printf("\t\tSet ELB Name to '%s'.", srv.AwsElbLoadBalancer.Name) // Define the target group for service to receive HTTP traffic from the load balancer. srv.AwsElbLoadBalancer.TargetGroups = []*devdeploy.AwsElbTargetGroup{ { Name: fmt.Sprintf("%s-http", ctx.Name), Port: 80, Protocol: "HTTP", TargetType: "ip", HealthCheckEnabled: true, HealthCheckIntervalSeconds: 30, HealthCheckPath: "/ping", HealthCheckProtocol: "HTTP", HealthCheckTimeoutSeconds: 5, HealthyThresholdCount: 3, UnhealthyThresholdCount: 3, Matcher: "200", }, } log.Printf("\t\t\tSet ELB Target Group Name for %s to '%s'.", srv.AwsElbLoadBalancer.TargetGroups[0].Protocol, srv.AwsElbLoadBalancer.TargetGroups[0].Name) // Set ECS configs based on specified env. if cfg.Env == EnvProd { srv.AwsElbLoadBalancer.EcsTaskDeregistrationDelay = 300 } else { // Force staging to deploy immediately without waiting for connections to drain srv.AwsElbLoadBalancer.EcsTaskDeregistrationDelay = 0 } } // AwsEcsService defines the details for the ecs service. srv.AwsEcsService = &devdeploy.AwsEcsService{ ServiceName: ctx.Name, DesiredCount: ctx.DesiredCount, EnableECSManagedTags: false, HealthCheckGracePeriodSeconds: 60, LaunchType: "FARGATE", } // Set ECS configs based on specified env. if cfg.Env == EnvProd { srv.AwsEcsService.DeploymentMinimumHealthyPercent = 100 srv.AwsEcsService.DeploymentMaximumPercent = 200 } else { srv.AwsEcsService.DeploymentMinimumHealthyPercent = 0 srv.AwsEcsService.DeploymentMaximumPercent = 100 } if EnableServiceAutoscaling { srv.AwsAppAutoscalingPolicy = &devdeploy.AwsAppAutoscalingPolicy{ // The name of the scaling policy. PolicyName: srv.AwsEcsService.ServiceName, // The policy type. This parameter is required if you are creating a scaling // policy. // // The following policy types are supported: // // TargetTrackingScaling—Not supported for Amazon EMR or AppStream // // StepScaling—Not supported for Amazon DynamoDB // // For more information, see Step Scaling Policies for Application Auto Scaling // (https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-step-scaling-policies.html) // and Target Tracking Scaling Policies for Application Auto Scaling (https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-target-tracking.html) // in the Application Auto Scaling User Guide. PolicyType: "TargetTrackingScaling", // The minimum value to scale to in response to a scale-in event. MinCapacity // is required to register a scalable target. MinCapacity: ctx.DesiredCount, // The maximum value to scale to in response to a scale-out event. MaxCapacity // is required to register a scalable target. MaxCapacity: ctx.DesiredCount * 2, // A target tracking scaling policy. Includes support for predefined or customized metrics. TargetTrackingScalingPolicyConfiguration: &applicationautoscaling.TargetTrackingScalingPolicyConfiguration{ // A predefined metric. You can specify either a predefined metric or a customized // metric. PredefinedMetricSpecification: &applicationautoscaling.PredefinedMetricSpecification{ // The metric type. The following predefined metrics are available: // // * ASGAverageCPUUtilization - Average CPU utilization of the Auto Scaling // group. // // * ASGAverageNetworkIn - Average number of bytes received on all network // interfaces by the Auto Scaling group. // // * ASGAverageNetworkOut - Average number of bytes sent out on all network // interfaces by the Auto Scaling group. // // * ALBRequestCountPerTarget - Number of requests completed per target in // an Application Load Balancer target group. ResourceLabel will be auto populated. // PredefinedMetricType: aws.String("ECSServiceAverageCPUUtilization"), }, // The target value for the metric. The range is 8.515920e-109 to 1.174271e+108 // (Base 10) or 2e-360 to 2e360 (Base 2). TargetValue: aws.Float64(70.0), // The amount of time, in seconds, after a scale-in activity completes before // another scale in activity can start. // // The cooldown period is used to block subsequent scale-in requests until it // has expired. The intention is to scale in conservatively to protect your // application's availability. However, if another alarm triggers a scale-out // policy during the cooldown period after a scale-in, Application Auto Scaling // scales out your scalable target immediately. ScaleInCooldown: aws.Int64(300), // The amount of time, in seconds, after a scale-out activity completes before // another scale-out activity can start. // // While the cooldown period is in effect, the capacity that has been added // by the previous scale-out event that initiated the cooldown is calculated // as part of the desired capacity for the next scale out. The intention is // to continuously (but not excessively) scale out. ScaleOutCooldown: aws.Int64(300), // Indicates whether scale in by the target tracking scaling policy is disabled. // If the value is true, scale in is disabled and the target tracking scaling // policy won't remove capacity from the scalable resource. Otherwise, scale // in is enabled and the target tracking scaling policy can remove capacity // from the scalable resource. The default value is false. DisableScaleIn: aws.Bool(false), }, } } // Load the web-app config for the web-api can reference it's hostname. webAppCtx, err := NewServiceContext(ServiceWebApp, cfg) if err != nil { return nil, err } // Load the web-api config for the web-app can reference it's hostname. webApiCtx, err := NewServiceContext(ServiceWebApi, cfg) if err != nil { return nil, err } // Try to find the Datadog API key, this value is optional. // If Datadog API key is not specified, then integration with Datadog for observability will not be active. datadogApiKey, err := getDatadogApiKey(cfg) if err != nil { return srv, err } // Add the Datadog container to the task definition if an API Key is set. var ddContainer *ecs.ContainerDefinition if datadogApiKey != "" { ddTags := []string{ "source:docker", "service:" + srv.AwsEcsService.ServiceName, "service_name:" + ctx.Name, "cluster:" + srv.AwsEcsCluster.ClusterName, "env:" + cfg.Env, } // Defined a container definition for the specific service. ddContainer = &ecs.ContainerDefinition{ Name: aws.String("datadog-agent"), Image: aws.String(srv.ReleaseImage), Essential: aws.Bool(true), PortMappings: []*ecs.PortMapping{ { ContainerPort: aws.Int64(8125), }, { ContainerPort: aws.Int64(8126), }, }, Cpu: aws.Int64(128), MemoryReservation: aws.Int64(256), Environment: []*ecs.KeyValuePair{ ecsKeyValuePair("DD_API_KEY", datadogApiKey), ecsKeyValuePair("DD_LOGS_ENABLED", "true"), ecsKeyValuePair("DD_APM_ENABLED", "true"), ecsKeyValuePair("DD_RECEIVER_PORT", "8126"), ecsKeyValuePair("DD_APM_NON_LOCAL_TRAFFIC", "true"), ecsKeyValuePair("DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL", "true"), ecsKeyValuePair("DD_TAGS", strings.Join(ddTags, " ")), ecsKeyValuePair("DD_DOGSTATSD_ORIGIN_DETECTION", "true"), ecsKeyValuePair("DD_DOGSTATSD_NON_LOCAL_TRAFFIC", "true"), ecsKeyValuePair("ECS_FARGATE", "true"), }, } } // Define a base set of environment variables that can be assigned to individual container definitions. baseEnvVals := func() []*ecs.KeyValuePair { var ciJobURL string if id := os.Getenv("CI_JOB_ID"); id != "" { ciJobURL = strings.TrimRight(GitLabProjectBaseUrl, "/") + "/-/jobs/" + os.Getenv("CI_JOB_ID") } var ciPipelineURL string if id := os.Getenv("CI_PIPELINE_ID"); id != "" { ciPipelineURL = strings.TrimRight(GitLabProjectBaseUrl, "/") + "/pipelines/" + os.Getenv("CI_PIPELINE_ID") } envVars := []*ecs.KeyValuePair{ ecsKeyValuePair(devdeploy.ENV_KEY_ECS_CLUSTER, srv.AwsEcsCluster.ClusterName), ecsKeyValuePair(devdeploy.ENV_KEY_ECS_SERVICE, srv.AwsEcsService.ServiceName), ecsKeyValuePair("AWS_DEFAULT_REGION", cfg.AwsCredentials.Region), ecsKeyValuePair("AWS_USE_ROLE", "true"), ecsKeyValuePair("AWSLOGS_GROUP", srv.AwsCloudWatchLogGroup.LogGroupName), ecsKeyValuePair("ECS_ENABLE_CONTAINER_METADATA", "true"), ecsKeyValuePair("CI_COMMIT_REF_NAME", os.Getenv("CI_COMMIT_REF_NAME")), ecsKeyValuePair("CI_COMMIT_SHORT_SHA", os.Getenv("CI_COMMIT_SHORT_SHA")), ecsKeyValuePair("CI_COMMIT_SHA", os.Getenv("CI_COMMIT_SHA")), ecsKeyValuePair("CI_COMMIT_TAG", os.Getenv("CI_COMMIT_TAG")), ecsKeyValuePair("CI_JOB_ID", os.Getenv("CI_JOB_ID")), ecsKeyValuePair("CI_PIPELINE_ID", os.Getenv("CI_PIPELINE_ID")), ecsKeyValuePair("CI_JOB_URL", ciJobURL), ecsKeyValuePair("CI_PIPELINE_URL", ciPipelineURL), ecsKeyValuePair("WEB_APP_BASE_URL", webAppCtx.BaseUrl()), ecsKeyValuePair("WEB_API_BASE_URL", webApiCtx.BaseUrl()), ecsKeyValuePair("EMAIL_SENDER", "lee+saas-starter-kit@geeksinthewoods.com"), } if datadogApiKey != "" { envVars = append(envVars, ecsKeyValuePair("DATADOG_ADDR", "127.0.0.1:8125"), ecsKeyValuePair("DD_API_KEY", datadogApiKey), ecsKeyValuePair("DD_TRACE_AGENT_PORT", "8126"), ecsKeyValuePair("DD_SERVICE_NAME", srv.AwsEcsService.ServiceName), ecsKeyValuePair("DD_ENV", cfg.Env)) } return envVars } // ========================================================================= // Service dependant settings. switch serviceName { // Define the ServiceContext for the web-app that will be used for build and deploy. case ServiceWebApp: // Defined a container definition for the specific service. container1 := &ecs.ContainerDefinition{ Name: aws.String(ctx.Name), Image: aws.String(srv.ReleaseImage), Essential: aws.Bool(true), LogConfiguration: &ecs.LogConfiguration{ LogDriver: aws.String("awslogs"), Options: map[string]*string{ "awslogs-group": aws.String(srv.AwsCloudWatchLogGroup.LogGroupName), "awslogs-region": aws.String(cfg.AwsCredentials.Region), "awslogs-stream-prefix": aws.String("ecs"), }, }, PortMappings: []*ecs.PortMapping{ { HostPort: aws.Int64(80), Protocol: aws.String("tcp"), ContainerPort: aws.Int64(80), }, }, Cpu: aws.Int64(128), MemoryReservation: aws.Int64(128), Environment: baseEnvVals(), HealthCheck: &ecs.HealthCheck{ Retries: aws.Int64(3), Command: aws.StringSlice([]string{ "CMD-SHELL", "curl -f http://localhost/ping || exit 1", }), Timeout: aws.Int64(5), Interval: aws.Int64(60), StartPeriod: aws.Int64(60), }, Ulimits: []*ecs.Ulimit{ { Name: aws.String("nofile"), SoftLimit: aws.Int64(987654), HardLimit: aws.Int64(999999), }, }, } // If the service has HTTPS enabled with the use of an AWS Elastic Load Balancer, then need to enable // traffic for port 443 for SSL traffic to get terminated on the deployed tasks. if ctx.EnableHTTPS && !enableElb { container1.PortMappings = append(container1.PortMappings, &ecs.PortMapping{ HostPort: aws.Int64(443), Protocol: aws.String("tcp"), ContainerPort: aws.Int64(443), }) } // Define the full task definition for the service. taskDef := &ecs.RegisterTaskDefinitionInput{ Family: aws.String(fmt.Sprintf("%s-%s-%s", cfg.Env, srv.AwsEcsCluster.ClusterName, ctx.Name)), NetworkMode: aws.String("awsvpc"), ContainerDefinitions: []*ecs.ContainerDefinition{ // Include the single container definition for the service. Additional definitions could be added // here like one for datadog. container1, }, RequiresCompatibilities: aws.StringSlice([]string{"FARGATE"}), } // Append the datadog container if defined. if ddContainer != nil { taskDef.ContainerDefinitions = append(taskDef.ContainerDefinitions, ddContainer) } srv.AwsEcsTaskDefinition = &devdeploy.AwsEcsTaskDefinition{ RegisterInput: taskDef, PreRegister: func(input *ecs.RegisterTaskDefinitionInput, vars devdeploy.AwsEcsServiceDeployVariables) error { // Append env vars for the service task. input.ContainerDefinitions[0].Environment = append(input.ContainerDefinitions[0].Environment, ecsKeyValuePair("SERVICE_NAME", ctx.Name), ecsKeyValuePair("PROJECT_NAME", cfg.ProjectName), // Use placeholders for these environment variables that will be replaced with devdeploy.DeployServiceToTargetEnv ecsKeyValuePair("WEB_APP_HTTP_HOST", vars.HTTPHost), ecsKeyValuePair("WEB_APP_HTTPS_HOST", vars.HTTPSHost), ecsKeyValuePair("WEB_APP_SERVICE_ENABLE_HTTPS", strconv.FormatBool(vars.HTTPSEnabled)), ecsKeyValuePair("WEB_APP_SERVICE_BASE_URL", vars.ServiceBaseUrl), ecsKeyValuePair("WEB_APP_SERVICE_HOST_NAMES", strings.Join(vars.AlternativeHostnames, ",")), ecsKeyValuePair("WEB_APP_SERVICE_STATICFILES_S3_ENABLED", strconv.FormatBool(vars.StaticFilesS3Enabled)), ecsKeyValuePair("WEB_APP_SERVICE_STATICFILES_S3_PREFIX", vars.StaticFilesS3Prefix), ecsKeyValuePair("WEB_APP_SERVICE_STATICFILES_CLOUDFRONT_ENABLED", strconv.FormatBool(vars.StaticFilesCloudfrontEnabled)), ecsKeyValuePair("WEB_APP_REDIS_HOST", vars.CacheHost), ecsKeyValuePair("WEB_APP_DB_HOST", vars.DbHost), ecsKeyValuePair("WEB_APP_DB_USER", vars.DbUser), ecsKeyValuePair("WEB_APP_DB_PASS", vars.DbPass), ecsKeyValuePair("WEB_APP_DB_DATABASE", vars.DbName), ecsKeyValuePair("WEB_APP_DB_DRIVER", vars.DbDriver), ecsKeyValuePair("WEB_APP_DB_DISABLE_TLS", strconv.FormatBool(vars.DbDisableTLS)), ecsKeyValuePair("WEB_APP_AWS_S3_BUCKET_PRIVATE", vars.AwsS3BucketNamePrivate), ecsKeyValuePair("WEB_APP_AWS_S3_BUCKET_PUBLIC", vars.AwsS3BucketNamePublic), ecsKeyValuePair("WEB_APP_SERVICE_MINIFY", "true"), ) // Enable image resize s3 is enabled. if vars.StaticFilesS3Enabled { input.ContainerDefinitions[0].Environment = append(input.ContainerDefinitions[0].Environment, ecsKeyValuePair("WEB_APP_SERVICE_STATICFILES_IMG_RESIZE_ENABLED", "true")) } // When no Elastic Load Balance is used, tasks need to be able to directly update the Route 53 records. if vars.AwsElbLoadBalancer == nil { input.ContainerDefinitions[0].Environment = append(input.ContainerDefinitions[0].Environment, ecsKeyValuePair(devdeploy.ENV_KEY_ROUTE53_ZONES, vars.EncodeRoute53Zones()), ecsKeyValuePair(devdeploy.ENV_KEY_ROUTE53_UPDATE_TASK_IPS, "true")) } return nil }, } // Define the ServiceContext for the web-api that will be used for build and deploy. case ServiceWebApi: // Defined a container definition for the specific service. container1 := &ecs.ContainerDefinition{ Name: aws.String(ctx.Name), Image: aws.String(srv.ReleaseImage), Essential: aws.Bool(true), LogConfiguration: &ecs.LogConfiguration{ LogDriver: aws.String("awslogs"), Options: map[string]*string{ "awslogs-group": aws.String(srv.AwsCloudWatchLogGroup.LogGroupName), "awslogs-region": aws.String(cfg.AwsCredentials.Region), "awslogs-stream-prefix": aws.String("ecs"), }, }, PortMappings: []*ecs.PortMapping{ { HostPort: aws.Int64(80), Protocol: aws.String("tcp"), ContainerPort: aws.Int64(80), }, }, Cpu: aws.Int64(128), MemoryReservation: aws.Int64(128), Environment: baseEnvVals(), HealthCheck: &ecs.HealthCheck{ Retries: aws.Int64(3), Command: aws.StringSlice([]string{ "CMD-SHELL", "curl -f http://localhost/ping || exit 1", }), Timeout: aws.Int64(5), Interval: aws.Int64(60), StartPeriod: aws.Int64(60), }, Ulimits: []*ecs.Ulimit{ { Name: aws.String("nofile"), SoftLimit: aws.Int64(987654), HardLimit: aws.Int64(999999), }, }, } // If the service has HTTPS enabled with the use of an AWS Elastic Load Balancer, then need to enable // traffic for port 443 for SSL traffic to get terminated on the deployed tasks. if ctx.EnableHTTPS && !enableElb { container1.PortMappings = append(container1.PortMappings, &ecs.PortMapping{ HostPort: aws.Int64(443), Protocol: aws.String("tcp"), ContainerPort: aws.Int64(443), }) } // Define the full task definition for the service. taskDef := &ecs.RegisterTaskDefinitionInput{ Family: aws.String(fmt.Sprintf("%s-%s-%s", cfg.Env, srv.AwsEcsCluster.ClusterName, ctx.Name)), NetworkMode: aws.String("awsvpc"), ContainerDefinitions: []*ecs.ContainerDefinition{ // Include the single container definition for the service. Additional definitions could be added // here like one for datadog. container1, }, RequiresCompatibilities: aws.StringSlice([]string{"FARGATE"}), } // Append the datadog container if defined. if ddContainer != nil { taskDef.ContainerDefinitions = append(taskDef.ContainerDefinitions, ddContainer) } srv.AwsEcsTaskDefinition = &devdeploy.AwsEcsTaskDefinition{ RegisterInput: taskDef, PreRegister: func(input *ecs.RegisterTaskDefinitionInput, vars devdeploy.AwsEcsServiceDeployVariables) error { // Append env vars for the service task. input.ContainerDefinitions[0].Environment = append(input.ContainerDefinitions[0].Environment, ecsKeyValuePair("SERVICE_NAME", ctx.Name), ecsKeyValuePair("PROJECT_NAME", cfg.ProjectName), // Use placeholders for these environment variables that will be replaced with devdeploy.DeployServiceToTargetEnv ecsKeyValuePair("WEB_API_HTTP_HOST", vars.HTTPHost), ecsKeyValuePair("WEB_API_HTTPS_HOST", vars.HTTPSHost), ecsKeyValuePair("WEB_API_SERVICE_ENABLE_HTTPS", strconv.FormatBool(vars.HTTPSEnabled)), ecsKeyValuePair("WEB_API_SERVICE_BASE_URL", vars.ServiceBaseUrl), ecsKeyValuePair("WEB_API_SERVICE_HOST_NAMES", strings.Join(vars.AlternativeHostnames, ",")), ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_S3_ENABLED", strconv.FormatBool(vars.StaticFilesS3Enabled)), ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_S3_PREFIX", vars.StaticFilesS3Prefix), ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_CLOUDFRONT_ENABLED", strconv.FormatBool(vars.StaticFilesCloudfrontEnabled)), ecsKeyValuePair("WEB_API_REDIS_HOST", vars.CacheHost), ecsKeyValuePair("WEB_API_DB_HOST", vars.DbHost), ecsKeyValuePair("WEB_API_DB_USER", vars.DbUser), ecsKeyValuePair("WEB_API_DB_PASS", vars.DbPass), ecsKeyValuePair("WEB_API_DB_DATABASE", vars.DbName), ecsKeyValuePair("WEB_API_DB_DRIVER", vars.DbDriver), ecsKeyValuePair("WEB_API_DB_DISABLE_TLS", strconv.FormatBool(vars.DbDisableTLS)), ecsKeyValuePair("WEB_API_AWS_S3_BUCKET_PRIVATE", vars.AwsS3BucketNamePrivate), ecsKeyValuePair("WEB_API_AWS_S3_BUCKET_PUBLIC", vars.AwsS3BucketNamePublic), ecsKeyValuePair("WEB_API_SERVICE_MINIFY", "true"), ) // When no Elastic Load Balance is used, tasks need to be able to directly update the Route 53 records. if vars.AwsElbLoadBalancer == nil { input.ContainerDefinitions[0].Environment = append(input.ContainerDefinitions[0].Environment, ecsKeyValuePair(devdeploy.ENV_KEY_ROUTE53_ZONES, vars.EncodeRoute53Zones()), ecsKeyValuePair(devdeploy.ENV_KEY_ROUTE53_UPDATE_TASK_IPS, "true")) } return nil }, } srv.DockerBuildArgs["swagInit"] = "1" default: return nil, errors.Wrapf(devdeploy.ErrInvalidService, "No service context defined for service '%s'", serviceName) } // Set the docker file if no custom one has been defined for the service. if srv.Dockerfile == "" { srv.Dockerfile = filepath.Join(srv.CodeDir, "Dockerfile") } if srv.StaticFilesDir == "" { srv.StaticFilesDir = filepath.Join(srv.CodeDir, "static") } // When only service host names are set, choose the first item as the primary host. if srv.ServiceHostPrimary == "" && len(srv.ServiceHostNames) > 0 { srv.ServiceHostPrimary = srv.ServiceHostNames[0] log.Printf("\t\tSet Service Primary Host to '%s'.", srv.ServiceHostPrimary) } return srv, nil } // BuildServiceForTargetEnv executes the build commands for a target service. func BuildServiceForTargetEnv(log *log.Logger, awsCredentials devdeploy.AwsCredentials, targetEnv Env, serviceName, releaseTag string, dryRun, noCache, noPush bool) error { cfg, err := NewConfig(log, targetEnv, awsCredentials) if err != nil { return err } targetSvc, err := NewService(log, serviceName, cfg) if err != nil { return err } // Override the release tag if set. if releaseTag != "" { targetSvc.ReleaseTag = releaseTag } // Append build args to be used for all services. if targetSvc.DockerBuildArgs == nil { targetSvc.DockerBuildArgs = make(map[string]string) } // servicePath is used to copy the service specific code in the Dockerfile. codePath, err := filepath.Rel(cfg.ProjectRoot, targetSvc.CodeDir) if err != nil { return err } targetSvc.DockerBuildArgs["code_path"] = codePath // commitRef is used by main.go:build constant. commitRef := getCommitRef() if commitRef == "" { commitRef = targetSvc.ReleaseTag } targetSvc.DockerBuildArgs["commit_ref"] = commitRef if dryRun { cfgJSON, err := json.MarshalIndent(cfg, "", " ") if err != nil { log.Fatalf("BuildServiceForTargetEnv : Marshalling config to JSON : %+v", err) } log.Printf("BuildServiceForTargetEnv : config : %v\n", string(cfgJSON)) detailsJSON, err := json.MarshalIndent(targetSvc, "", " ") if err != nil { log.Fatalf("BuildServiceForTargetEnv : Marshalling details to JSON : %+v", err) } log.Printf("BuildServiceForTargetEnv : details : %v\n", string(detailsJSON)) return nil } return devdeploy.BuildServiceForTargetEnv(log, cfg, targetSvc, noCache, noPush) } // DeployServiceForTargetEnv executes the build commands for a target service. func DeployServiceForTargetEnv(log *log.Logger, awsCredentials devdeploy.AwsCredentials, targetEnv Env, serviceName, releaseTag string, dryRun bool) error { cfg, err := NewConfig(log, targetEnv, awsCredentials) if err != nil { return err } targetSvc, err := NewService(log, serviceName, cfg) if err != nil { return err } // Override the release tag if set. if releaseTag != "" { targetSvc.ReleaseTag = releaseTag } return devdeploy.DeployServiceToTargetEnv(log, cfg, targetSvc) } // ecsKeyValuePair returns an *ecs.KeyValuePair func ecsKeyValuePair(name, value string) *ecs.KeyValuePair { return &ecs.KeyValuePair{ Name: aws.String(name), Value: aws.String(value), } }