2019-08-21 14:31:28 -08:00
package config
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
2019-08-28 23:21:12 -08:00
"strconv"
2019-08-21 14:31:28 -08:00
"strings"
"github.com/aws/aws-sdk-go/aws"
2019-08-27 22:26:42 -08:00
"github.com/aws/aws-sdk-go/service/applicationautoscaling"
2019-08-21 14:31:28 -08:00
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/iancoleman/strcase"
2019-08-21 19:28:23 -08:00
"github.com/pkg/errors"
2019-08-21 14:31:28 -08:00
"gitlab.com/geeks-accelerator/oss/devops/pkg/devdeploy"
"gopkg.in/go-playground/validator.v9"
)
2019-08-27 22:26:42 -08:00
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
)
2019-08-21 14:31:28 -08:00
// Service define the name of a service.
type Service = string
var (
2019-08-28 23:21:12 -08:00
ServiceWebApi Service = "web-api"
ServiceWebApp Service = "web-app"
2019-08-21 14:31:28 -08:00
)
// List of service names used by main.go for help.
var ServiceNames = [ ] Service {
ServiceWebApi ,
ServiceWebApp ,
}
2019-08-26 04:48:43 -08:00
// ServiceContext defines the settings for a service.
type ServiceContext struct {
2019-08-21 14:31:28 -08:00
// Required flags.
2019-08-21 19:28:23 -08:00
Name string ` validate:"required" example:"web-api" `
ServiceHostPrimary string ` validate:"required" example:"example-project.com" `
2019-08-27 22:26:42 -08:00
DesiredCount int64 ` validate:"required" example:"2" `
2019-08-21 19:28:23 -08:00
ServiceDir string ` validate:"required" `
Dockerfile string ` validate:"required" example:"./cmd/web-api/Dockerfile" `
ReleaseTag string ` validate:"required" `
2019-08-21 14:31:28 -08:00
// Optional flags.
ServiceHostNames [ ] string ` validate:"omitempty" example:"subdomain.example-project.com" `
EnableHTTPS bool ` validate:"omitempty" example:"false" `
StaticFilesS3Enable bool ` validate:"omitempty" example:"false" `
2019-08-26 05:21:55 -08:00
DockerBuildDir string ` validate:"omitempty" `
2019-08-21 14:31:28 -08:00
DockerBuildContext string ` validate:"omitempty" example:"." `
}
2019-08-26 04:48:43 -08:00
// NewServiceContext returns the Service for a service that is configured for the target deployment env.
func NewServiceContext ( serviceName string , cfg * devdeploy . Config ) ( ServiceContext , error ) {
2019-08-21 14:31:28 -08:00
// =========================================================================
// New service context.
2019-08-26 04:48:43 -08:00
srv := ServiceContext {
2019-08-21 14:31:28 -08:00
Name : serviceName ,
DesiredCount : 1 ,
DockerBuildContext : "." ,
2019-08-21 18:01:02 -08:00
ServiceDir : filepath . Join ( cfg . ProjectRoot , "cmd" , serviceName ) ,
2019-08-21 14:31:28 -08:00
// 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 {
2019-08-21 19:28:23 -08:00
srv . ServiceHostPrimary = "example.saasstartupkit.com"
2019-08-21 14:31:28 -08:00
// 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 {
2019-08-21 19:28:23 -08:00
srv . ServiceHostPrimary = fmt . Sprintf ( "%s.example.saasstartupkit.com" , cfg . Env )
2019-08-21 14:31:28 -08:00
}
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 :
2019-08-26 04:48:43 -08:00
return ServiceContext { } , errors . Wrapf ( devdeploy . ErrInvalidService ,
2019-08-21 19:28:23 -08:00
"No service config defined for service '%s'" ,
serviceName )
2019-08-21 14:31:28 -08:00
}
// 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.
2019-08-26 04:48:43 -08:00
func ( c ServiceContext ) BaseUrl ( ) string {
2019-08-21 14:31:28 -08:00
var schema string
if c . EnableHTTPS {
schema = "https"
} else {
schema = "http"
}
return fmt . Sprintf ( "%s://%s/" , schema , c . ServiceHostPrimary )
}
2019-08-26 04:48:43 -08:00
// NewService returns the ProjectService for a service that is configured for the target deployment env.
2019-09-03 15:00:47 -08:00
func NewService ( log * log . Logger , serviceName string , cfg * devdeploy . Config ) ( * devdeploy . ProjectService , error ) {
2019-08-21 14:31:28 -08:00
2019-08-26 05:21:55 -08:00
ctx , err := NewServiceContext ( serviceName , cfg )
2019-08-21 14:31:28 -08:00
if err != nil {
return nil , err
}
// =========================================================================
2019-08-26 04:48:43 -08:00
// New project service.
srv := & devdeploy . ProjectService {
Name : serviceName ,
CodeDir : filepath . Join ( cfg . ProjectRoot , "cmd" , serviceName ) ,
DockerBuildDir : ctx . DockerBuildDir ,
DockerBuildContext : "." ,
2019-08-26 05:21:55 -08:00
EnableHTTPS : ctx . EnableHTTPS ,
2019-08-21 14:31:28 -08:00
ServiceHostPrimary : ctx . ServiceHostPrimary ,
2019-08-26 05:21:55 -08:00
ServiceHostNames : ctx . ServiceHostNames ,
ReleaseTag : ctx . ReleaseTag ,
2019-08-26 05:44:09 -08:00
DockerBuildArgs : make ( map [ string ] string ) ,
2019-08-21 14:31:28 -08:00
}
2019-08-26 04:48:43 -08:00
if srv . DockerBuildDir == "" {
srv . DockerBuildDir = cfg . ProjectRoot
2019-08-21 14:31:28 -08:00
}
2019-08-26 04:48:43 -08:00
// Sync static files to S3 will be enabled when the S3 prefix is defined.
2019-08-21 14:31:28 -08:00
if ctx . StaticFilesS3Enable {
2019-08-26 04:48:43 -08:00
srv . StaticFilesS3Prefix = filepath . Join ( cfg . AwsS3BucketPublicKeyPrefix , ctx . ReleaseTag , "static" )
2019-08-21 14:31:28 -08:00
}
2019-08-27 22:26:42 -08:00
// =========================================================================
// Service settings based on target env.
var enableElb bool
if cfg . Env == EnvStage || cfg . Env == EnvProd {
if cfg . Env == EnvProd && EnableServiceElb {
enableElb = true
}
}
2019-08-26 04:48:43 -08:00
// =========================================================================
// Shared details that could be applied to all task definitions.
2019-08-21 14:31:28 -08:00
// 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 {
2019-08-26 04:48:43 -08:00
LogGroupName : fmt . Sprintf ( "logs/env_%s/aws/ecs/cluster_%s/service_%s" , cfg . Env , srv . AwsEcsCluster . ClusterName , srv . Name ) ,
2019-08-21 14:31:28 -08:00
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.
2019-08-27 22:26:42 -08:00
if enableElb {
2019-08-21 14:31:28 -08:00
// AwsElbLoadBalancer defines if the service should use an elastic load balancer.
srv . AwsElbLoadBalancer = & devdeploy . AwsElbLoadBalancer {
2019-08-27 22:26:42 -08:00
Name : fmt . Sprintf ( "%s-%s" , cfg . Env , ctx . Name ) ,
2019-08-21 14:31:28 -08:00
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.
2019-08-26 04:48:43 -08:00
srv . AwsElbLoadBalancer . TargetGroups = [ ] * devdeploy . AwsElbTargetGroup {
2020-01-18 18:46:23 -09:00
{
2019-08-26 04:48:43 -08:00
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" ,
} ,
2019-08-21 14:31:28 -08:00
}
log . Printf ( "\t\t\tSet ELB Target Group Name for %s to '%s'." ,
2019-08-26 04:48:43 -08:00
srv . AwsElbLoadBalancer . TargetGroups [ 0 ] . Protocol ,
srv . AwsElbLoadBalancer . TargetGroups [ 0 ] . Name )
2019-08-21 14:31:28 -08:00
// Set ECS configs based on specified env.
2019-08-28 23:21:12 -08:00
if cfg . Env == EnvProd {
2019-08-21 14:31:28 -08:00
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 ,
2019-08-27 22:26:42 -08:00
DesiredCount : ctx . DesiredCount ,
2019-08-21 14:31:28 -08:00
EnableECSManagedTags : false ,
HealthCheckGracePeriodSeconds : 60 ,
LaunchType : "FARGATE" ,
}
// Set ECS configs based on specified env.
2019-08-28 23:21:12 -08:00
if cfg . Env == EnvProd {
2019-08-21 14:31:28 -08:00
srv . AwsEcsService . DeploymentMinimumHealthyPercent = 100
srv . AwsEcsService . DeploymentMaximumPercent = 200
} else {
2019-08-28 23:21:12 -08:00
srv . AwsEcsService . DeploymentMinimumHealthyPercent = 0
srv . AwsEcsService . DeploymentMaximumPercent = 100
2019-08-21 14:31:28 -08:00
}
2019-08-27 22:26:42 -08:00
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 ) ,
} ,
}
}
2019-08-26 04:48:43 -08:00
// Load the web-app config for the web-api can reference it's hostname.
webAppCtx , err := NewServiceContext ( ServiceWebApp , cfg )
2019-08-21 14:31:28 -08:00
if err != nil {
return nil , err
}
2019-08-26 04:48:43 -08:00
// 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
}
2019-08-21 14:31:28 -08:00
2019-08-28 23:21:12 -08:00
// 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 {
2020-01-18 18:46:23 -09:00
{
2019-08-28 23:21:12 -08:00
ContainerPort : aws . Int64 ( 8125 ) ,
} ,
2020-01-18 18:46:23 -09:00
{
2019-08-28 23:21:12 -08:00
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" ) ,
} ,
}
}
2019-08-26 04:48:43 -08:00
// 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" )
}
2019-08-21 14:31:28 -08:00
2019-08-26 04:48:43 -08:00
var ciPipelineURL string
if id := os . Getenv ( "CI_PIPELINE_ID" ) ; id != "" {
ciPipelineURL = strings . TrimRight ( GitLabProjectBaseUrl , "/" ) + "/pipelines/" + os . Getenv ( "CI_PIPELINE_ID" )
}
2019-08-28 23:21:12 -08:00
envVars := [ ] * ecs . KeyValuePair {
2019-08-26 04:48:43 -08:00
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" ) ,
}
2019-08-28 23:21:12 -08:00
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
2019-08-26 04:48:43 -08:00
}
// =========================================================================
// 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 {
2020-01-18 18:46:23 -09:00
{
2019-08-26 04:48:43 -08:00
HostPort : aws . Int64 ( 80 ) ,
Protocol : aws . String ( "tcp" ) ,
ContainerPort : aws . Int64 ( 80 ) ,
} ,
} ,
Cpu : aws . Int64 ( 128 ) ,
MemoryReservation : aws . Int64 ( 128 ) ,
2019-08-26 05:21:55 -08:00
Environment : baseEnvVals ( ) ,
2019-08-26 04:48:43 -08:00
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 {
2020-01-18 18:46:23 -09:00
{
2019-08-26 04:48:43 -08:00
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.
2019-08-27 22:26:42 -08:00
if ctx . EnableHTTPS && ! enableElb {
2019-08-26 04:48:43 -08:00
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" } ) ,
}
2019-08-28 23:21:12 -08:00
// Append the datadog container if defined.
if ddContainer != nil {
taskDef . ContainerDefinitions = append ( taskDef . ContainerDefinitions , ddContainer )
}
2019-08-26 04:48:43 -08:00
srv . AwsEcsTaskDefinition = & devdeploy . AwsEcsTaskDefinition {
RegisterInput : taskDef ,
2019-08-28 23:21:12 -08:00
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 ) ,
2019-08-29 01:37:52 -08:00
ecsKeyValuePair ( "WEB_APP_DB_USER" , vars . DbUser ) ,
ecsKeyValuePair ( "WEB_APP_DB_PASS" , vars . DbPass ) ,
2019-08-28 23:21:12 -08:00
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 ) ,
2020-01-19 22:55:29 -09:00
ecsKeyValuePair ( "WEB_APP_SERVICE_MINIFY" , "true" ) ,
2019-08-28 23:21:12 -08:00
)
2019-08-29 01:37:52 -08:00
// 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" ) )
}
2019-08-28 23:21:12 -08:00
// 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" ) )
2019-08-21 14:31:28 -08:00
}
2019-08-26 04:48:43 -08:00
return nil
} ,
}
// Define the ServiceContext for the web-api that will be used for build and deploy.
case ServiceWebApi :
2019-08-21 14:31:28 -08:00
2019-08-26 04:48:43 -08:00
// 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 {
2020-01-18 18:46:23 -09:00
{
2019-08-26 04:48:43 -08:00
HostPort : aws . Int64 ( 80 ) ,
Protocol : aws . String ( "tcp" ) ,
ContainerPort : aws . Int64 ( 80 ) ,
} ,
} ,
Cpu : aws . Int64 ( 128 ) ,
MemoryReservation : aws . Int64 ( 128 ) ,
2019-08-26 05:21:55 -08:00
Environment : baseEnvVals ( ) ,
2019-08-26 04:48:43 -08:00
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 {
2020-01-18 18:46:23 -09:00
{
2019-08-26 04:48:43 -08:00
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.
2019-08-27 22:26:42 -08:00
if ctx . EnableHTTPS && ! enableElb {
2019-08-26 04:48:43 -08:00
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" } ) ,
}
2019-08-28 23:21:12 -08:00
// Append the datadog container if defined.
if ddContainer != nil {
taskDef . ContainerDefinitions = append ( taskDef . ContainerDefinitions , ddContainer )
}
2019-08-26 04:48:43 -08:00
srv . AwsEcsTaskDefinition = & devdeploy . AwsEcsTaskDefinition {
RegisterInput : taskDef ,
2019-08-28 23:21:12 -08:00
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 ) ,
2019-08-29 01:37:52 -08:00
ecsKeyValuePair ( "WEB_API_DB_USER" , vars . DbUser ) ,
ecsKeyValuePair ( "WEB_API_DB_PASS" , vars . DbPass ) ,
2019-08-28 23:21:12 -08:00
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 ) ,
2020-01-19 22:55:29 -09:00
ecsKeyValuePair ( "WEB_API_SERVICE_MINIFY" , "true" ) ,
2019-08-28 23:21:12 -08:00
)
// 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" ) )
2019-08-21 14:31:28 -08:00
}
2019-08-26 04:48:43 -08:00
return nil
} ,
}
2019-08-26 05:44:09 -08:00
srv . DockerBuildArgs [ "swagInit" ] = "1"
2019-08-26 04:48:43 -08:00
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" )
2019-08-21 14:31:28 -08:00
}
2019-08-26 04:48:43 -08:00
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 )
}
2019-08-21 14:31:28 -08:00
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 {
2019-08-26 04:48:43 -08:00
cfg , err := NewConfig ( log , targetEnv , awsCredentials )
2019-08-21 14:31:28 -08:00
if err != nil {
return err
}
2019-09-03 15:00:47 -08:00
targetSvc , err := NewService ( log , serviceName , cfg )
2019-08-21 14:31:28 -08:00
if err != nil {
return err
}
// Override the release tag if set.
if releaseTag != "" {
2019-08-26 04:48:43 -08:00
targetSvc . ReleaseTag = releaseTag
2019-08-21 14:31:28 -08:00
}
2019-08-26 04:48:43 -08:00
// Append build args to be used for all services.
if targetSvc . DockerBuildArgs == nil {
targetSvc . DockerBuildArgs = make ( map [ string ] string )
2019-08-21 14:31:28 -08:00
}
// servicePath is used to copy the service specific code in the Dockerfile.
2019-08-26 04:48:43 -08:00
codePath , err := filepath . Rel ( cfg . ProjectRoot , targetSvc . CodeDir )
2019-08-21 14:31:28 -08:00
if err != nil {
return err
}
2019-08-26 04:48:43 -08:00
targetSvc . DockerBuildArgs [ "code_path" ] = codePath
2019-08-21 14:31:28 -08:00
// commitRef is used by main.go:build constant.
commitRef := getCommitRef ( )
if commitRef == "" {
2019-08-26 04:48:43 -08:00
commitRef = targetSvc . ReleaseTag
2019-08-21 14:31:28 -08:00
}
2019-08-26 04:48:43 -08:00
targetSvc . DockerBuildArgs [ "commit_ref" ] = commitRef
2019-08-21 14:31:28 -08:00
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 ) )
2019-08-26 04:48:43 -08:00
detailsJSON , err := json . MarshalIndent ( targetSvc , "" , " " )
2019-08-21 14:31:28 -08:00
if err != nil {
log . Fatalf ( "BuildServiceForTargetEnv : Marshalling details to JSON : %+v" , err )
}
log . Printf ( "BuildServiceForTargetEnv : details : %v\n" , string ( detailsJSON ) )
return nil
}
2019-08-26 04:48:43 -08:00
return devdeploy . BuildServiceForTargetEnv ( log , cfg , targetSvc , noCache , noPush )
2019-08-21 14:31:28 -08:00
}
// 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 {
2019-08-26 04:48:43 -08:00
cfg , err := NewConfig ( log , targetEnv , awsCredentials )
2019-08-21 14:31:28 -08:00
if err != nil {
return err
}
2019-09-03 15:00:47 -08:00
targetSvc , err := NewService ( log , serviceName , cfg )
2019-08-21 14:31:28 -08:00
if err != nil {
return err
}
// Override the release tag if set.
if releaseTag != "" {
2019-08-26 04:48:43 -08:00
targetSvc . ReleaseTag = releaseTag
2019-08-21 14:31:28 -08:00
}
2019-08-26 04:48:43 -08:00
return devdeploy . DeployServiceToTargetEnv ( log , cfg , targetSvc )
2019-08-21 14:31:28 -08:00
}
// ecsKeyValuePair returns an *ecs.KeyValuePair
func ecsKeyValuePair ( name , value string ) * ecs . KeyValuePair {
return & ecs . KeyValuePair {
Name : aws . String ( name ) ,
Value : aws . String ( value ) ,
}
}