mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-06 23:46:29 +02:00
757 lines
27 KiB
Go
757 lines
27 KiB
Go
|
package config
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
"github.com/aws/aws-sdk-go/aws"
|
||
|
"github.com/aws/aws-sdk-go/service/ecs"
|
||
|
"github.com/iancoleman/strcase"
|
||
|
"gitlab.com/geeks-accelerator/oss/devops/pkg/devdeploy"
|
||
|
"gopkg.in/go-playground/validator.v9"
|
||
|
)
|
||
|
|
||
|
// Service define the name of a service.
|
||
|
type Service = string
|
||
|
|
||
|
var (
|
||
|
ServiceWebApi = "web-api"
|
||
|
ServiceWebApp = "web-app"
|
||
|
)
|
||
|
|
||
|
// List of service names used by main.go for help.
|
||
|
var ServiceNames = []Service{
|
||
|
ServiceWebApi,
|
||
|
ServiceWebApp,
|
||
|
}
|
||
|
|
||
|
// ServiceConfig defines the settings for a service.
|
||
|
type ServiceConfig struct {
|
||
|
// Required flags.
|
||
|
Name string `validate:"required" example:"web-api"`
|
||
|
ServiceHostPrimary string `validate:"required" example:"example-project.com"`
|
||
|
DesiredCount int `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"`
|
||
|
EnableElb bool `validate:"omitempty" example:"false"`
|
||
|
StaticFilesS3Enable bool `validate:"omitempty" example:"false"`
|
||
|
BuildDir string `validate:"omitempty"`
|
||
|
DockerBuildContext string `validate:"omitempty" example:"."`
|
||
|
}
|
||
|
|
||
|
// ServiceContext includes the config and task definition for building and deploying a service.
|
||
|
type ServiceContext struct {
|
||
|
ServiceConfig
|
||
|
|
||
|
// AwsEcsTaskDefinition defines the ECS task definition based on the service configs.
|
||
|
AwsEcsTaskDefinition func(cfg *devdeploy.Config, srv *devdeploy.DeployService) (*ecs.RegisterTaskDefinitionInput, error)
|
||
|
}
|
||
|
|
||
|
// NewServiceConfig returns the Service for a service that is configured for the target deployment env.
|
||
|
func NewServiceConfig(serviceName string, cfg *devdeploy.Config) (ServiceConfig, error) {
|
||
|
|
||
|
// =========================================================================
|
||
|
// New service context.
|
||
|
srv := ServiceConfig{
|
||
|
Name: serviceName,
|
||
|
DesiredCount: 1,
|
||
|
DockerBuildContext: ".",
|
||
|
ServiceDir: filepath.Join(cfg.ProjectRoot, "examples", 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 ServiceConfig{}, 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 ServiceConfig) BaseUrl() string {
|
||
|
var schema string
|
||
|
if c.EnableHTTPS {
|
||
|
schema = "https"
|
||
|
} else {
|
||
|
schema = "http"
|
||
|
}
|
||
|
return fmt.Sprintf("%s://%s/", schema, c.ServiceHostPrimary)
|
||
|
}
|
||
|
|
||
|
|
||
|
// NewServiceContext returns the ServiceContext for a service that is configured for the target deployment env.
|
||
|
func NewServiceContext(serviceName Service, cfg *devdeploy.Config) (*ServiceContext, error) {
|
||
|
|
||
|
// =========================================================================
|
||
|
// Shared details that could be applied to all task definitions.
|
||
|
|
||
|
// Load the web-app config for the web-api can reference it's hostname.
|
||
|
webAppCfg, err := NewServiceConfig(ServiceWebApp, cfg)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Load the web-api config for the web-app can reference it's hostname.
|
||
|
webApiCfg, err := NewServiceConfig(ServiceWebApi, cfg)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Define a base set of environment variables that can be assigned to individual container definitions.
|
||
|
baseEnvVals := func(cfg *devdeploy.Config, srv *devdeploy.DeployService) []*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")
|
||
|
}
|
||
|
|
||
|
return []*ecs.KeyValuePair{
|
||
|
ecsKeyValuePair(devdeploy.ENV_KEY_ECS_CLUSTER, srv.AwsEcsCluster.ClusterName),
|
||
|
ecsKeyValuePair(devdeploy.ENV_KEY_ECS_SERVICE, srv.AwsEcsService.ServiceName),
|
||
|
ecsKeyValuePair("AWS_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", webAppCfg.BaseUrl()),
|
||
|
ecsKeyValuePair("WEB_API_BASE_URL", webApiCfg.BaseUrl()),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// =========================================================================
|
||
|
// Service dependant settings.
|
||
|
|
||
|
var ctx *ServiceContext
|
||
|
switch serviceName {
|
||
|
|
||
|
// Define the ServiceContext for the web-app that will be used for build and deploy.
|
||
|
case ServiceWebApp:
|
||
|
|
||
|
ctx := &ServiceContext{
|
||
|
ServiceConfig: webAppCfg,
|
||
|
}
|
||
|
|
||
|
// Define the service task definition with a function to enable use of config and deploy details.
|
||
|
ctx.AwsEcsTaskDefinition = func(cfg *devdeploy.Config, srv *devdeploy.DeployService) (*ecs.RegisterTaskDefinitionInput, error) {
|
||
|
|
||
|
// 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{
|
||
|
&ecs.PortMapping{
|
||
|
HostPort: aws.Int64(80),
|
||
|
Protocol: aws.String("tcp"),
|
||
|
ContainerPort: aws.Int64(80),
|
||
|
},
|
||
|
},
|
||
|
Cpu: aws.Int64(128),
|
||
|
MemoryReservation: aws.Int64(128),
|
||
|
Environment: baseEnvVals(cfg, srv),
|
||
|
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{
|
||
|
&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 && !ctx.EnableElb {
|
||
|
container1.PortMappings = append(container1.PortMappings, &ecs.PortMapping{
|
||
|
HostPort: aws.Int64(443),
|
||
|
Protocol: aws.String("tcp"),
|
||
|
ContainerPort: aws.Int64(443),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Append env vars for the service task.
|
||
|
container1.Environment = append(container1.Environment,
|
||
|
ecsKeyValuePair("SERVICE_NAME", srv.ServiceName),
|
||
|
ecsKeyValuePair("PROJECT_NAME", cfg.ProjectName),
|
||
|
|
||
|
// Use placeholders for these environment variables that will be replaced with devdeploy.DeployServiceToTargetEnv
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_HOST", "{HTTP_HOST}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_HTTPS_HOST", "{HTTPS_HOST}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_ENABLE_HTTPS", "{HTTPS_ENABLED}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_BASE_URL", "{APP_BASE_URL}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_HOST_NAMES", "{HOST_NAMES}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_S3_ENABLED", "{STATIC_FILES_S3_ENABLED}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_S3_PREFIX", "{STATIC_FILES_S3_PREFIX}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_CLOUDFRONT_ENABLED", "{STATIC_FILES_CLOUDFRONT_ENABLED}"),
|
||
|
ecsKeyValuePair("WEB_API_REDIS_HOST", "{CACHE_HOST}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_HOST", "{DB_HOST}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_USERNAME", "{DB_USER}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_PASSWORD", "{DB_PASS}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_DATABASE", "{DB_DATABASE}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_DRIVER", "{DB_DRIVER}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_DISABLE_TLS", "{DB_DISABLE_TLS}"),
|
||
|
ecsKeyValuePair("WEB_API_AWS_S3_BUCKET_PRIVATE", "{AWS_S3_BUCKET_PRIVATE}"),
|
||
|
ecsKeyValuePair("WEB_API_AWS_S3_BUCKET_PUBLIC", "{AWS_S3_BUCKET_PUBLIC}"),
|
||
|
ecsKeyValuePair(devdeploy.ENV_KEY_ROUTE53_UPDATE_TASK_IPS, "{ROUTE53_UPDATE_TASK_IPS}"),
|
||
|
ecsKeyValuePair(devdeploy.ENV_KEY_ROUTE53_ZONES, "{ROUTE53_ZONES}"),
|
||
|
)
|
||
|
|
||
|
// Define the full task definition for the service.
|
||
|
def := &ecs.RegisterTaskDefinitionInput{
|
||
|
Family: aws.String(srv.ServiceName),
|
||
|
ExecutionRoleArn: aws.String(srv.AwsEcsExecutionRole.Arn()),
|
||
|
TaskRoleArn: aws.String(srv.AwsEcsTaskRole.Arn()),
|
||
|
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"}),
|
||
|
}
|
||
|
|
||
|
return def, nil
|
||
|
}
|
||
|
|
||
|
|
||
|
// Define the ServiceContext for the web-api that will be used for build and deploy.
|
||
|
case ServiceWebApi:
|
||
|
|
||
|
ctx := &ServiceContext{
|
||
|
ServiceConfig: webApiCfg,
|
||
|
}
|
||
|
|
||
|
// Define the service task definition with a function to enable use of config and deploy details.
|
||
|
ctx.AwsEcsTaskDefinition = func(cfg *devdeploy.Config, srv *devdeploy.DeployService) (*ecs.RegisterTaskDefinitionInput, error) {
|
||
|
|
||
|
// 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{
|
||
|
&ecs.PortMapping{
|
||
|
HostPort: aws.Int64(80),
|
||
|
Protocol: aws.String("tcp"),
|
||
|
ContainerPort: aws.Int64(80),
|
||
|
},
|
||
|
},
|
||
|
Cpu: aws.Int64(128),
|
||
|
MemoryReservation: aws.Int64(128),
|
||
|
Environment: baseEnvVals(cfg, srv),
|
||
|
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{
|
||
|
&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 && !ctx.EnableElb {
|
||
|
container1.PortMappings = append(container1.PortMappings, &ecs.PortMapping{
|
||
|
HostPort: aws.Int64(443),
|
||
|
Protocol: aws.String("tcp"),
|
||
|
ContainerPort: aws.Int64(443),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Append env vars for the service task.
|
||
|
container1.Environment = append(container1.Environment,
|
||
|
ecsKeyValuePair("SERVICE_NAME", srv.ServiceName),
|
||
|
ecsKeyValuePair("PROJECT_NAME", cfg.ProjectName),
|
||
|
|
||
|
// Use placeholders for these environment variables that will be replaced with devdeploy.DeployServiceToTargetEnv
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_HOST", "{HTTP_HOST}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_HTTPS_HOST", "{HTTPS_HOST}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_ENABLE_HTTPS", "{HTTPS_ENABLED}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_BASE_URL", "{APP_BASE_URL}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_HOST_NAMES", "{HOST_NAMES}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_S3_ENABLED", "{STATIC_FILES_S3_ENABLED}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_S3_PREFIX", "{STATIC_FILES_S3_PREFIX}"),
|
||
|
ecsKeyValuePair("WEB_API_SERVICE_STATICFILES_CLOUDFRONT_ENABLED", "{STATIC_FILES_CLOUDFRONT_ENABLED}"),
|
||
|
ecsKeyValuePair("WEB_API_REDIS_HOST", "{CACHE_HOST}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_HOST", "{DB_HOST}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_USERNAME", "{DB_USER}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_PASSWORD", "{DB_PASS}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_DATABASE", "{DB_DATABASE}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_DRIVER", "{DB_DRIVER}"),
|
||
|
ecsKeyValuePair("WEB_API_DB_DISABLE_TLS", "{DB_DISABLE_TLS}"),
|
||
|
ecsKeyValuePair("WEB_API_AWS_S3_BUCKET_PRIVATE", "{AWS_S3_BUCKET_PRIVATE}"),
|
||
|
ecsKeyValuePair("WEB_API_AWS_S3_BUCKET_PUBLIC", "{AWS_S3_BUCKET_PUBLIC}"),
|
||
|
ecsKeyValuePair(devdeploy.ENV_KEY_ROUTE53_UPDATE_TASK_IPS, "{ROUTE53_UPDATE_TASK_IPS}"),
|
||
|
ecsKeyValuePair(devdeploy.ENV_KEY_ROUTE53_ZONES, "{ROUTE53_ZONES}"),
|
||
|
)
|
||
|
|
||
|
// Define the full task definition for the service.
|
||
|
def := &ecs.RegisterTaskDefinitionInput{
|
||
|
Family: aws.String(srv.ServiceName),
|
||
|
ExecutionRoleArn: aws.String(srv.AwsEcsExecutionRole.Arn()),
|
||
|
TaskRoleArn: aws.String(srv.AwsEcsTaskRole.Arn()),
|
||
|
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"}),
|
||
|
}
|
||
|
|
||
|
return def, nil
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
return nil, errors.Wrapf(devdeploy.ErrInvalidService,
|
||
|
"No service context defined for service '%s'",
|
||
|
serviceName)
|
||
|
}
|
||
|
|
||
|
return ctx, nil
|
||
|
}
|
||
|
|
||
|
// BuildService handles defining all the information needed to a service with docker and push to AWS ECR.
|
||
|
func (ctx *ServiceContext) Build(log *log.Logger, noCache, noPush bool) (*devdeploy.BuildService, error) {
|
||
|
|
||
|
log.Printf("Define build for service '%s'.", ctx.Name)
|
||
|
log.Printf("\tUsing release tag %s.", ctx.ReleaseTag)
|
||
|
|
||
|
srv := &devdeploy.BuildService{
|
||
|
ServiceName: ctx.Name,
|
||
|
ReleaseTag: ctx.ReleaseTag,
|
||
|
BuildDir: ctx.BuildDir,
|
||
|
Dockerfile: ctx.Dockerfile,
|
||
|
DockerBuildContext: ctx.DockerBuildContext,
|
||
|
NoCache: noCache,
|
||
|
NoPush: noPush,
|
||
|
}
|
||
|
|
||
|
return srv, nil
|
||
|
}
|
||
|
|
||
|
// DeployService handles defining all the information needed to deploy a service to AWS ECS.
|
||
|
func (ctx *ServiceContext) Deploy(log *log.Logger, cfg *devdeploy.Config) (*devdeploy.DeployService, error) {
|
||
|
|
||
|
log.Printf("Define deploy for service '%s'.", ctx.Name)
|
||
|
log.Printf("\tUsing release tag %s.", ctx.ReleaseTag)
|
||
|
|
||
|
// Start to define all the information for the service from the service context.
|
||
|
srv := &devdeploy.DeployService{
|
||
|
ServiceName: ctx.Name,
|
||
|
ReleaseTag: ctx.ReleaseTag,
|
||
|
EnableHTTPS: ctx.EnableHTTPS,
|
||
|
ServiceHostPrimary: ctx.ServiceHostPrimary,
|
||
|
ServiceHostNames: ctx.ServiceHostNames,
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
}
|
||
|
|
||
|
// The S3 prefix used to upload static files served to public.
|
||
|
if ctx.StaticFilesS3Enable {
|
||
|
srv.StaticFilesS3Prefix = filepath.Join(cfg.AwsS3BucketPublicKeyPrefix, srv.ReleaseTag, "static")
|
||
|
}
|
||
|
|
||
|
// Determine the Dockerfile for the service.
|
||
|
if ctx.Dockerfile != "" {
|
||
|
srv.Dockerfile = ctx.Dockerfile
|
||
|
log.Printf("\t\tUsing docker file '%s'.", srv.Dockerfile)
|
||
|
} else {
|
||
|
var err error
|
||
|
srv.Dockerfile, err = devdeploy.FindServiceDockerFile(cfg.ProjectRoot, srv.ServiceName)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
log.Printf("\t\tFound service docker file '%s'.", srv.Dockerfile)
|
||
|
}
|
||
|
|
||
|
// Set the service directory.
|
||
|
if ctx.ServiceDir == "" {
|
||
|
ctx.ServiceDir = filepath.Dir(srv.Dockerfile)
|
||
|
}
|
||
|
srv.StaticFilesDir = filepath.Join(ctx.ServiceDir, "static")
|
||
|
|
||
|
// 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.ServiceName),
|
||
|
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 ctx.EnableElb {
|
||
|
// AwsElbLoadBalancer defines if the service should use an elastic load balancer.
|
||
|
srv.AwsElbLoadBalancer = &devdeploy.AwsElbLoadBalancer{
|
||
|
Name: fmt.Sprintf("%s-%s-%s", cfg.Env, srv.AwsEcsCluster.ClusterName, srv.ServiceName),
|
||
|
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.TargetGroup = &devdeploy.AwsElbTargetGroup{
|
||
|
Name: fmt.Sprintf("%s-http", srv.ServiceName),
|
||
|
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.TargetGroup.Protocol,
|
||
|
srv.AwsElbLoadBalancer.TargetGroup.Name)
|
||
|
|
||
|
// Set ECS configs based on specified env.
|
||
|
if cfg.Env == "prod" {
|
||
|
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: int64(ctx.DesiredCount),
|
||
|
EnableECSManagedTags: false,
|
||
|
HealthCheckGracePeriodSeconds: 60,
|
||
|
LaunchType: "FARGATE",
|
||
|
}
|
||
|
|
||
|
// Ensure when deploying a new service there is always at-least one running.
|
||
|
if srv.AwsEcsService.DesiredCount == 0 {
|
||
|
srv.AwsEcsService.DesiredCount = 1
|
||
|
}
|
||
|
|
||
|
// Set ECS configs based on specified env.
|
||
|
if cfg.Env == "prod" {
|
||
|
srv.AwsEcsService.DeploymentMinimumHealthyPercent = 100
|
||
|
srv.AwsEcsService.DeploymentMaximumPercent = 200
|
||
|
} else {
|
||
|
srv.AwsEcsService.DeploymentMinimumHealthyPercent = 100
|
||
|
srv.AwsEcsService.DeploymentMaximumPercent = 200
|
||
|
}
|
||
|
|
||
|
// AwsEcsTaskDefinition defines the details for registering a new ECS task definition.
|
||
|
taskDef, err := ctx.AwsEcsTaskDefinition(cfg, srv)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
srv.AwsEcsTaskDefinition = &devdeploy.AwsEcsTaskDefinition{
|
||
|
RegisterInput: taskDef,
|
||
|
UpdatePlaceholders: func(placeholders map[string]string) error {
|
||
|
|
||
|
// 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 err
|
||
|
}
|
||
|
|
||
|
if datadogApiKey != "" {
|
||
|
log.Println("DATADOG API Key set.")
|
||
|
} else {
|
||
|
log.Printf("DATADOG API Key NOT set.")
|
||
|
}
|
||
|
|
||
|
placeholders["{DATADOG_APIKEY}"] = datadogApiKey
|
||
|
|
||
|
// When the datadog API key is empty, don't force the container to be essential have have the whole task fail.
|
||
|
if datadogApiKey != "" {
|
||
|
placeholders["{DATADOG_ESSENTIAL}"] = "true"
|
||
|
} else {
|
||
|
placeholders["{DATADOG_ESSENTIAL}"] = "false"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
},
|
||
|
}
|
||
|
|
||
|
log.Printf("\t\tDeploying task to '%s'.", ctx.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 {
|
||
|
|
||
|
cfgCtx, err := NewConfigContext(targetEnv, awsCredentials)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
cfg, err := cfgCtx.Config(log)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
srvCtx, err := NewServiceContext(serviceName, cfg)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Override the release tag if set.
|
||
|
if releaseTag != "" {
|
||
|
srvCtx.ReleaseTag = releaseTag
|
||
|
}
|
||
|
|
||
|
details, err := srvCtx.Build(log, noCache, noPush)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// servicePath is used to copy the service specific code in the Dockerfile.
|
||
|
servicePath, err := filepath.Rel(cfg.ProjectRoot, srvCtx.ServiceDir)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// commitRef is used by main.go:build constant.
|
||
|
commitRef := getCommitRef()
|
||
|
if commitRef == "" {
|
||
|
commitRef = srvCtx.ReleaseTag
|
||
|
}
|
||
|
|
||
|
details.BuildArgs = map[string]string{
|
||
|
"service_path": servicePath,
|
||
|
"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(details, "", " ")
|
||
|
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, details)
|
||
|
}
|
||
|
|
||
|
// 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 {
|
||
|
|
||
|
cfgCtx, err := NewConfigContext(targetEnv, awsCredentials)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
cfg, err := cfgCtx.Config(log)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
srvCtx, err := NewServiceContext(serviceName, cfg)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Override the release tag if set.
|
||
|
if releaseTag != "" {
|
||
|
srvCtx.ReleaseTag = releaseTag
|
||
|
}
|
||
|
|
||
|
details, err := srvCtx.Deploy(log, cfg)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return devdeploy.DeployServiceToTargetEnv(log, cfg, details)
|
||
|
}
|
||
|
|
||
|
// ecsKeyValuePair returns an *ecs.KeyValuePair
|
||
|
func ecsKeyValuePair(name, value string) *ecs.KeyValuePair {
|
||
|
return &ecs.KeyValuePair{
|
||
|
Name: aws.String(name),
|
||
|
Value: aws.String(value),
|
||
|
}
|
||
|
}
|