1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-06 23:46:29 +02:00

3559 lines
134 KiB
Go
Raw Normal View History

package cicd
2019-07-07 12:52:55 -08:00
import (
2019-07-10 00:17:35 -08:00
"compress/gzip"
"context"
"crypto/md5"
"encoding/base64"
2019-07-08 12:21:22 -08:00
"encoding/json"
2019-07-07 12:52:55 -08:00
"fmt"
2019-07-10 00:17:35 -08:00
"io/ioutil"
2019-07-08 19:13:41 -08:00
"log"
"net/url"
2019-07-08 19:13:41 -08:00
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
2019-07-10 00:17:35 -08:00
"time"
2019-07-08 19:13:41 -08:00
"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/internal/schema"
2019-07-13 20:55:45 -08:00
"geeks-accelerator/oss/saas-starter-kit/tools/devops/internal/retry"
2019-07-08 12:21:22 -08:00
"github.com/aws/aws-sdk-go/aws"
2019-07-07 12:52:55 -08:00
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/acm"
2019-07-15 00:37:07 -08:00
"github.com/aws/aws-sdk-go/service/cloudfront"
2019-07-08 19:13:41 -08:00
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2"
2019-07-08 12:21:22 -08:00
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/aws/aws-sdk-go/service/ecs"
2019-07-11 00:58:45 -08:00
"github.com/aws/aws-sdk-go/service/elasticache"
2019-07-08 19:13:41 -08:00
"github.com/aws/aws-sdk-go/service/elbv2"
2019-07-08 12:21:22 -08:00
"github.com/aws/aws-sdk-go/service/iam"
2019-07-11 00:58:45 -08:00
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/aws/aws-sdk-go/service/s3"
2019-07-07 12:52:55 -08:00
"github.com/aws/aws-sdk-go/service/secretsmanager"
2019-07-11 00:58:45 -08:00
"github.com/aws/aws-sdk-go/service/servicediscovery"
"github.com/bobesa/go-domain-util/domainutil"
2019-07-08 12:21:22 -08:00
"github.com/iancoleman/strcase"
"github.com/lib/pq"
2019-07-11 00:58:45 -08:00
"github.com/pborman/uuid"
2019-07-07 12:52:55 -08:00
"github.com/pkg/errors"
"github.com/urfave/cli"
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
"gopkg.in/go-playground/validator.v9"
2019-07-07 12:52:55 -08:00
)
// ServiceDeployFlags defines the flags used for executing a service deployment.
type ServiceDeployFlags struct {
// Required flags.
ServiceName string `validate:"required" example:"web-api"`
Env string `validate:"oneof=dev stage prod" example:"dev"`
// Optional flags.
2019-07-14 20:13:56 -08:00
EnableHTTPS bool `validate:"omitempty" example:"false"`
ServiceHostPrimary string `validate:"omitempty" example:"example-project.com"`
ServiceHostNames cli.StringSlice `validate:"omitempty" example:"subdomain.example-project.com"`
S3BucketPrivateName string `validate:"omitempty" example:"saas-example-project-private"`
S3BucketPublicName string `validate:"omitempty" example:"saas-example-project-public"`
S3BucketPublicCloudfront bool `validate:"omitempty" example:"false"`
ProjectRoot string `validate:"omitempty" example:"."`
ProjectName string ` validate:"omitempty" example:"example-project"`
DockerFile string `validate:"omitempty" example:"./cmd/web-api/Dockerfile"`
EnableLambdaVPC bool `validate:"omitempty" example:"false"`
EnableEcsElb bool `validate:"omitempty" example:"false"`
2019-07-14 19:13:09 -08:00
2019-07-14 20:13:56 -08:00
StaticFilesS3Enable bool `validate:"omitempty" example:"false"`
StaticFilesImgResizeEnable bool `validate:"omitempty" example:"false"`
2019-07-14 19:13:09 -08:00
RecreateService bool `validate:"omitempty" example:"false"`
}
// serviceDeployRequest defines the details needed to execute a service deployment.
type serviceDeployRequest struct {
*serviceRequest
EnableHTTPS bool `validate:"omitempty"`
ServiceHostPrimary string `validate:"omitempty,required_with=EnableHTTPS,fqdn"`
ServiceHostNames []string `validate:"omitempty,dive,fqdn"`
EcrRepositoryName string `validate:"required"`
EcsClusterName string `validate:"required"`
EcsCluster *ecs.CreateClusterInput
EcsServiceName string `validate:"required"`
EcsServiceDesiredCount int64 `validate:"required"`
EcsServiceMinimumHealthyPercent *int64 `validate:"omitempty"`
EcsServiceMaximumPercent *int64 `validate:"omitempty"`
EscServiceHealthCheckGracePeriodSeconds *int64 `validate:"omitempty"`
EcsExecutionRoleName string `validate:"required"`
EcsExecutionRole *iam.CreateRoleInput
EcsExecutionRolePolicyArns []string `validate:"required"`
EcsTaskRoleName string `validate:"required"`
EcsTaskRole *iam.CreateRoleInput
EcsTaskPolicyName string `validate:"required"`
EcsTaskPolicy *iam.CreatePolicyInput
EcsTaskPolicyDocument IamPolicyDocument
Ec2SecurityGroupName string `validate:"required"`
Ec2SecurityGroup *ec2.CreateSecurityGroupInput
GitlabRunnerEc2SecurityGroupName string `validate:"required"`
CloudWatchLogGroupName string `validate:"required"`
CloudWatchLogGroup *cloudwatchlogs.CreateLogGroupInput
2019-07-14 19:13:09 -08:00
S3BucketTempPrefix string `validate:"required_with=S3BucketPrivateName S3BucketPublicName"`
S3BucketPrivateName string `validate:"omitempty"`
S3BucketPublicName string `validate:"omitempty"`
S3BucketPublicKeyPrefix string `validate:"omitempty"`
S3Buckets []S3Bucket
2019-07-14 20:13:56 -08:00
CloudfrontPublic *cloudfront.DistributionConfig
StaticFilesS3Enable bool `validate:"omitempty"`
StaticFilesS3Prefix string `validate:"omitempty"`
StaticFilesImgResizeEnable bool `validate:"omitempty"`
EnableEcsElb bool `validate:"omitempty"`
ElbLoadBalancerName string `validate:"omitempty"`
ElbDeregistrationDelay *int `validate:"omitempty"`
ElbLoadBalancer *elbv2.CreateLoadBalancerInput
ElbTargetGroupName string `validate:"omitempty"`
ElbTargetGroup *elbv2.CreateTargetGroupInput
VpcPublicName string `validate:"omitempty"`
VpcPublic *ec2.CreateVpcInput
VpcPublicSubnets []*ec2.CreateSubnetInput
EnableLambdaVPC bool `validate:"omitempty"`
RecreateService bool `validate:"omitempty"`
SDNamepsace *servicediscovery.CreatePrivateDnsNamespaceInput
SDService *servicediscovery.CreateServiceInput
CacheCluster *elasticache.CreateCacheClusterInput
CacheClusterParameter []*elasticache.ParameterNameValue
DBCluster *rds.CreateDBClusterInput
DBInstance *rds.CreateDBInstanceInput
flags ServiceDeployFlags
}
// NewServiceDeployRequest generates a new request for executing deployment of a single service for a given set of CLI flags.
func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*serviceDeployRequest, error) {
2019-07-07 12:52:55 -08:00
2019-07-13 20:50:00 -08:00
// Validates specified CLI flags map to struct successfully.
2019-07-08 19:13:41 -08:00
log.Println("Validate flags.")
{
errs := validator.New().Struct(flags)
if errs != nil {
return nil, errs
}
log.Printf("\t%s\tFlags ok.", tests.Success)
}
2019-07-07 12:52:55 -08:00
2019-07-13 20:50:00 -08:00
// Generate a deploy request using CLI flags and AWS credentials.
2019-07-08 19:13:41 -08:00
log.Println("Generate deploy request.")
var req serviceDeployRequest
2019-07-08 12:21:22 -08:00
{
// Define new service request.
sr := &serviceRequest{
ServiceName: flags.ServiceName,
Env: flags.Env,
ProjectRoot: flags.ProjectRoot,
ProjectName: flags.ProjectName,
DockerFile: flags.DockerFile,
}
if err := sr.init(log); err != nil {
return nil, err
}
req = serviceDeployRequest{
serviceRequest: sr,
2019-07-14 19:13:09 -08:00
EnableHTTPS: flags.EnableHTTPS,
ServiceHostPrimary: flags.ServiceHostPrimary,
ServiceHostNames: flags.ServiceHostNames,
2019-07-14 20:13:56 -08:00
StaticFilesS3Enable: flags.StaticFilesS3Enable,
StaticFilesImgResizeEnable: flags.StaticFilesImgResizeEnable,
2019-07-14 19:13:09 -08:00
S3BucketPrivateName: flags.S3BucketPrivateName,
S3BucketPublicName: flags.S3BucketPublicName,
2019-07-14 20:13:56 -08:00
EnableLambdaVPC: flags.EnableLambdaVPC,
EnableEcsElb: flags.EnableEcsElb,
RecreateService: flags.RecreateService,
flags: flags,
2019-07-08 19:13:41 -08:00
}
2019-07-13 20:50:00 -08:00
// Set default configuration values. Primarily setting default values for all the AWS services:
// - AWS S3 bucket settings
// - AWS ECR repository settings
// - AWS ECS cluster, service, task, and task policy settings
// - AWS CloudWatch group settings
// - AWS EC2 security groups
// - AWS ECS settings and enable ELB
// - AWS Elastic Cache settings for a Redis cache cluster
// - AWS RDS configuration for Postgres via Aurora
2019-07-12 23:28:53 -08:00
log.Println("\tSet defaults.")
2019-07-08 12:21:22 -08:00
{
2019-07-12 23:28:53 -08:00
// When only service host names are set, choose the first item as the primary host.
if req.ServiceHostPrimary == "" && len(req.ServiceHostNames) > 0 {
req.ServiceHostPrimary = req.ServiceHostNames[0]
log.Printf("\t\t\tSet Service Primary Host to '%s'.", req.ServiceHostPrimary)
}
2019-07-11 00:58:45 -08:00
// S3 temp prefix used by services for short term storage. A lifecycle policy will be used for expiration.
req.S3BucketTempPrefix = "tmp/"
// Defines a life cycle policy to expire keys for the temp directory.
bucketLifecycleTempRule := &s3.LifecycleRule{
2019-07-11 14:46:05 -08:00
ID: aws.String("Rule for : " + req.S3BucketTempPrefix),
2019-07-11 00:58:45 -08:00
Status: aws.String("Enabled"),
Filter: &s3.LifecycleRuleFilter{
Prefix: aws.String(req.S3BucketTempPrefix),
},
Expiration: &s3.LifecycleExpiration{
// Indicates the lifetime, in days, of the objects that are subject to the rule.
// The value must be a non-zero positive integer.
Days: aws.Int64(1),
},
// Specifies the days since the initiation of an incomplete multipart upload
// that Amazon S3 will wait before permanently removing all parts of the upload.
// For more information, see Aborting Incomplete Multipart Uploads Using a Bucket
// Lifecycle Policy (https://docs.aws.amazon.com/AmazonS3/latest/dev/mpuoverview.html#mpu-abort-incomplete-mpu-lifecycle-config)
// in the Amazon Simple Storage Service Developer Guide.
AbortIncompleteMultipartUpload: &s3.AbortIncompleteMultipartUpload{
DaysAfterInitiation: aws.Int64(1),
},
}
// Defines the S3 Buckets used for all services.
2019-07-12 23:28:53 -08:00
// The public S3 Bucket used to serve static files and other assets.
if req.S3BucketPublicName != "" {
req.S3Buckets = append(req.S3Buckets,
S3Bucket{
Name: req.S3BucketPublicName,
Input: &s3.CreateBucketInput{
Bucket: aws.String(req.S3BucketPublicName),
},
LifecycleRules: []*s3.LifecycleRule{bucketLifecycleTempRule},
CORSRules: []*s3.CORSRule{
&s3.CORSRule{
// Headers that are specified in the Access-Control-Request-Headers header.
// These headers are allowed in a preflight OPTIONS request. In response to
// any preflight OPTIONS request, Amazon S3 returns any requested headers that
// are allowed.
// AllowedHeaders: aws.StringSlice([]string{}),
// An HTTP method that you allow the origin to execute. Valid values are GET,
// PUT, HEAD, POST, and DELETE.
//
// AllowedMethods is a required field
AllowedMethods: aws.StringSlice([]string{"GET", "POST"}),
2019-07-11 00:58:45 -08:00
2019-07-12 23:28:53 -08:00
// One or more origins you want customers to be able to access the bucket from.
//
// AllowedOrigins is a required field
AllowedOrigins: aws.StringSlice([]string{"*"}),
2019-07-11 00:58:45 -08:00
2019-07-12 23:28:53 -08:00
// One or more headers in the response that you want customers to be able to
// access from their applications (for example, from a JavaScript XMLHttpRequest
// object).
// ExposeHeaders: aws.StringSlice([]string{}),
2019-07-11 00:58:45 -08:00
2019-07-12 23:28:53 -08:00
// The time in seconds that your browser is to cache the preflight response
// for the specified resource.
// MaxAgeSeconds: aws.Int64(),
},
2019-07-11 00:58:45 -08:00
},
2019-07-12 23:28:53 -08:00
})
2019-07-14 20:13:56 -08:00
2019-07-15 00:37:07 -08:00
// The S3 key prefix used as the origin when cloud front is enabled.
if req.S3BucketPublicKeyPrefix == "" {
req.S3BucketPublicKeyPrefix = "/public"
}
2019-07-14 21:26:19 -08:00
2019-07-15 00:37:07 -08:00
if flags.S3BucketPublicCloudfront {
allowedMethods := &cloudfront.AllowedMethods{
Items: aws.StringSlice([]string{"HEAD", "GET"}),
}
allowedMethods.Quantity = aws.Int64(int64(len(allowedMethods.Items)))
2019-07-15 00:37:07 -08:00
cacheMethods := &cloudfront.CachedMethods{
Items: aws.StringSlice([]string{"HEAD", "GET"}),
}
cacheMethods.Quantity = aws.Int64(int64(len(cacheMethods.Items)))
allowedMethods.SetCachedMethods(cacheMethods)
2019-07-15 00:37:07 -08:00
domainId := "S3-" + req.S3BucketPublicName
domainName := fmt.Sprintf("%s.s3.%s.amazonaws.com", req.S3BucketPublicName, req.AwsCreds.Region)
2019-07-14 21:26:19 -08:00
2019-07-15 00:37:07 -08:00
origins := &cloudfront.Origins{
Items: []*cloudfront.Origin{
&cloudfront.Origin{
Id: aws.String(domainId),
DomainName: aws.String(domainName),
OriginPath: aws.String(req.S3BucketPublicKeyPrefix),
S3OriginConfig: &cloudfront.S3OriginConfig{
OriginAccessIdentity: aws.String(""),
},
CustomHeaders: &cloudfront.CustomHeaders{
Quantity: aws.Int64(0),
},
},
},
2019-07-15 00:37:07 -08:00
}
origins.Quantity = aws.Int64(int64(len(origins.Items)))
2019-07-14 21:26:19 -08:00
2019-07-14 20:13:56 -08:00
req.CloudfrontPublic = &cloudfront.DistributionConfig{
2019-07-15 00:37:07 -08:00
Comment: aws.String(""),
Enabled: aws.Bool(true),
HttpVersion: aws.String("http2"),
2019-07-14 20:13:56 -08:00
IsIPV6Enabled: aws.Bool(true),
DefaultCacheBehavior: &cloudfront.DefaultCacheBehavior{
TargetOriginId: aws.String(domainId),
2019-07-14 21:26:19 -08:00
AllowedMethods: allowedMethods,
2019-07-15 00:37:07 -08:00
Compress: aws.Bool(true),
DefaultTTL: aws.Int64(1209600),
MinTTL: aws.Int64(604800),
MaxTTL: aws.Int64(31536000),
2019-07-14 21:26:19 -08:00
ForwardedValues: &cloudfront.ForwardedValues{
2019-07-15 00:37:07 -08:00
QueryString: aws.Bool(true),
Cookies: &cloudfront.CookiePreference{
Forward: aws.String("none"),
},
},
TrustedSigners: &cloudfront.TrustedSigners{
Enabled: aws.Bool(false),
Quantity: aws.Int64(0),
2019-07-14 21:26:19 -08:00
},
2019-07-15 00:37:07 -08:00
ViewerProtocolPolicy: aws.String("allow-all"),
2019-07-14 20:13:56 -08:00
},
2019-07-14 21:26:19 -08:00
Origins: origins,
2019-07-14 20:13:56 -08:00
ViewerCertificate: &cloudfront.ViewerCertificate{
2019-07-15 00:37:07 -08:00
CertificateSource: aws.String("cloudfront"),
MinimumProtocolVersion: aws.String("TLSv1"),
2019-07-14 21:26:19 -08:00
CloudFrontDefaultCertificate: aws.Bool(true),
2019-07-14 20:13:56 -08:00
},
2019-07-15 00:37:07 -08:00
PriceClass: aws.String("PriceClass_All"),
2019-07-14 20:13:56 -08:00
CallerReference: aws.String("devops-deploy"),
}
}
2019-07-12 23:28:53 -08:00
}
2019-07-11 00:58:45 -08:00
2019-07-12 23:28:53 -08:00
// The private S3 Bucket used to persist data for services.
if req.S3BucketPrivateName != "" {
req.S3Buckets = append(req.S3Buckets,
S3Bucket{
Name: req.S3BucketPrivateName,
Input: &s3.CreateBucketInput{
Bucket: aws.String(req.S3BucketPrivateName),
},
LifecycleRules: []*s3.LifecycleRule{bucketLifecycleTempRule},
PublicAccessBlock: &s3.PublicAccessBlockConfiguration{
// Specifies whether Amazon S3 should block public access control lists (ACLs)
// for this bucket and objects in this bucket. Setting this element to TRUE
// causes the following behavior:
//
// * PUT Bucket acl and PUT Object acl calls fail if the specified ACL is
// public.
//
// * PUT Object calls fail if the request includes a public ACL.
//
// Enabling this setting doesn't affect existing policies or ACLs.
BlockPublicAcls: aws.Bool(true),
2019-07-11 00:58:45 -08:00
2019-07-12 23:28:53 -08:00
// Specifies whether Amazon S3 should block public bucket policies for this
// bucket. Setting this element to TRUE causes Amazon S3 to reject calls to
// PUT Bucket policy if the specified bucket policy allows public access.
//
// Enabling this setting doesn't affect existing bucket policies.
BlockPublicPolicy: aws.Bool(true),
2019-07-11 00:58:45 -08:00
2019-07-12 23:28:53 -08:00
// Specifies whether Amazon S3 should restrict public bucket policies for this
// bucket. Setting this element to TRUE restricts access to this bucket to only
// AWS services and authorized users within this account if the bucket has a
// public policy.
//
// Enabling this setting doesn't affect previously stored bucket policies, except
// that public and cross-account access within any public bucket policy, including
// non-public delegation to specific accounts, is blocked.
RestrictPublicBuckets: aws.Bool(true),
// Specifies whether Amazon S3 should ignore public ACLs for this bucket and
// objects in this bucket. Setting this element to TRUE causes Amazon S3 to
// ignore all public ACLs on this bucket and objects in this bucket.
//
// Enabling this setting doesn't affect the persistence of any existing ACLs
// and doesn't prevent new public ACLs from being set.
IgnorePublicAcls: aws.Bool(true),
},
Policy: func() string {
// Add a bucket policy to enable exports from Cloudwatch Logs for the private S3 bucket.
policyResource := strings.Trim(filepath.Join(req.S3BucketPrivateName, req.S3BucketTempPrefix), "/")
return fmt.Sprintf(`{
2019-07-11 00:58:45 -08:00
"Version": "2012-10-17",
"Statement": [
{
"Action": "s3:GetBucketAcl",
"Effect": "Allow",
"Resource": "arn:aws:s3:::%s",
"Principal": { "Service": "logs.%s.amazonaws.com" }
},
{
"Action": "s3:PutObject" ,
"Effect": "Allow",
"Resource": "arn:aws:s3:::%s/*",
"Condition": { "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" } },
"Principal": { "Service": "logs.%s.amazonaws.com" }
}
]
}`, req.S3BucketPrivateName, req.AwsCreds.Region, policyResource, req.AwsCreds.Region)
2019-07-12 23:28:53 -08:00
}(),
})
2019-07-11 00:58:45 -08:00
}
2019-07-14 19:13:09 -08:00
// The S3 prefix used to upload static files served to public.
if req.StaticFilesS3Prefix == "" {
req.StaticFilesS3Prefix = filepath.Join(req.S3BucketPublicKeyPrefix, releaseTag(req.Env, req.ServiceName), "static")
}
2019-07-08 19:13:41 -08:00
// Set default AWS ECR Repository Name.
req.EcrRepositoryName = ecrRepositoryName(req.ProjectName)
log.Printf("\t\t\tSet ECR Repository Name to '%s'.", req.EcrRepositoryName)
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
// Set default AWS ECS Cluster Name.
req.EcsClusterName = req.ProjectName + "-" + req.Env
2019-07-11 00:58:45 -08:00
req.EcsCluster = &ecs.CreateClusterInput{
ClusterName: aws.String(req.EcsClusterName),
Tags: []*ecs.Tag{
&ecs.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
&ecs.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
},
}
log.Printf("\t\t\tSet ECS Cluster Name to '%s'.", req.EcsClusterName)
2019-07-08 19:13:41 -08:00
// Set default AWS ECS Service Name.
req.EcsServiceName = req.ServiceName + "-" + req.Env
log.Printf("\t\t\tSet ECS Service Name to '%s'.", req.EcsServiceName)
2019-07-08 12:21:22 -08:00
// Set default AWS ECS Execution Role Name.
req.EcsExecutionRoleName = fmt.Sprintf("ecsExecutionRole%s%s", req.ProjectNameCamel(), strcase.ToCamel(req.Env))
2019-07-11 00:58:45 -08:00
req.EcsExecutionRole = &iam.CreateRoleInput{
RoleName: aws.String(req.EcsExecutionRoleName),
Description: aws.String(fmt.Sprintf("Provides access to other AWS service resources that are required to run Amazon ECS tasks for %s. ", req.ProjectName)),
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ecs-tasks.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}"),
Tags: []*iam.Tag{
&iam.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
&iam.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
},
}
req.EcsExecutionRolePolicyArns = []string{
"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
}
log.Printf("\t\t\tSet ECS Execution Role Name to '%s'.", req.EcsExecutionRoleName)
// Set default AWS ECS Task Role Name.
req.EcsTaskRoleName = fmt.Sprintf("ecsTaskRole%s%s", req.ProjectNameCamel(), strcase.ToCamel(req.Env))
2019-07-11 00:58:45 -08:00
req.EcsTaskRole = &iam.CreateRoleInput{
RoleName: aws.String(req.EcsTaskRoleName),
Description: aws.String(fmt.Sprintf("Allows ECS tasks for %s to call AWS services on your behalf.", req.ProjectName)),
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ecs-tasks.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}"),
Tags: []*iam.Tag{
&iam.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
&iam.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
},
}
log.Printf("\t\t\tSet ECS Task Role Name to '%s'.", req.EcsTaskRoleName)
// Set default AWS ECS Task Policy Name.
req.EcsTaskPolicyName = fmt.Sprintf("%s%sServices", req.ProjectNameCamel(), strcase.ToCamel(req.Env))
2019-07-10 16:24:10 -08:00
req.EcsTaskPolicy = &iam.CreatePolicyInput{
2019-07-11 14:46:05 -08:00
PolicyName: aws.String(req.EcsTaskPolicyName),
Description: aws.String(fmt.Sprintf("Defines access for %s services. ", req.ProjectName)),
2019-07-10 16:24:10 -08:00
}
log.Printf("\t\t\tSet ECS Task Policy Name to '%s'.", req.EcsTaskPolicyName)
2019-07-08 12:21:22 -08:00
2019-07-10 16:24:10 -08:00
// EcsTaskPolicyDocument defines the default document policy used to create the AWS ECS Task Policy. If the
// policy already exists, the permissions will be used to add new required actions, but not for removal.
// The policy document grants the permissions required for deployed services to access AWS services.
req.EcsTaskPolicyDocument = IamPolicyDocument{
Version: "2012-10-17",
Statement: []IamStatementEntry{
IamStatementEntry{
Sid: "DefaultServiceAccess",
Effect: "Allow",
Action: []string{
"s3:HeadBucket",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface",
"ecs:ListTasks",
2019-07-11 00:58:45 -08:00
"ecs:DescribeServices",
2019-07-10 16:24:10 -08:00
"ecs:DescribeTasks",
"ec2:DescribeNetworkInterfaces",
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets",
"ecs:UpdateService",
"ses:SendEmail",
2019-07-11 00:58:45 -08:00
"secretsmanager:ListSecretVersionIds",
"secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret",
"secretsmanager:UpdateSecret",
"secretsmanager:RestoreSecret",
"secretsmanager:DeleteSecret",
2019-07-10 16:24:10 -08:00
},
Resource: "*",
},
IamStatementEntry{
Sid: "ServiceInvokeLambda",
Effect: "Allow",
Action: []string{
"iam:GetRole",
"lambda:InvokeFunction",
"lambda:ListVersionsByFunction",
"lambda:GetFunction",
"lambda:InvokeAsync",
"lambda:GetFunctionConfiguration",
"iam:PassRole",
"lambda:GetAlias",
"lambda:GetPolicy",
},
Resource: []string{
"arn:aws:iam:::role/*",
"arn:aws:lambda:::function:*",
},
},
IamStatementEntry{
Sid: "datadoglambda",
Effect: "Allow",
Action: []string{
"cloudwatch:Get*",
"cloudwatch:List*",
"ec2:Describe*",
"support:*",
"tag:GetResources",
"tag:GetTagKeys",
"tag:GetTagValues",
},
Resource: "*",
},
},
}
2019-07-08 19:13:41 -08:00
// Set default Cloudwatch Log Group Name.
req.CloudWatchLogGroupName = fmt.Sprintf("logs/env_%s/aws/ecs/cluster_%s/service_%s", req.Env, req.EcsClusterName, req.ServiceName)
2019-07-11 00:58:45 -08:00
req.CloudWatchLogGroup = &cloudwatchlogs.CreateLogGroupInput{
LogGroupName: aws.String(req.CloudWatchLogGroupName),
Tags: map[string]*string{
awsTagNameProject: aws.String(req.ProjectName),
awsTagNameEnv: aws.String(req.Env),
},
}
log.Printf("\t\t\tSet CloudWatch Log Group Name to '%s'.", req.CloudWatchLogGroupName)
2019-07-08 19:13:41 -08:00
// Set default EC2 Security Group Name.
req.Ec2SecurityGroupName = req.EcsClusterName
2019-07-11 00:58:45 -08:00
req.Ec2SecurityGroup = &ec2.CreateSecurityGroupInput{
// The name of the security group.
// Constraints: Up to 255 characters in length. Cannot start with sg-.
// Constraints for EC2-Classic: ASCII characters
// Constraints for EC2-VPC: a-z, A-Z, 0-9, spaces, and ._-:/()#,@[]+=&;{}!$*
// GroupName is a required field
GroupName: aws.String(req.Ec2SecurityGroupName),
2019-07-11 00:58:45 -08:00
// A description for the security group. This is informational only.
// Constraints: Up to 255 characters in length
// Constraints for EC2-Classic: ASCII characters
// Constraints for EC2-VPC: a-z, A-Z, 0-9, spaces, and ._-:/()#,@[]+=&;{}!$*
// Description is a required field
Description: aws.String(fmt.Sprintf("Security group for %s running on ECS cluster %s", req.ProjectName, req.EcsClusterName)),
}
log.Printf("\t\t\tSet ECS Security Group Name to '%s'.", req.Ec2SecurityGroupName)
2019-07-10 16:24:10 -08:00
// Set the name of the EC2 Security Group used by the gitlab runner. This is used to ensure the security
// group defined above has access to the RDS cluster/instance and can thus handle schema migrations.
req.GitlabRunnerEc2SecurityGroupName = "gitlab-runner"
// Set default ELB Load Balancer Name when ELB is enabled.
if req.EnableEcsElb {
if !strings.Contains(req.EcsClusterName, req.Env) && !strings.Contains(req.ServiceName, req.Env) {
// When a custom cluster name is provided and/or service name, ensure the ELB contains the current env.
req.ElbLoadBalancerName = fmt.Sprintf("%s-%s-%s", req.EcsClusterName, req.ServiceName, req.Env)
} else {
// Default value when when custom cluster/service name is supplied.
req.ElbLoadBalancerName = fmt.Sprintf("%s-%s", req.EcsClusterName, req.ServiceName)
}
2019-07-11 00:58:45 -08:00
req.ElbLoadBalancer = &elbv2.CreateLoadBalancerInput{
// The name of the load balancer.
// This name must be unique per region per account, can have a maximum of 32
// characters, must contain only alphanumeric characters or hyphens, must not
// begin or end with a hyphen, and must not begin with "internal-".
// Name is a required field
Name: aws.String(req.ElbLoadBalancerName),
// [Application Load Balancers] The type of IP addresses used by the subnets
// for your load balancer. The possible values are ipv4 (for IPv4 addresses)
// and dualstack (for IPv4 and IPv6 addresses).
2019-07-11 02:52:10 -08:00
IpAddressType: aws.String("ipv4"),
2019-07-11 00:58:45 -08:00
// The nodes of an Internet-facing load balancer have public IP addresses. The
// DNS name of an Internet-facing load balancer is publicly resolvable to the
// public IP addresses of the nodes. Therefore, Internet-facing load balancers
// can route requests from clients over the internet.
// The nodes of an internal load balancer have only private IP addresses. The
// DNS name of an internal load balancer is publicly resolvable to the private
// IP addresses of the nodes. Therefore, internal load balancers can only route
// requests from clients with access to the VPC for the load balancer.
2019-07-11 02:52:10 -08:00
Scheme: aws.String("internet-facing"),
2019-07-11 00:58:45 -08:00
// The type of load balancer.
Type: aws.String("application"),
// One or more tags to assign to the load balancer.
Tags: []*elbv2.Tag{
&elbv2.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
&elbv2.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
},
}
log.Printf("\t\t\tSet ELB Name to '%s'.", req.ElbLoadBalancerName)
2019-07-11 00:58:45 -08:00
req.ElbTargetGroupName = fmt.Sprintf("%s-http", req.EcsServiceName)
req.ElbTargetGroup = &elbv2.CreateTargetGroupInput{
// The name of the target group.
// This name must be unique per region per account, can have a maximum of 32
// characters, must contain only alphanumeric characters or hyphens, and must
// not begin or end with a hyphen.
// Name is a required field
2019-07-11 14:46:05 -08:00
Name: aws.String(req.ElbTargetGroupName),
// The port on which the targets receive traffic. This port is used unless you
// specify a port override when registering the target. If the target is a Lambda
// function, this parameter does not apply.
Port: aws.Int64(80),
// The protocol to use for routing traffic to the targets. For Application Load
// Balancers, the supported protocols are HTTP and HTTPS. For Network Load Balancers,
// the supported protocols are TCP, TLS, UDP, or TCP_UDP. A TCP_UDP listener
// must be associated with a TCP_UDP target group. If the target is a Lambda
// function, this parameter does not apply.
Protocol: aws.String("HTTP"),
// Indicates whether health checks are enabled. If the target type is lambda,
// health checks are disabled by default but can be enabled. If the target type
// is instance or ip, health checks are always enabled and cannot be disabled.
HealthCheckEnabled: aws.Bool(true),
// The approximate amount of time, in seconds, between health checks of an individual
// target. For HTTP and HTTPS health checks, the range is 5–300 seconds. For
// TCP health checks, the supported values are 10 and 30 seconds. If the target
// type is instance or ip, the default is 30 seconds. If the target type is
// lambda, the default is 35 seconds.
HealthCheckIntervalSeconds: aws.Int64(30),
// [HTTP/HTTPS health checks] The ping path that is the destination on the targets
// for health checks. The default is /.
HealthCheckPath: aws.String("/ping"),
// The protocol the load balancer uses when performing health checks on targets.
// For Application Load Balancers, the default is HTTP. For Network Load Balancers,
// the default is TCP. The TCP protocol is supported for health checks only
// if the protocol of the target group is TCP, TLS, UDP, or TCP_UDP. The TLS,
// UDP, and TCP_UDP protocols are not supported for health checks.
HealthCheckProtocol: aws.String("HTTP"),
// The amount of time, in seconds, during which no response from a target means
// a failed health check. For target groups with a protocol of HTTP or HTTPS,
// the default is 5 seconds. For target groups with a protocol of TCP or TLS,
// this value must be 6 seconds for HTTP health checks and 10 seconds for TCP
// and HTTPS health checks. If the target type is lambda, the default is 30
// seconds.
HealthCheckTimeoutSeconds: aws.Int64(5),
// The number of consecutive health checks successes required before considering
// an unhealthy target healthy. For target groups with a protocol of HTTP or
// HTTPS, the default is 5. For target groups with a protocol of TCP or TLS,
// the default is 3. If the target type is lambda, the default is 5.
HealthyThresholdCount: aws.Int64(3),
// The number of consecutive health check failures required before considering
// a target unhealthy. For target groups with a protocol of HTTP or HTTPS, the
// default is 2. For target groups with a protocol of TCP or TLS, this value
// must be the same as the healthy threshold count. If the target type is lambda,
// the default is 2.
UnhealthyThresholdCount: aws.Int64(3),
// [HTTP/HTTPS health checks] The HTTP codes to use when checking for a successful
// response from a target.
Matcher: &elbv2.Matcher{
HttpCode: aws.String("200"),
},
// The type of target that you must specify when registering targets with this
// target group. You can't specify targets for a target group using more than
// one target type.
//
// * instance - Targets are specified by instance ID. This is the default
// value. If the target group protocol is UDP or TCP_UDP, the target type
// must be instance.
//
// * ip - Targets are specified by IP address. You can specify IP addresses
// from the subnets of the virtual private cloud (VPC) for the target group,
// the RFC 1918 range (10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16), and
// the RFC 6598 range (100.64.0.0/10). You can't specify publicly routable
// IP addresses.
//
// * lambda - The target groups contains a single Lambda function.
TargetType: aws.String("ip"),
}
2019-07-11 14:46:05 -08:00
log.Printf("\t\t\tSet ELB Target Group Name to '%s'.", req.ElbTargetGroupName)
}
2019-07-08 19:13:41 -08:00
// Set ECS configs based on specified env.
if flags.Env == "prod" {
req.EcsServiceMinimumHealthyPercent = aws.Int64(100)
req.EcsServiceMaximumPercent = aws.Int64(200)
2019-07-08 19:13:41 -08:00
req.ElbDeregistrationDelay = aws.Int(300)
2019-07-08 19:13:41 -08:00
} else {
req.EcsServiceMinimumHealthyPercent = aws.Int64(100)
req.EcsServiceMaximumPercent = aws.Int64(200)
2019-07-08 19:13:41 -08:00
// force staging to deploy immediately without waiting for connections to drain
req.ElbDeregistrationDelay = aws.Int(0)
2019-07-08 12:21:22 -08:00
}
2019-07-11 00:58:45 -08:00
if req.EcsServiceDesiredCount == 0 {
req.EcsServiceDesiredCount = 1
}
2019-07-08 12:21:22 -08:00
2019-07-11 00:58:45 -08:00
req.EscServiceHealthCheckGracePeriodSeconds = aws.Int64(60)
2019-07-10 00:17:35 -08:00
2019-07-10 16:24:10 -08:00
// Service Discovery Namespace settings.
req.SDNamepsace = &servicediscovery.CreatePrivateDnsNamespaceInput{
2019-07-11 14:46:05 -08:00
Name: aws.String(req.EcsClusterName),
2019-07-10 16:24:10 -08:00
Description: aws.String(fmt.Sprintf("Private DNS namespace used for services running on the ECS Cluster %s", req.EcsClusterName)),
// A unique string that identifies the request and that allows failed CreatePrivateDnsNamespace
// requests to be retried without the risk of executing the operation twice.
// CreatorRequestId can be any unique string, for example, a date/time stamp.
2019-07-14 20:13:56 -08:00
CreatorRequestId: aws.String("devops-deploy"),
2019-07-10 16:24:10 -08:00
}
// Service Discovery Service settings.
req.SDService = &servicediscovery.CreateServiceInput{
2019-07-11 14:46:05 -08:00
Name: aws.String(req.EcsServiceName),
2019-07-10 16:24:10 -08:00
Description: aws.String(fmt.Sprintf("Service %s running on the ECS Cluster %s", req.EcsServiceName, req.EcsClusterName)),
// A complex type that contains information about the Amazon Route 53 records
// that you want AWS Cloud Map to create when you register an instance.
DnsConfig: &servicediscovery.DnsConfig{
DnsRecords: []*servicediscovery.DnsRecord{
{
// The amount of time, in seconds, that you want DNS resolvers to cache the
// settings for this record.
TTL: aws.Int64(300),
// The type of the resource, which indicates the type of value that Route 53
// returns in response to DNS queries.
Type: aws.String("A"),
},
},
},
// A complex type that contains information about an optional custom health
// check.
//
// If you specify a health check configuration, you can specify either HealthCheckCustomConfig
// or HealthCheckConfig but not both.
HealthCheckCustomConfig: &servicediscovery.HealthCheckCustomConfig{
// The number of 30-second intervals that you want Cloud Map to wait after receiving
// an UpdateInstanceCustomHealthStatus request before it changes the health
// status of a service instance. For example, suppose you specify a value of
// 2 for FailureTheshold, and then your application sends an UpdateInstanceCustomHealthStatus
// request. Cloud Map waits for approximately 60 seconds (2 x 30) before changing
// the status of the service instance based on that request.
//
// Sending a second or subsequent UpdateInstanceCustomHealthStatus request with
// the same value before FailureThreshold x 30 seconds has passed doesn't accelerate
// the change. Cloud Map still waits FailureThreshold x 30 seconds after the
// first request to make the change.
FailureThreshold: aws.Int64(3),
},
// A unique string that identifies the request and that allows failed CreatePrivateDnsNamespace
// requests to be retried without the risk of executing the operation twice.
// CreatorRequestId can be any unique string, for example, a date/time stamp.
2019-07-14 20:13:56 -08:00
CreatorRequestId: aws.String("devops-deploy"),
2019-07-10 16:24:10 -08:00
}
2019-07-10 00:17:35 -08:00
// Elastic Cache settings for a Redis cache cluster. Could defined different settings by env.
req.CacheCluster = &elasticache.CreateCacheClusterInput{
2019-07-11 14:46:05 -08:00
AutoMinorVersionUpgrade: aws.Bool(true),
CacheClusterId: aws.String(req.ProjectName + "-" + req.Env),
CacheNodeType: aws.String("cache.t2.micro"),
CacheSubnetGroupName: aws.String("default"),
Engine: aws.String("redis"),
EngineVersion: aws.String("5.0.4"),
NumCacheNodes: aws.Int64(1),
Port: aws.Int64(6379),
SnapshotRetentionLimit: aws.Int64(7),
2019-07-10 00:17:35 -08:00
}
2019-07-10 04:11:39 -08:00
// Recommended to be set to allkeys-lru to avoid OOM since redis will be used as an ephemeral store.
req.CacheClusterParameter = []*elasticache.ParameterNameValue{
&elasticache.ParameterNameValue{
2019-07-11 14:46:05 -08:00
ParameterName: aws.String("maxmemory-policy"),
2019-07-10 04:11:39 -08:00
ParameterValue: aws.String("allkeys-lru"),
},
}
// RDS cluster is used for Aurora which is limited to regions and db instance types so not good for example.
req.DBCluster = nil
// RDS settings for a Postgres database Instance. Could defined different settings by env.
2019-07-11 14:46:05 -08:00
req.DBInstance = &rds.CreateDBInstanceInput{
DBInstanceIdentifier: aws.String(dBInstanceIdentifier(req.ProjectName, req.Env)),
2019-07-11 14:46:05 -08:00
DBName: aws.String("shared"),
Engine: aws.String("postgres"),
MasterUsername: aws.String("god"),
MasterUserPassword: aws.String("mypassword"), // When empty auto generated.
Port: aws.Int64(5432),
DBInstanceClass: aws.String("db.t2.small"),
AllocatedStorage: aws.Int64(20),
MultiAZ: aws.Bool(false),
PubliclyAccessible: aws.Bool(false),
StorageEncrypted: aws.Bool(true),
BackupRetentionPeriod: aws.Int64(7),
2019-07-10 04:11:39 -08:00
EnablePerformanceInsights: aws.Bool(false),
2019-07-11 14:46:05 -08:00
AutoMinorVersionUpgrade: aws.Bool(true),
CopyTagsToSnapshot: aws.Bool(true),
2019-07-10 04:11:39 -08:00
Tags: []*rds.Tag{
{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
},
}
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tDefaults set.", tests.Success)
}
2019-07-08 19:13:41 -08:00
log.Println("\tValidate request.")
errs := validator.New().Struct(req)
if errs != nil {
return nil, errs
}
log.Printf("\t%s\tNew request generated.", tests.Success)
}
return &req, nil
2019-07-08 19:13:41 -08:00
}
2019-07-13 20:50:00 -08:00
// Run is the main entrypoint for deploying a service for a given target environment.
2019-07-08 19:13:41 -08:00
func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
2019-07-11 00:58:45 -08:00
2019-07-10 00:17:35 -08:00
startTime := time.Now()
2019-07-10 04:52:38 -08:00
// Load the AWS ECR repository. Try to find by name else create new one.
2019-07-08 12:21:22 -08:00
{
2019-07-10 04:52:38 -08:00
log.Println("ECR - Get or create repository.")
svc := ecr.New(req.awsSession())
2019-07-08 12:21:22 -08:00
2019-07-13 20:50:00 -08:00
// First try to find ECR repository by name.
var awsRepo *ecr.Repository
2019-07-08 12:21:22 -08:00
descRes, err := svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{
RepositoryNames: []*string{aws.String(req.EcrRepositoryName)},
2019-07-08 12:21:22 -08:00
})
if err != nil {
// The repository should have been created by build or manually created and should exist at this point.
return errors.Wrapf(err, "Failed to describe repository '%s'.", req.EcrRepositoryName)
2019-07-08 12:21:22 -08:00
} else if len(descRes.Repositories) > 0 {
awsRepo = descRes.Repositories[0]
2019-07-08 19:13:41 -08:00
}
log.Printf("\t\tFound: %s.", *awsRepo.RepositoryArn)
2019-07-08 12:21:22 -08:00
req.ReleaseImage = releaseImage(req.Env, req.ServiceName, *awsRepo.RepositoryUri)
if err != nil {
return err
2019-07-08 12:21:22 -08:00
}
2019-07-07 12:52:55 -08:00
log.Printf("\t\trelease image: %s", req.ReleaseImage)
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tRelease image valid.", tests.Success)
2019-07-07 12:52:55 -08:00
}
2019-07-13 20:50:00 -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.
2019-07-08 12:21:22 -08:00
var datadogApiKey string
{
2019-07-10 04:52:38 -08:00
log.Println("Datadog - Get API Key")
2019-07-13 20:50:00 -08:00
// Load Datadog API key which can be either stored in an environment variable or in AWS Secrets Manager.
2019-07-08 12:21:22 -08:00
// 1. Check env vars for [DEV|STAGE|PROD]_DD_API_KEY and DD_API_KEY
datadogApiKey = getTargetEnv(req.Env, "DD_API_KEY")
2019-07-08 12:21:22 -08:00
2019-07-13 20:50:00 -08:00
// 2. Check AWS Secrets Manager for datadog entry prefixed with target environment.
2019-07-08 12:21:22 -08:00
if datadogApiKey == "" {
prefixedSecretId := secretID(req.ProjectName, req.Env, "datadog")
2019-07-08 12:21:22 -08:00
var err error
datadogApiKey, err = GetAwsSecretValue(req.AwsCreds, prefixedSecretId)
2019-07-08 12:21:22 -08:00
if err != nil {
if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
return err
2019-07-08 12:21:22 -08:00
}
}
}
2019-07-13 20:50:00 -08:00
// 3. Check AWS Secrets Manager for Datadog entry.
2019-07-08 12:21:22 -08:00
if datadogApiKey == "" {
secretId := "DATADOG"
var err error
datadogApiKey, err = GetAwsSecretValue(req.AwsCreds, secretId)
2019-07-08 12:21:22 -08:00
if err != nil {
if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
return err
}
}
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
if datadogApiKey != "" {
log.Printf("\t%s\tAPI Key set.\n", tests.Success)
2019-07-07 12:52:55 -08:00
} else {
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tAPI Key NOT set.\n", tests.Failed)
2019-07-07 12:52:55 -08:00
}
}
2019-07-13 20:50:00 -08:00
// Helper function to tag ECS resources.
2019-07-11 00:58:45 -08:00
ec2TagResource := func(resource, name string, tags ...*ec2.Tag) error {
svc := ec2.New(req.awsSession())
ec2Tags := []*ec2.Tag{
{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
{Key: aws.String(awsTagNameName), Value: aws.String(name)},
}
if tags != nil {
for _, t := range tags {
ec2Tags = append(ec2Tags, t)
}
}
_, err := svc.CreateTags(&ec2.CreateTagsInput{
Resources: aws.StringSlice([]string{resource}),
2019-07-11 14:46:05 -08:00
Tags: ec2Tags,
2019-07-11 00:58:45 -08:00
})
if err != nil {
return errors.Wrapf(err, "failed to create tags for %s", resource)
}
return nil
}
2019-07-13 20:55:45 -08:00
_ = ec2TagResource
2019-07-11 00:58:45 -08:00
2019-07-10 04:52:38 -08:00
// Try to find the AWS Cloudwatch Log Group by name or create new one.
2019-07-08 12:21:22 -08:00
{
2019-07-10 04:52:38 -08:00
log.Println("CloudWatch Logs - Get or Create Log Group")
svc := cloudwatchlogs.New(req.awsSession())
2019-07-08 19:13:41 -08:00
// If no log group was found, create one.
var err error
2019-07-11 14:46:05 -08:00
_, err = svc.CreateLogGroup(req.CloudWatchLogGroup)
2019-07-08 12:21:22 -08:00
if err != nil {
2019-07-08 19:13:41 -08:00
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != cloudwatchlogs.ErrCodeResourceAlreadyExistsException {
return errors.Wrapf(err, "failed to create log group '%s'", req.CloudWatchLogGroupName)
2019-07-08 19:13:41 -08:00
}
2019-07-08 12:21:22 -08:00
log.Printf("\t\tFound: %s.", req.CloudWatchLogGroupName)
2019-07-08 19:13:41 -08:00
} else {
log.Printf("\t\tCreated: %s.", req.CloudWatchLogGroupName)
2019-07-08 12:21:22 -08:00
}
log.Printf("\t%s\tUsing Log Group '%s'.\n", tests.Success, req.CloudWatchLogGroupName)
2019-07-07 12:52:55 -08:00
}
2019-07-10 04:52:38 -08:00
// Try to find the AWS S3 Buckets by names or create new ones.
2019-07-10 00:17:35 -08:00
{
2019-07-10 04:52:38 -08:00
log.Println("S3 - Setup Buckets")
2019-07-10 00:17:35 -08:00
svc := s3.New(req.awsSession())
2019-07-13 20:50:00 -08:00
// Iterate through specified S3 buckets. Try to create S3 bucket for each.
// Create bucket function will return record and not create it if it already exists.
2019-07-10 00:17:35 -08:00
log.Println("\tGet or Create S3 Buckets")
2019-07-11 00:58:45 -08:00
for _, bucket := range req.S3Buckets {
_, err := svc.CreateBucket(bucket.Input)
2019-07-10 00:17:35 -08:00
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || (aerr.Code() != s3.ErrCodeBucketAlreadyExists && aerr.Code() != s3.ErrCodeBucketAlreadyOwnedByYou) {
2019-07-11 00:58:45 -08:00
return errors.Wrapf(err, "failed to create s3 bucket '%s'", bucket.Name)
2019-07-10 00:17:35 -08:00
}
2019-07-13 20:50:00 -08:00
// If bucket found during create, returns it.
2019-07-11 00:58:45 -08:00
log.Printf("\t\tFound: %s.", bucket.Name)
2019-07-10 00:17:35 -08:00
} else {
2019-07-13 20:50:00 -08:00
// If no bucket found during create, create new one.
2019-07-11 00:58:45 -08:00
log.Printf("\t\tCreated: %s.", bucket.Name)
2019-07-10 00:17:35 -08:00
}
}
2019-07-13 20:50:00 -08:00
// S3 has a delay between when one is created vs when it is available to use.
// Thus, need to iterate through each bucket and wait until it exists.
2019-07-10 00:17:35 -08:00
log.Println("\tWait for S3 Buckets to exist")
2019-07-11 00:58:45 -08:00
for _, bucket := range req.S3Buckets {
log.Printf("\t\t%s", bucket.Name)
2019-07-10 00:17:35 -08:00
err := svc.WaitUntilBucketExists(&s3.HeadBucketInput{
2019-07-11 00:58:45 -08:00
Bucket: aws.String(bucket.Name),
2019-07-10 00:17:35 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to wait for s3 bucket '%s' to exist", bucket.Name)
2019-07-10 00:17:35 -08:00
}
log.Printf("\t\t\tExists")
}
2019-07-13 20:50:00 -08:00
// Loop through each S3 bucket and configure policies.
log.Println("\tConfiguring each S3 Bucket")
2019-07-11 00:58:45 -08:00
for _, bucket := range req.S3Buckets {
log.Printf("\t\t%s", bucket.Name)
// Add all the defined lifecycle rules for the bucket.
if len(bucket.LifecycleRules) > 0 {
_, err := svc.PutBucketLifecycleConfiguration(&s3.PutBucketLifecycleConfigurationInput{
Bucket: aws.String(bucket.Name),
LifecycleConfiguration: &s3.BucketLifecycleConfiguration{
Rules: bucket.LifecycleRules,
2019-07-10 00:17:35 -08:00
},
2019-07-11 00:58:45 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to configure lifecycle rule for s3 bucket '%s'", bucket.Name)
2019-07-11 00:58:45 -08:00
}
for _, r := range bucket.LifecycleRules {
log.Printf("\t\t\tAdded lifecycle '%s'", *r.ID)
}
2019-07-10 00:17:35 -08:00
}
2019-07-11 00:58:45 -08:00
// Add all the defined CORS rules for the bucket.
if len(bucket.CORSRules) > 0 {
_, err := svc.PutBucketCors(&s3.PutBucketCorsInput{
Bucket: aws.String(bucket.Name),
2019-07-10 00:17:35 -08:00
CORSConfiguration: &s3.CORSConfiguration{
2019-07-11 00:58:45 -08:00
CORSRules: bucket.CORSRules,
2019-07-10 00:17:35 -08:00
},
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to put CORS on s3 bucket '%s'", bucket.Name)
2019-07-10 00:17:35 -08:00
}
log.Printf("\t\t\tUpdated CORS")
2019-07-11 00:58:45 -08:00
}
2019-07-10 00:17:35 -08:00
2019-07-11 00:58:45 -08:00
// Block public access for all non-public buckets.
if bucket.PublicAccessBlock != nil {
_, err := svc.PutPublicAccessBlock(&s3.PutPublicAccessBlockInput{
Bucket: aws.String(bucket.Name),
PublicAccessBlockConfiguration: bucket.PublicAccessBlock,
2019-07-10 00:17:35 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to block public access for s3 bucket '%s'", bucket.Name)
2019-07-10 00:17:35 -08:00
}
log.Printf("\t\t\tBlocked public access")
2019-07-11 00:58:45 -08:00
}
2019-07-10 00:17:35 -08:00
2019-07-11 00:58:45 -08:00
// Add the bucket policy if not empty.
if bucket.Policy != "" {
_, err := svc.PutBucketPolicy(&s3.PutBucketPolicyInput{
Bucket: aws.String(bucket.Name),
Policy: aws.String(bucket.Policy),
2019-07-10 00:17:35 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to put bucket policy for s3 bucket '%s'", bucket.Name)
2019-07-10 00:17:35 -08:00
}
log.Printf("\t\t\tUpdated bucket policy")
}
}
2019-07-13 20:50:00 -08:00
log.Printf("\t%s\tS3 buckets configured successfully.\n", tests.Success)
2019-07-10 00:17:35 -08:00
}
2019-07-14 23:53:31 -08:00
if req.CloudfrontPublic != nil {
log.Println("Cloudfront - Setup Distribution")
svc := cloudfront.New(req.awsSession())
_, err := svc.CreateDistribution(&cloudfront.CreateDistributionInput{
DistributionConfig: req.CloudfrontPublic,
2019-07-15 00:37:07 -08:00
})
2019-07-14 23:53:31 -08:00
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || (aerr.Code() != cloudfront.ErrCodeDistributionAlreadyExists) {
return errors.Wrapf(err, "Failed to create cloudfront distribution '%s'", *req.CloudfrontPublic.DefaultCacheBehavior.TargetOriginId)
}
// If bucket found during create, returns it.
log.Printf("\t\tFound: %s.", *req.CloudfrontPublic.DefaultCacheBehavior.TargetOriginId)
} else {
// If no bucket found during create, create new one.
log.Printf("\t\tCreated: %s.", *req.CloudfrontPublic.DefaultCacheBehavior.TargetOriginId)
}
}
2019-07-13 20:50:00 -08:00
// Find the default VPC and associated subnets.
// Custom subnets outside of the default VPC are not currently supported.
2019-07-11 02:52:10 -08:00
var projectSubnetsIDs []string
var projectVpcId string
2019-07-10 00:17:35 -08:00
{
2019-07-10 04:52:38 -08:00
log.Println("EC2 - Find Subnets")
2019-07-10 00:17:35 -08:00
svc := ec2.New(req.awsSession())
2019-07-13 20:50:00 -08:00
log.Println("\t\tFind all subnets are that default for each availability zone.")
2019-07-10 00:17:35 -08:00
2019-07-13 20:50:00 -08:00
// Find all subnets that are default for each availability zone.
2019-07-10 16:24:10 -08:00
var subnets []*ec2.Subnet
err := svc.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{}, func(res *ec2.DescribeSubnetsOutput, lastPage bool) bool {
for _, s := range res.Subnets {
if *s.DefaultForAz {
2019-07-10 00:17:35 -08:00
subnets = append(subnets, s)
}
2019-07-10 16:24:10 -08:00
}
return !lastPage
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to find default subnets")
2019-07-10 00:17:35 -08:00
}
2019-07-13 20:50:00 -08:00
// This deployment process requires at least one subnet.
// Each AWS account gets a default VPC and default subnet for each availability zone.
// Likely error with AWs is can not find at least one.
2019-07-10 00:17:35 -08:00
if len(subnets) == 0 {
2019-07-13 20:50:00 -08:00
return errors.New("Failed to find any subnets, expected at least 1")
2019-07-10 00:17:35 -08:00
}
2019-07-13 20:50:00 -08:00
// Iterate through subnets and make sure they belong to the same VPC as the project.
2019-07-10 00:17:35 -08:00
for _, s := range subnets {
if s.VpcId == nil {
continue
}
2019-07-11 02:52:10 -08:00
if projectVpcId == "" {
projectVpcId = *s.VpcId
} else if projectVpcId != *s.VpcId {
2019-07-13 20:50:00 -08:00
return errors.Errorf("Invalid subnet %s, all subnets should belong to the same VPC, expected %s, got %s", *s.SubnetId, projectVpcId, *s.VpcId)
2019-07-10 00:17:35 -08:00
}
2019-07-11 02:52:10 -08:00
projectSubnetsIDs = append(projectSubnetsIDs, *s.SubnetId)
2019-07-10 00:17:35 -08:00
log.Printf("\t\t\t%s", *s.SubnetId)
}
log.Printf("\t\tFound %d subnets.\n", len(subnets))
}
2019-07-10 04:52:38 -08:00
// Try to find the AWS Security Group by name or create a new one.
2019-07-10 00:17:35 -08:00
var securityGroupId string
{
2019-07-10 04:52:38 -08:00
log.Println("EC2 - Find Security Group")
2019-07-10 00:17:35 -08:00
svc := ec2.New(req.awsSession())
log.Printf("\t\tFind security group '%s'.\n", req.Ec2SecurityGroupName)
2019-07-13 20:50:00 -08:00
// Link the ID of the VPC.
2019-07-11 02:52:10 -08:00
req.Ec2SecurityGroup.VpcId = aws.String(projectVpcId)
2019-07-11 00:58:45 -08:00
2019-07-13 20:50:00 -08:00
// Find all the security groups and then parse the group name to get the Id of the security group.
var runnerSgId string
2019-07-10 00:17:35 -08:00
err := svc.DescribeSecurityGroupsPages(&ec2.DescribeSecurityGroupsInput{
GroupNames: aws.StringSlice([]string{req.Ec2SecurityGroupName, req.GitlabRunnerEc2SecurityGroupName}),
2019-07-10 00:17:35 -08:00
}, func(res *ec2.DescribeSecurityGroupsOutput, lastPage bool) bool {
for _, s := range res.SecurityGroups {
if *s.GroupName == req.Ec2SecurityGroupName {
securityGroupId = *s.GroupId
} else if *s.GroupName == req.GitlabRunnerEc2SecurityGroupName {
runnerSgId = *s.GroupId
2019-07-10 00:17:35 -08:00
}
}
return !lastPage
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != "InvalidGroup.NotFound" {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to find security group '%s'", req.Ec2SecurityGroupName)
2019-07-10 00:17:35 -08:00
}
}
if securityGroupId == "" {
// If no security group was found, create one.
2019-07-11 00:58:45 -08:00
createRes, err := svc.CreateSecurityGroup(req.Ec2SecurityGroup)
2019-07-10 00:17:35 -08:00
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create security group '%s'", req.Ec2SecurityGroupName)
2019-07-10 00:17:35 -08:00
}
securityGroupId = *createRes.GroupId
log.Printf("\t\tCreated: %s.", req.Ec2SecurityGroupName)
} else {
log.Printf("\t\tFound: %s.", req.Ec2SecurityGroupName)
}
ingressInputs := []*ec2.AuthorizeSecurityGroupIngressInput{
// Enable services to be publicly available via HTTP port 80
&ec2.AuthorizeSecurityGroupIngressInput{
IpProtocol: aws.String("tcp"),
CidrIp: aws.String("0.0.0.0/0"),
FromPort: aws.Int64(80),
ToPort: aws.Int64(80),
GroupId: aws.String(securityGroupId),
},
// Allow all services in the security group to access other services.
&ec2.AuthorizeSecurityGroupIngressInput{
SourceSecurityGroupName: aws.String(req.Ec2SecurityGroupName),
GroupId: aws.String(securityGroupId),
},
}
2019-07-13 20:50:00 -08:00
// When not using an Elastic Load Balancer, services need to support direct access via HTTPS.
2019-07-10 00:17:35 -08:00
// HTTPS is terminated via the web server and not on the Load Balancer.
2019-07-11 02:52:10 -08:00
if req.EnableHTTPS {
2019-07-11 14:15:29 -08:00
// Enable services to be publicly available via HTTPS port 443.
ingressInputs = append(ingressInputs, &ec2.AuthorizeSecurityGroupIngressInput{
IpProtocol: aws.String("tcp"),
CidrIp: aws.String("0.0.0.0/0"),
FromPort: aws.Int64(443),
ToPort: aws.Int64(443),
GroupId: aws.String(securityGroupId),
})
2019-07-10 00:17:35 -08:00
}
// When a db instance is defined, deploy needs access to the RDS instance to handle executing schema migration.
if req.DBInstance != nil {
// The gitlab runner security group is required when a db instance is defined.
if runnerSgId == "" {
return errors.Errorf("Failed to find security group '%s'", req.GitlabRunnerEc2SecurityGroupName)
}
// Enable GitLab runner to communicate with deployment created services.
ingressInputs = append(ingressInputs, &ec2.AuthorizeSecurityGroupIngressInput{
SourceSecurityGroupName: aws.String(req.GitlabRunnerEc2SecurityGroupName),
GroupId: aws.String(securityGroupId),
})
}
2019-07-10 00:17:35 -08:00
// Add all the default ingress to the security group.
for _, ingressInput := range ingressInputs {
_, err = svc.AuthorizeSecurityGroupIngress(ingressInput)
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != "InvalidPermission.Duplicate" {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to add ingress for security group '%s'", req.Ec2SecurityGroupName)
2019-07-10 00:17:35 -08:00
}
}
}
log.Printf("\t%s\tUsing Security Group '%s'.\n", tests.Success, req.Ec2SecurityGroupName)
}
2019-07-13 20:50:00 -08:00
// This is only used when service uses Aurora via RDS for serverless Postgres and database cluster is defined.
// Aurora Postgres is limited to specific AWS regions and thus not used by default.
// If an Aurora Postgres cluster is defined, ensure it exists with RDS else create a new one.
2019-07-10 04:11:39 -08:00
var dbCluster *rds.DBCluster
if req.DBCluster != nil {
log.Println("RDS - Get or Create Database Cluster")
svc := rds.New(req.awsSession())
2019-07-13 20:50:00 -08:00
// Try to find a RDS database cluster using cluster identifier.
2019-07-10 04:11:39 -08:00
descRes, err := svc.DescribeDBClusters(&rds.DescribeDBClustersInput{
DBClusterIdentifier: req.DBCluster.DBClusterIdentifier,
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != rds.ErrCodeDBClusterNotFoundFault {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to describe database cluster '%s'", *req.DBCluster.DBClusterIdentifier)
2019-07-10 04:11:39 -08:00
}
} else if len(descRes.DBClusters) > 0 {
dbCluster = descRes.DBClusters[0]
}
if dbCluster == nil {
// If no cluster was found, create one.
createRes, err := svc.CreateDBCluster(req.DBCluster)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create cluster '%s'", *req.DBCluster.DBClusterIdentifier)
2019-07-10 04:11:39 -08:00
}
dbCluster = createRes.DBCluster
log.Printf("\t\tCreated: %s", *dbCluster.DBClusterArn)
} else {
log.Printf("\t\tFound: %s", *dbCluster.DBClusterArn)
}
// The status of the cluster.
log.Printf("\t\t\tStatus: %s", *dbCluster.Status)
log.Printf("\t%s\tUsing DB Cluster '%s'.\n", tests.Success, *dbCluster.DatabaseName)
}
2019-07-13 20:50:00 -08:00
// Regardless if deployment is using Aurora or not, still need to setup database instance.
// If a database instance is defined, then ensure it exists with RDS in else create a new one.
2019-07-10 04:11:39 -08:00
var db *DB
if req.DBInstance != nil {
log.Println("RDS - Get or Create Database Instance")
// Secret ID used to store the DB username and password across deploys.
dbSecretId := secretID(req.ProjectName, req.Env, *req.DBInstance.DBInstanceIdentifier)
2019-07-10 04:11:39 -08:00
// Retrieve the current secret value if something is stored.
{
sm := secretsmanager.New(req.awsSession())
res, err := sm.GetSecretValue(&secretsmanager.GetSecretValueInput{
SecretId: aws.String(dbSecretId),
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to get value for secret id %s", dbSecretId)
2019-07-10 04:11:39 -08:00
}
} else {
err = json.Unmarshal([]byte(*res.SecretString), &db)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to json decode db credentials")
2019-07-10 04:11:39 -08:00
}
}
}
2019-07-10 04:52:38 -08:00
// Init a new RDS client.
2019-07-10 04:11:39 -08:00
svc := rds.New(req.awsSession())
2019-07-10 04:52:38 -08:00
// Always set the VPC Security Group ID dynamically with the one created/found previously.
2019-07-10 04:11:39 -08:00
req.DBInstance.VpcSecurityGroupIds = aws.StringSlice([]string{securityGroupId})
2019-07-10 04:52:38 -08:00
// When a DB cluster exists, add the identifier to the instance input. This is for creating databases with
// the storage engine of AWS Aurora.
2019-07-10 04:11:39 -08:00
if dbCluster != nil {
req.DBInstance.DBClusterIdentifier = dbCluster.DBClusterIdentifier
} else {
req.DBInstance.DBClusterIdentifier = nil
}
2019-07-10 04:52:38 -08:00
// Try to find an existing DB instance with the same identifier.
2019-07-10 04:11:39 -08:00
var dbInstance *rds.DBInstance
descRes, err := svc.DescribeDBInstances(&rds.DescribeDBInstancesInput{
DBInstanceIdentifier: req.DBInstance.DBInstanceIdentifier,
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != rds.ErrCodeDBInstanceNotFoundFault {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to describe database instance '%s'", *req.DBInstance.DBInstanceIdentifier)
2019-07-10 04:11:39 -08:00
}
} else if len(descRes.DBInstances) > 0 {
dbInstance = descRes.DBInstances[0]
}
2019-07-10 04:52:38 -08:00
// No DB instance was found, so create a new one.
2019-07-10 04:11:39 -08:00
if dbInstance == nil {
if db == nil {
2019-07-10 04:52:38 -08:00
// If master password is not set, pull from cluster or generate random.
if req.DBInstance.MasterUserPassword == nil {
if req.DBCluster.MasterUserPassword != nil && *req.DBCluster.MasterUserPassword != "" {
req.DBInstance.MasterUserPassword = req.DBCluster.MasterUserPassword
} else {
req.DBInstance.MasterUserPassword = aws.String(uuid.NewRandom().String())
}
}
2019-07-13 20:50:00 -08:00
// Only set the password right now,
// all other configuration details will be set after the database instance is created.
2019-07-10 04:11:39 -08:00
db = &DB{
Pass: *req.DBInstance.MasterUserPassword,
}
// Store the secret first in the event that create fails.
{
2019-07-10 04:52:38 -08:00
// Json encode the db details to be stored as secret text.
2019-07-10 04:11:39 -08:00
dat, err := json.Marshal(db)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to marshal db credentials")
2019-07-10 04:11:39 -08:00
}
2019-07-10 04:52:38 -08:00
// Create the new entry in AWS Secret Manager with the database password.
sm := secretsmanager.New(req.awsSession())
2019-07-10 04:11:39 -08:00
_, err = sm.CreateSecret(&secretsmanager.CreateSecretInput{
Name: aws.String(dbSecretId),
SecretString: aws.String(string(dat)),
})
if err != nil {
2019-07-10 04:52:38 -08:00
/*
2019-07-11 14:46:05 -08:00
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceExistsException {
return errors.Wrap(err, "failed to create new secret with db credentials")
}
_, err = sm.UpdateSecret(&secretsmanager.UpdateSecretInput{
SecretId: aws.String(dbSecretId),
SecretString: aws.String(string(dat)),
})
if err != nil {
return errors.Wrap(err, "failed to update secret with db credentials")
}*/
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to create new secret with db credentials")
2019-07-10 04:11:39 -08:00
}
log.Printf("\t\tStored Secret\n")
}
2019-07-10 04:52:38 -08:00
} else {
req.DBInstance.MasterUserPassword = aws.String(db.Pass)
2019-07-10 04:11:39 -08:00
}
// If no cluster was found, create one.
createRes, err := svc.CreateDBInstance(req.DBInstance)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create instance '%s'", *req.DBInstance.DBInstanceIdentifier)
2019-07-10 04:11:39 -08:00
}
dbInstance = createRes.DBInstance
log.Printf("\t\tCreated: %s", *dbInstance.DBInstanceArn)
} else {
log.Printf("\t\tFound: %s", *dbInstance.DBInstanceArn)
}
// The status of the instance.
log.Printf("\t\t\tStatus: %s", *dbInstance.DBInstanceStatus)
2019-07-13 20:50:00 -08:00
// If the instance is not active because it was recently created, wait for it to become active.
2019-07-10 04:11:39 -08:00
if *dbInstance.DBInstanceStatus != "available" {
2019-07-13 20:50:00 -08:00
log.Printf("\t\tWait for instance to become available.")
2019-07-10 04:11:39 -08:00
err = svc.WaitUntilDBInstanceAvailable(&rds.DescribeDBInstancesInput{
DBInstanceIdentifier: dbInstance.DBInstanceIdentifier,
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to wait for database instance '%s' to enter available state", *req.DBInstance.DBInstanceIdentifier)
2019-07-10 04:11:39 -08:00
}
}
2019-07-13 20:50:00 -08:00
// Update the secret with the DB instance details. This happens after DB create to help address when the
// DB instance was successfully created, but the secret failed to save. The DB details host should be empty or
2019-07-10 04:52:38 -08:00
// match the current instance endpoint.
curHost := fmt.Sprintf("%s:%d", *dbInstance.Endpoint.Address, *dbInstance.Endpoint.Port)
if curHost != db.Host {
2019-07-10 04:11:39 -08:00
2019-07-13 20:50:00 -08:00
// Copy the instance details to the DB struct.
2019-07-10 04:52:38 -08:00
db.Host = curHost
db.User = *dbInstance.MasterUsername
db.Database = *dbInstance.DBName
db.Driver = *dbInstance.Engine
db.DisableTLS = false
2019-07-10 04:11:39 -08:00
2019-07-13 20:50:00 -08:00
// Json encode the DB details to be stored as text via AWS Secrets Manager.
2019-07-10 04:52:38 -08:00
dat, err := json.Marshal(db)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to marshal db credentials")
2019-07-10 04:52:38 -08:00
}
2019-07-10 04:11:39 -08:00
2019-07-10 04:52:38 -08:00
// Update the current AWS Secret.
sm := secretsmanager.New(req.awsSession())
_, err = sm.UpdateSecret(&secretsmanager.UpdateSecretInput{
2019-07-11 14:46:05 -08:00
SecretId: aws.String(dbSecretId),
2019-07-10 04:52:38 -08:00
SecretString: aws.String(string(dat)),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to update secret with db credentials")
2019-07-10 04:52:38 -08:00
}
log.Printf("\t\tUpdate Secret\n")
// Ensure the newly created database is seeded.
log.Printf("\t\tOpen database connection")
// Register informs the sqlxtrace package of the driver that we will be using in our program.
// It uses a default service name, in the below case "postgres.db". To use a custom service
// name use RegisterWithServiceName.
sqltrace.Register(db.Driver, &pq.Driver{}, sqltrace.WithServiceName("devops:migrate"))
masterDb, err := sqlxtrace.Open(db.Driver, db.URL())
if err != nil {
return errors.WithStack(err)
}
defer masterDb.Close()
// Start the database migrations.
log.Printf("\t\tStart migrations.")
if err = schema.Migrate(masterDb, log); err != nil {
return errors.WithStack(err)
}
log.Printf("\t\tFinished migrations.")
2019-07-10 04:52:38 -08:00
}
2019-07-10 04:11:39 -08:00
log.Printf("\t%s\tUsing DB Instance '%s'.\n", tests.Success, *dbInstance.DBInstanceIdentifier)
}
2019-07-13 20:50:00 -08:00
// Setup AWS Elastic Cache cluster for Redis.
2019-07-10 00:17:35 -08:00
var cacheCluster *elasticache.CacheCluster
if req.CacheCluster != nil {
log.Println("Elastic Cache - Get or Create Cache Cluster")
// Set the security group of the cache cluster
req.CacheCluster.SecurityGroupIds = aws.StringSlice([]string{securityGroupId})
svc := elasticache.New(req.awsSession())
2019-07-13 20:50:00 -08:00
// Find Elastic Cache cluster given Id.
2019-07-10 00:17:35 -08:00
descRes, err := svc.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{
2019-07-11 14:46:05 -08:00
CacheClusterId: req.CacheCluster.CacheClusterId,
2019-07-10 00:17:35 -08:00
ShowCacheNodeInfo: aws.Bool(true),
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != elasticache.ErrCodeCacheClusterNotFoundFault {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to describe cache cluster '%s'", *req.CacheCluster.CacheClusterId)
2019-07-10 00:17:35 -08:00
}
} else if len(descRes.CacheClusters) > 0 {
cacheCluster = descRes.CacheClusters[0]
}
if cacheCluster == nil {
// If no repository was found, create one.
createRes, err := svc.CreateCacheCluster(req.CacheCluster)
if err != nil {
2019-07-11 14:46:05 -08:00
return errors.Wrapf(err, "failed to create cluster '%s'", *req.CacheCluster.CacheClusterId)
2019-07-10 00:17:35 -08:00
}
cacheCluster = createRes.CacheCluster
/*
2019-07-13 20:50:00 -08:00
// TODO: Tag cache cluster, ARN for the cache cluster when it is not readily available.
2019-07-10 00:17:35 -08:00
_, err = svc.AddTagsToResource(&elasticache.AddTagsToResourceInput{
ResourceName: ???,
Tags: []*elasticache.Tag{
{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
},
})
if err != nil {
return errors.Wrapf(err, "failed to create cluster '%s'", *req.CacheCluster.CacheClusterId)
}
*/
log.Printf("\t\tCreated: %s", *cacheCluster.CacheClusterId)
} else {
log.Printf("\t\tFound: %s", *cacheCluster.CacheClusterId)
}
// The status of the cluster.
log.Printf("\t\t\tStatus: %s", *cacheCluster.CacheClusterStatus)
2019-07-13 20:50:00 -08:00
// If the cache cluster is not active because it was recently created, wait for it to become active.
2019-07-10 00:17:35 -08:00
if *cacheCluster.CacheClusterStatus != "available" {
log.Printf("\t\tWhat for cluster to become available.")
err = svc.WaitUntilCacheClusterAvailable(&elasticache.DescribeCacheClustersInput{
CacheClusterId: req.CacheCluster.CacheClusterId,
})
2019-07-10 04:11:39 -08:00
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to wait for cache cluster '%s' to enter available state", req.CacheCluster.CacheClusterId)
2019-07-10 04:11:39 -08:00
}
2019-07-10 00:17:35 -08:00
}
2019-07-10 04:11:39 -08:00
// If there are custom cache group parameters set, then create a new group and keep them modified.
if len(req.CacheClusterParameter) > 0 {
customCacheParameterGroupName := fmt.Sprintf("%s-%s%s", strings.ToLower(req.ProjectNameCamel()), *cacheCluster.Engine, *cacheCluster.EngineVersion)
customCacheParameterGroupName = strings.Replace(customCacheParameterGroupName, ".", "-", -1)
2019-07-10 00:17:35 -08:00
// If the cache cluster is using the default parameter group, create a new custom group.
if strings.HasPrefix(*cacheCluster.CacheParameterGroup.CacheParameterGroupName, "default") {
// Lookup the group family from the current cache parameter group.
descRes, err := svc.DescribeCacheParameterGroups(&elasticache.DescribeCacheParameterGroupsInput{
CacheParameterGroupName: cacheCluster.CacheParameterGroup.CacheParameterGroupName,
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != elasticache.ErrCodeCacheClusterNotFoundFault {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to describe cache parameter group '%s'", *req.CacheCluster.CacheClusterId)
2019-07-10 00:17:35 -08:00
}
}
log.Printf("\t\tCreated custom Cache Parameter Group : %s", customCacheParameterGroupName)
_, err = svc.CreateCacheParameterGroup(&elasticache.CreateCacheParameterGroupInput{
CacheParameterGroupFamily: descRes.CacheParameterGroups[0].CacheParameterGroupFamily,
2019-07-11 14:46:05 -08:00
CacheParameterGroupName: aws.String(customCacheParameterGroupName),
Description: aws.String(fmt.Sprintf("Customized default parameter group for %s %s", *cacheCluster.Engine, *cacheCluster.EngineVersion)),
2019-07-10 00:17:35 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to cache parameter group '%s'", customCacheParameterGroupName)
2019-07-10 00:17:35 -08:00
}
log.Printf("\t\tSet Cache Parameter Group : %s", customCacheParameterGroupName)
2019-07-10 04:11:39 -08:00
updateRes, err := svc.ModifyCacheCluster(&elasticache.ModifyCacheClusterInput{
2019-07-11 14:46:05 -08:00
CacheClusterId: cacheCluster.CacheClusterId,
2019-07-10 00:17:35 -08:00
CacheParameterGroupName: aws.String(customCacheParameterGroupName),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed modify cache parameter group '%s' for cache cluster '%s'", customCacheParameterGroupName, *cacheCluster.CacheClusterId)
2019-07-10 00:17:35 -08:00
}
2019-07-10 04:11:39 -08:00
cacheCluster = updateRes.CacheCluster
2019-07-10 00:17:35 -08:00
}
// Only modify the cache parameter group if the cache cluster is custom one created to allow other groups to
// be set on the cache cluster but not modified.
if *cacheCluster.CacheParameterGroup.CacheParameterGroupName == customCacheParameterGroupName {
log.Printf("\t\tUpdating Cache Parameter Group : %s", *cacheCluster.CacheParameterGroup.CacheParameterGroupName)
_, err = svc.ModifyCacheParameterGroup(&elasticache.ModifyCacheParameterGroupInput{
CacheParameterGroupName: cacheCluster.CacheParameterGroup.CacheParameterGroupName,
2019-07-11 14:46:05 -08:00
ParameterNameValues: req.CacheClusterParameter,
2019-07-10 00:17:35 -08:00
})
if err != nil {
2019-07-11 14:46:05 -08:00
return errors.Wrapf(err, "failed to modify cache parameter group '%s'", *cacheCluster.CacheParameterGroup.CacheParameterGroupName)
2019-07-10 00:17:35 -08:00
}
2019-07-10 04:11:39 -08:00
for _, p := range req.CacheClusterParameter {
log.Printf("\t\t\tSet '%s' to '%s'", *p.ParameterName, *p.ParameterValue)
2019-07-10 00:17:35 -08:00
}
}
}
2019-07-14 12:53:36 -08:00
// Ensure cache nodes are set after updating parameters.
if len(cacheCluster.CacheNodes) == 0 {
// Find Elastic Cache cluster given Id.
descRes, err := svc.DescribeCacheClusters(&elasticache.DescribeCacheClustersInput{
CacheClusterId: req.CacheCluster.CacheClusterId,
ShowCacheNodeInfo: aws.Bool(true),
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != elasticache.ErrCodeCacheClusterNotFoundFault {
return errors.Wrapf(err, "Failed to describe cache cluster '%s'", *req.CacheCluster.CacheClusterId)
}
} else if len(descRes.CacheClusters) > 0 {
cacheCluster = descRes.CacheClusters[0]
}
}
2019-07-13 20:50:00 -08:00
log.Printf("\t%s\tDone setting up Cache Cluster '%s' successfully.\n", tests.Success, *cacheCluster.CacheClusterId)
2019-07-10 00:17:35 -08:00
}
2019-07-10 16:24:10 -08:00
// Route 53 zone lookup when hostname is set. Supports both top level domains or sub domains.
var zoneArecNames = map[string][]string{}
if req.ServiceHostPrimary != "" {
2019-07-10 16:24:10 -08:00
log.Println("Route 53 - Get or create hosted zones.")
svc := route53.New(req.awsSession())
log.Println("\tList all hosted zones.")
var zones []*route53.HostedZone
err := svc.ListHostedZonesPages(&route53.ListHostedZonesInput{},
func(res *route53.ListHostedZonesOutput, lastPage bool) bool {
for _, z := range res.HostedZones {
zones = append(zones, z)
}
return !lastPage
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed list route 53 hosted zones")
2019-07-10 16:24:10 -08:00
}
// Generate a slice with the primary domain name and include all the alternative domain names.
2019-07-12 23:28:53 -08:00
lookupDomains := []string{}
if req.ServiceHostPrimary != "" {
lookupDomains = append(lookupDomains, req.ServiceHostPrimary)
}
for _, dn := range req.ServiceHostNames {
2019-07-10 16:24:10 -08:00
lookupDomains = append(lookupDomains, dn)
}
// Loop through all the defined domain names and find the associated zone even when they are a sub domain.
for _, dn := range lookupDomains {
log.Printf("\t\tFind zone for domain '%s'", dn)
// Get the top level domain from url.
zoneName := domainutil.Domain(dn)
var subdomain string
if zoneName == "" {
// Handle domain names that have weird TDL: ie .tech
zoneName = dn
log.Printf("\t\t\tNon-standard Level Domain: '%s'", zoneName)
} else {
log.Printf("\t\t\tTop Level Domain: '%s'", zoneName)
// Check if url has subdomain.
if domainutil.HasSubdomain(dn) {
subdomain = domainutil.Subdomain(dn)
log.Printf("\t\t\tsubdomain: '%s'", subdomain)
}
}
// Start at the top level domain and try to find a hosted zone. Search until a match is found or there are
// no more domain levels to search for.
var zoneId string
for {
log.Printf("\t\t\tChecking zone '%s' for associated hosted zone.", zoneName)
// Loop over each one of hosted zones and try to find match.
for _, z := range zones {
zn := strings.TrimRight(*z.Name, ".")
log.Printf("\t\t\t\tChecking if '%s' matches '%s'", zn, zoneName)
if zn == zoneName {
zoneId = *z.Id
break
}
}
if zoneId != "" || zoneName == dn {
2019-07-13 20:50:00 -08:00
// Found a matching zone or have to search all possibilities!
2019-07-10 16:24:10 -08:00
break
}
// If we have not found a hosted zone, append the next level from the domain to the zone.
pts := strings.Split(subdomain, ".")
subs := []string{}
for idx, sn := range pts {
if idx == len(pts)-1 {
zoneName = sn + "." + zoneName
} else {
subs = append(subs, sn)
}
}
subdomain = strings.Join(subs, ".")
}
var aName string
if zoneId == "" {
// Get the top level domain from url again.
zoneName := domainutil.Domain(dn)
if zoneName == "" {
// Handle domain names that have weird TDL: ie .tech
zoneName = dn
}
log.Printf("\t\t\tNo hosted zone found for '%s', create '%s'.", dn, zoneName)
createRes, err := svc.CreateHostedZone(&route53.CreateHostedZoneInput{
Name: aws.String(zoneName),
HostedZoneConfig: &route53.HostedZoneConfig{
Comment: aws.String(fmt.Sprintf("Public hosted zone created by saas-starter-kit.")),
},
// A unique string that identifies the request and that allows failed CreateHostedZone
// requests to be retried without the risk of executing the operation twice.
// You must use a unique CallerReference string every time you submit a CreateHostedZone
// request. CallerReference can be any unique string, for example, a date/time
// stamp.
//
// CallerReference is a required field
2019-07-14 20:13:56 -08:00
CallerReference: aws.String("devops-deploy"),
2019-07-10 16:24:10 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create route 53 hosted zone '%s' for domain '%s'", zoneName, dn)
2019-07-10 16:24:10 -08:00
}
zoneId = *createRes.HostedZone.Id
log.Printf("\t\t\tCreated hosted zone '%s'", zoneId)
// The fully qualified A record name.
aName = dn
} else {
log.Printf("\t\t\tFound hosted zone '%s'", zoneId)
// The fully qualified A record name.
if subdomain != "" {
aName = subdomain + "." + zoneName
} else {
aName = zoneName
}
}
// Add the A record to be maintained for the zone.
if _, ok := zoneArecNames[zoneId]; !ok {
zoneArecNames[zoneId] = []string{}
}
zoneArecNames[zoneId] = append(zoneArecNames[zoneId], aName)
log.Printf("\t%s\tZone '%s' found with A record name '%s'.\n", tests.Success, zoneId, aName)
}
}
// Setup service discovery.
var sdService *servicediscovery.Service
{
log.Println("SD - Get or Create Namespace")
svc := servicediscovery.New(req.awsSession())
log.Println("\t\tList all the private namespaces and try to find an existing entry.")
listNamespaces := func() (*servicediscovery.NamespaceSummary, error) {
var found *servicediscovery.NamespaceSummary
err := svc.ListNamespacesPages(&servicediscovery.ListNamespacesInput{
Filters: []*servicediscovery.NamespaceFilter{
&servicediscovery.NamespaceFilter{
Name: aws.String("TYPE"),
Condition: aws.String("EQ"),
Values: aws.StringSlice([]string{"DNS_PRIVATE"}),
},
},
}, func(res *servicediscovery.ListNamespacesOutput, lastPage bool) bool {
for _, n := range res.Namespaces {
if *n.Name == *req.SDNamepsace.Name {
found = n
return false
}
}
return !lastPage
})
if err != nil {
2019-07-13 20:50:00 -08:00
return nil, errors.Wrap(err, "Failed to list namespaces")
2019-07-10 16:24:10 -08:00
}
return found, nil
}
sdNamespace, err := listNamespaces()
if err != nil {
return err
}
if sdNamespace == nil {
// Link the namespace to the VPC.
2019-07-11 02:52:10 -08:00
req.SDNamepsace.Vpc = aws.String(projectVpcId)
2019-07-10 16:24:10 -08:00
log.Println("\t\tCreate private namespace.")
// If no namespace was found, create one.
createRes, err := svc.CreatePrivateDnsNamespace(req.SDNamepsace)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create namespace '%s'", *req.SDNamepsace.Name)
2019-07-10 16:24:10 -08:00
}
operationId := createRes.OperationId
log.Println("\t\tWait for create operation to finish.")
retryFunc := func() (bool, error) {
opRes, err := svc.GetOperation(&servicediscovery.GetOperationInput{
OperationId: operationId,
})
if err != nil {
return true, err
}
log.Printf("\t\t\tStatus: %s.", *opRes.Operation.Status)
// The status of the operation. Values include the following:
// * SUBMITTED: This is the initial state immediately after you submit a
// request.
// * PENDING: AWS Cloud Map is performing the operation.
// * SUCCESS: The operation succeeded.
// * FAIL: The operation failed. For the failure reason, see ErrorMessage.
if *opRes.Operation.Status == "SUCCESS" {
return true, nil
} else if *opRes.Operation.Status == "FAIL" {
2019-07-13 20:50:00 -08:00
err = errors.Errorf("Operation failed")
2019-07-10 16:24:10 -08:00
err = awserr.New(*opRes.Operation.ErrorCode, *opRes.Operation.ErrorMessage, err)
return true, err
}
return false, nil
}
err = retry.Retry(context.Background(), nil, retryFunc)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to get operation for namespace '%s'", *req.SDNamepsace.Name)
2019-07-10 16:24:10 -08:00
}
// Now that the create operation is complete, try to find the namespace again.
sdNamespace, err = listNamespaces()
if err != nil {
return err
}
log.Printf("\t\tCreated: %s.", *sdNamespace.Arn)
} else {
log.Printf("\t\tFound: %s.", *sdNamespace.Arn)
// The number of services that are associated with the namespace.
if sdNamespace.ServiceCount != nil {
log.Printf("\t\t\tServiceCount: %d.", *sdNamespace.ServiceCount)
}
}
log.Printf("\t%s\tUsing Service Discovery Namespace '%s'.\n", tests.Success, *sdNamespace.Id)
// Try to find an existing entry for the current service.
var existingService *servicediscovery.ServiceSummary
err = svc.ListServicesPages(&servicediscovery.ListServicesInput{
Filters: []*servicediscovery.ServiceFilter{
&servicediscovery.ServiceFilter{
Name: aws.String("NAMESPACE_ID"),
Condition: aws.String("EQ"),
Values: aws.StringSlice([]string{*sdNamespace.Id}),
},
},
}, func(res *servicediscovery.ListServicesOutput, lastPage bool) bool {
for _, n := range res.Services {
if *n.Name == req.EcsServiceName {
existingService = n
return false
}
}
return !lastPage
})
if err != nil {
return errors.Wrapf(err, "failed to list services for namespace '%s'", *sdNamespace.Id)
}
if existingService == nil {
// Link the service to the namespace.
req.SDService.NamespaceId = sdNamespace.Id
// If no namespace was found, create one.
createRes, err := svc.CreateService(req.SDService)
if err != nil {
return errors.Wrapf(err, "failed to create service '%s'", *req.SDService.Name)
}
sdService = createRes.Service
log.Printf("\t\tCreated: %s.", *sdService.Arn)
} else {
// If no namespace was found, create one.
getRes, err := svc.GetService(&servicediscovery.GetServiceInput{
Id: existingService.Id,
})
if err != nil {
return errors.Wrapf(err, "failed to get service '%s'", *req.SDService.Name)
}
sdService = getRes.Service
log.Printf("\t\tFound: %s.", *sdService.Arn)
// The number of instances that are currently associated with the service. Instances
// that were previously associated with the service but that have been deleted
// are not included in the count.
if sdService.InstanceCount != nil {
log.Printf("\t\t\tInstanceCount: %d.", *sdService.InstanceCount)
}
}
log.Printf("\t%s\tUsing Service Discovery Service '%s'.\n", tests.Success, *sdService.Id)
}
2019-07-11 02:52:10 -08:00
// If an Elastic Load Balancer is enabled, then ensure one exists else create one.
var ecsELBs []*ecs.LoadBalancer
var elb *elbv2.LoadBalancer
if req.EnableEcsElb {
2019-07-13 20:50:00 -08:00
// If HTTPS enabled on ELB, then need to find ARN certificates first.
2019-07-11 02:52:10 -08:00
var certificateArn string
if req.EnableHTTPS {
log.Println("ACM - Find Elastic Load Balance")
svc := acm.New(req.awsSession())
err := svc.ListCertificatesPages(&acm.ListCertificatesInput{},
func(res *acm.ListCertificatesOutput, lastPage bool) bool {
for _, cert := range res.CertificateSummaryList {
2019-07-12 23:28:53 -08:00
if *cert.DomainName == req.ServiceHostPrimary {
2019-07-11 02:52:10 -08:00
certificateArn = *cert.CertificateArn
return false
}
}
return !lastPage
})
if err != nil {
2019-07-12 23:28:53 -08:00
return errors.Wrapf(err, "failed to list certificates for '%s'", req.ServiceHostPrimary)
2019-07-11 02:52:10 -08:00
}
if certificateArn == "" {
// Create hash of all the domain names to be used to mark unique requests.
2019-07-12 23:28:53 -08:00
idempotencyToken := req.ServiceHostPrimary + "|" + strings.Join(req.ServiceHostNames, "|")
2019-07-11 02:52:10 -08:00
idempotencyToken = fmt.Sprintf("%x", md5.Sum([]byte(idempotencyToken)))
// If no certicate was found, create one.
createRes, err := svc.RequestCertificate(&acm.RequestCertificateInput{
// Fully qualified domain name (FQDN), such as www.example.com, that you want
// to secure with an ACM certificate. Use an asterisk (*) to create a wildcard
// certificate that protects several sites in the same domain. For example,
// *.example.com protects www.example.com, site.example.com, and images.example.com.
//
// The first domain name you enter cannot exceed 63 octets, including periods.
// Each subsequent Subject Alternative Name (SAN), however, can be up to 253
// octets in length.
//
// DomainName is a required field
2019-07-12 23:28:53 -08:00
DomainName: aws.String(req.ServiceHostPrimary),
2019-07-11 02:52:10 -08:00
// Customer chosen string that can be used to distinguish between calls to RequestCertificate.
// Idempotency tokens time out after one hour. Therefore, if you call RequestCertificate
// multiple times with the same idempotency token within one hour, ACM recognizes
// that you are requesting only one certificate and will issue only one. If
// you change the idempotency token for each call, ACM recognizes that you are
// requesting multiple certificates.
IdempotencyToken: aws.String(idempotencyToken),
// Currently, you can use this parameter to specify whether to add the certificate
// to a certificate transparency log. Certificate transparency makes it possible
// to detect SSL/TLS certificates that have been mistakenly or maliciously issued.
// Certificates that have not been logged typically produce an error message
// in a browser. For more information, see Opting Out of Certificate Transparency
// Logging (https://docs.aws.amazon.com/acm/latest/userguide/acm-bestpractices.html#best-practices-transparency).
Options: &acm.CertificateOptions{
CertificateTransparencyLoggingPreference: aws.String("DISABLED"),
},
// Additional FQDNs to be included in the Subject Alternative Name extension
// of the ACM certificate. For example, add the name www.example.net to a certificate
// for which the DomainName field is www.example.com if users can reach your
// site by using either name. The maximum number of domain names that you can
// add to an ACM certificate is 100. However, the initial limit is 10 domain
// names. If you need more than 10 names, you must request a limit increase.
// For more information, see Limits (https://docs.aws.amazon.com/acm/latest/userguide/acm-limits.html).
2019-07-12 23:28:53 -08:00
SubjectAlternativeNames: aws.StringSlice(req.ServiceHostNames),
2019-07-11 02:52:10 -08:00
// The method you want to use if you are requesting a public certificate to
// validate that you own or control domain. You can validate with DNS (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html)
// or validate with email (https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-email.html).
// We recommend that you use DNS validation.
ValidationMethod: aws.String("DNS"),
})
if err != nil {
2019-07-12 23:28:53 -08:00
return errors.Wrapf(err, "failed to create certificate '%s'", req.ServiceHostPrimary)
2019-07-11 02:52:10 -08:00
}
certificateArn = *createRes.CertificateArn
2019-07-12 23:28:53 -08:00
log.Printf("\t\tCreated certificate '%s'", req.ServiceHostPrimary)
2019-07-11 02:52:10 -08:00
} else {
2019-07-12 23:28:53 -08:00
log.Printf("\t\tFound certificate '%s'", req.ServiceHostPrimary)
2019-07-11 14:15:29 -08:00
}
descRes, err := svc.DescribeCertificate(&acm.DescribeCertificateInput{
CertificateArn: aws.String(certificateArn),
})
if err != nil {
return errors.Wrapf(err, "failed to describe certificate '%s'", certificateArn)
}
cert := descRes.Certificate
log.Printf("\t\t\tStatus: %s", *cert.Status)
if *cert.Status == "PENDING_VALIDATION" {
svc := route53.New(req.awsSession())
log.Println("\tList all hosted zones.")
var zoneValOpts = map[string][]*acm.DomainValidation{}
for _, opt := range cert.DomainValidationOptions {
var found bool
for zoneId, aNames := range zoneArecNames {
for _, aName := range aNames {
fmt.Println(*opt.DomainName, " ==== ", aName)
if *opt.DomainName == aName {
if _, ok := zoneValOpts[zoneId]; !ok {
zoneValOpts[zoneId] = []*acm.DomainValidation{}
}
zoneValOpts[zoneId] = append(zoneValOpts[zoneId], opt)
found = true
break
}
}
if found {
break
}
}
if !found {
return errors.Errorf("Failed to find zone ID for '%s'", *opt.DomainName)
}
}
for zoneId, opts := range zoneValOpts {
for _, opt := range opts {
if *opt.ValidationStatus == "SUCCESS" {
continue
}
input := &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{
&route53.Change{
Action: aws.String("UPSERT"),
ResourceRecordSet: &route53.ResourceRecordSet{
Name: opt.ResourceRecord.Name,
ResourceRecords: []*route53.ResourceRecord{
&route53.ResourceRecord{Value: opt.ResourceRecord.Value},
},
Type: opt.ResourceRecord.Type,
TTL: aws.Int64(60),
},
},
},
},
HostedZoneId: aws.String(zoneId),
}
log.Printf("\tAdded verification record for '%s'.\n", *opt.ResourceRecord.Name)
_, err := svc.ChangeResourceRecordSets(input)
if err != nil {
return errors.Wrapf(err, "failed to update A records for zone '%s'", zoneId)
}
}
}
2019-07-11 02:52:10 -08:00
}
log.Printf("\t%s\tUsing ACM Certicate '%s'.\n", tests.Success, certificateArn)
}
log.Println("EC2 - Find Elastic Load Balance")
{
svc := elbv2.New(req.awsSession())
2019-07-13 20:50:00 -08:00
// Try to find load balancer given a name.
2019-07-11 02:52:10 -08:00
err := svc.DescribeLoadBalancersPages(&elbv2.DescribeLoadBalancersInput{
Names: []*string{aws.String(req.ElbLoadBalancerName)},
}, func(res *elbv2.DescribeLoadBalancersOutput, lastPage bool) bool {
2019-07-13 20:50:00 -08:00
// Loop through the results to find the match ELB.
2019-07-11 02:52:10 -08:00
for _, lb := range res.LoadBalancers {
if *lb.LoadBalancerName == req.ElbLoadBalancerName {
elb = lb
return false
}
}
return !lastPage
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != elbv2.ErrCodeLoadBalancerNotFoundException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to describe load balancer '%s'", req.ElbLoadBalancerName)
2019-07-11 02:52:10 -08:00
}
}
var curListeners []*elbv2.Listener
if elb == nil {
// Link the security group and subnets to the Load Balancer
req.ElbLoadBalancer.SecurityGroups = aws.StringSlice([]string{securityGroupId})
req.ElbLoadBalancer.Subnets = aws.StringSlice(projectSubnetsIDs)
// If no repository was found, create one.
createRes, err := svc.CreateLoadBalancer(req.ElbLoadBalancer)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create load balancer '%s'", req.ElbLoadBalancerName)
2019-07-11 02:52:10 -08:00
}
elb = createRes.LoadBalancers[0]
log.Printf("\t\tCreated: %s.", *elb.LoadBalancerArn)
} else {
log.Printf("\t\tFound: %s.", *elb.LoadBalancerArn)
// Search for existing listeners associated with the load balancer.
res, err := svc.DescribeListeners(&elbv2.DescribeListenersInput{
// The Amazon Resource Name (ARN) of the load balancer.
LoadBalancerArn: elb.LoadBalancerArn,
// There are two target groups, return both associated listeners if they exist.
PageSize: aws.Int64(2),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to find listeners for load balancer '%s'", req.ElbLoadBalancerName)
2019-07-11 02:52:10 -08:00
}
curListeners = res.Listeners
}
// The state code. The initial state of the load balancer is provisioning. After
// the load balancer is fully set up and ready to route traffic, its state is
// active. If the load balancer could not be set up, its state is failed.
log.Printf("\t\t\tState: %s.", *elb.State.Code)
2019-07-11 14:46:05 -08:00
var targetGroup *elbv2.TargetGroup
err = svc.DescribeTargetGroupsPages(&elbv2.DescribeTargetGroupsInput{
LoadBalancerArn: elb.LoadBalancerArn,
}, func(res *elbv2.DescribeTargetGroupsOutput, lastPage bool) bool {
for _, tg := range res.TargetGroups {
2019-07-11 14:46:05 -08:00
if *tg.TargetGroupName == req.ElbTargetGroupName {
targetGroup = tg
return false
}
}
return !lastPage
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != elbv2.ErrCodeTargetGroupNotFoundException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to describe target group '%s'", req.ElbTargetGroupName)
}
}
2019-07-11 02:52:10 -08:00
if targetGroup == nil {
// The identifier of the virtual private cloud (VPC). If the target is a Lambda
// function, this parameter does not apply.
req.ElbTargetGroup.VpcId = aws.String(projectVpcId)
2019-07-11 02:52:10 -08:00
// If no target group was found, create one.
createRes, err := svc.CreateTargetGroup(req.ElbTargetGroup)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create target group '%s'", req.ElbTargetGroupName)
}
targetGroup = createRes.TargetGroups[0]
2019-07-11 02:52:10 -08:00
log.Printf("\t\tAdded target group: %s.", *targetGroup.TargetGroupArn)
} else {
log.Printf("\t\tHas target group: %s.", *targetGroup.TargetGroupArn)
2019-07-11 02:52:10 -08:00
}
if req.ElbDeregistrationDelay != nil {
// If no target group was found, create one.
_, err = svc.ModifyTargetGroupAttributes(&elbv2.ModifyTargetGroupAttributesInput{
TargetGroupArn: targetGroup.TargetGroupArn,
Attributes: []*elbv2.TargetGroupAttribute{
&elbv2.TargetGroupAttribute{
// The name of the attribute.
Key: aws.String("deregistration_delay.timeout_seconds"),
2019-07-11 02:52:10 -08:00
// The value of the attribute.
Value: aws.String(strconv.Itoa(*req.ElbDeregistrationDelay)),
},
},
2019-07-11 02:52:10 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to modify target group '%s' attributes", req.ElbTargetGroupName)
2019-07-11 02:52:10 -08:00
}
log.Printf("\t\t\tSet sttributes.")
}
2019-07-11 02:52:10 -08:00
2019-07-11 14:46:05 -08:00
listenerPorts := map[string]int64{
"HTTP": 80,
}
if req.EnableHTTPS {
listenerPorts["HTTPS"] = 443
}
2019-07-11 02:52:10 -08:00
for listenerProtocol, listenerPort := range listenerPorts {
2019-07-11 02:52:10 -08:00
var foundListener bool
for _, cl := range curListeners {
if *cl.Port == listenerPort {
2019-07-11 02:52:10 -08:00
foundListener = true
break
}
}
if !foundListener {
listenerInput := &elbv2.CreateListenerInput{
// The actions for the default rule. The rule must include one forward action
// or one or more fixed-response actions.
//
// If the action type is forward, you specify a target group. The protocol of
// the target group must be HTTP or HTTPS for an Application Load Balancer.
// The protocol of the target group must be TCP, TLS, UDP, or TCP_UDP for a
// Network Load Balancer.
//
// DefaultActions is a required field
DefaultActions: []*elbv2.Action{
&elbv2.Action{
// The type of action. Each rule must include exactly one of the following types
// of actions: forward, fixed-response, or redirect.
//
// Type is a required field
Type: aws.String("forward"),
// The Amazon Resource Name (ARN) of the target group. Specify only when Type
// is forward.
TargetGroupArn: targetGroup.TargetGroupArn,
},
},
// The Amazon Resource Name (ARN) of the load balancer.
//
// LoadBalancerArn is a required field
LoadBalancerArn: elb.LoadBalancerArn,
// The port on which the load balancer is listening.
//
// Port is a required field
Port: aws.Int64(listenerPort),
2019-07-11 02:52:10 -08:00
// The protocol for connections from clients to the load balancer. For Application
// Load Balancers, the supported protocols are HTTP and HTTPS. For Network Load
// Balancers, the supported protocols are TCP, TLS, UDP, and TCP_UDP.
//
// Protocol is a required field
Protocol: aws.String(listenerProtocol),
2019-07-11 02:52:10 -08:00
}
if listenerProtocol == "HTTPS" {
2019-07-11 02:52:10 -08:00
listenerInput.Certificates = append(listenerInput.Certificates, &elbv2.Certificate{
CertificateArn: aws.String(certificateArn),
})
}
// If no repository was found, create one.
createRes, err := svc.CreateListener(listenerInput)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create listener '%s'", req.ElbLoadBalancerName)
2019-07-11 02:52:10 -08:00
}
log.Printf("\t\t\tAdded Listener: %s.", *createRes.Listeners[0].ListenerArn)
}
2019-07-11 14:15:29 -08:00
}
ecsELBs = append(ecsELBs, &ecs.LoadBalancer{
// The name of the container (as it appears in a container definition) to associate
// with the load balancer.
ContainerName: aws.String(req.EcsServiceName),
// The port on the container to associate with the load balancer. This port
// must correspond to a containerPort in the service's task definition. Your
// container instances must allow ingress traffic on the hostPort of the port
// mapping.
ContainerPort: targetGroup.Port,
// The full Amazon Resource Name (ARN) of the Elastic Load Balancing target
// group or groups associated with a service or task set.
TargetGroupArn: targetGroup.TargetGroupArn,
})
2019-07-11 14:46:05 -08:00
2019-07-11 14:15:29 -08:00
{
log.Println("Ensure Load Balancer DNS name exists for hosted zones.")
log.Printf("\t\tDNSName: '%s'.\n", *elb.DNSName)
svc := route53.New(req.awsSession())
for zoneId, aNames := range zoneArecNames {
log.Printf("\tChange zone '%s'.\n", zoneId)
input := &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &route53.ChangeBatch{
Changes: []*route53.Change{},
},
HostedZoneId: aws.String(zoneId),
}
// Add all the A record names with the same set of public IPs.
for _, aName := range aNames {
log.Printf("\t\tAdd A record for '%s'.\n", aName)
input.ChangeBatch.Changes = append(input.ChangeBatch.Changes, &route53.Change{
Action: aws.String("UPSERT"),
ResourceRecordSet: &route53.ResourceRecordSet{
2019-07-11 14:46:05 -08:00
Name: aws.String(aName),
Type: aws.String("A"),
2019-07-11 14:15:29 -08:00
AliasTarget: &route53.AliasTarget{
2019-07-11 14:46:05 -08:00
HostedZoneId: elb.CanonicalHostedZoneId,
DNSName: elb.DNSName,
2019-07-11 14:15:29 -08:00
EvaluateTargetHealth: aws.Bool(true),
},
},
})
}
log.Printf("\tUpdated '%s'.\n", zoneId)
_, err := svc.ChangeResourceRecordSets(input)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to update A records for zone '%s'", zoneId)
2019-07-11 14:15:29 -08:00
}
}
2019-07-11 02:52:10 -08:00
}
log.Printf("\t%s\tUsing ELB '%s'.\n", tests.Success, *elb.LoadBalancerName)
}
}
2019-07-10 04:52:38 -08:00
// Try to find AWS ECS Cluster by name or create new one.
2019-07-08 12:21:22 -08:00
var ecsCluster *ecs.Cluster
{
2019-07-10 04:52:38 -08:00
log.Println("ECS - Get or Create Cluster")
svc := ecs.New(req.awsSession())
2019-07-08 19:13:41 -08:00
descRes, err := svc.DescribeClusters(&ecs.DescribeClustersInput{
Clusters: []*string{aws.String(req.EcsClusterName)},
2019-07-08 19:13:41 -08:00
})
2019-07-08 12:21:22 -08:00
if err != nil {
2019-07-08 19:13:41 -08:00
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != ecs.ErrCodeClusterNotFoundException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to describe cluster '%s'", req.EcsClusterName)
2019-07-08 19:13:41 -08:00
}
} else if len(descRes.Clusters) > 0 {
2019-07-08 19:13:41 -08:00
ecsCluster = descRes.Clusters[0]
2019-07-08 12:21:22 -08:00
}
2019-07-07 12:52:55 -08:00
if ecsCluster == nil {
2019-07-08 19:13:41 -08:00
// If no repository was found, create one.
2019-07-11 00:58:45 -08:00
createRes, err := svc.CreateCluster(req.EcsCluster)
2019-07-08 19:13:41 -08:00
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create cluster '%s'", req.EcsClusterName)
2019-07-08 19:13:41 -08:00
}
ecsCluster = createRes.Cluster
2019-07-08 12:21:22 -08:00
log.Printf("\t\tCreated: %s.", *ecsCluster.ClusterArn)
} else {
log.Printf("\t\tFound: %s.", *ecsCluster.ClusterArn)
// The number of services that are running on the cluster in an ACTIVE state.
// You can view these services with ListServices.
log.Printf("\t\t\tActiveServicesCount: %d.", *ecsCluster.ActiveServicesCount)
// The number of tasks in the cluster that are in the PENDING state.
log.Printf("\t\t\tPendingTasksCount: %d.", *ecsCluster.PendingTasksCount)
// The number of container instances registered into the cluster. This includes
// container instances in both ACTIVE and DRAINING status.
log.Printf("\t\t\tRegisteredContainerInstancesCount: %d.", *ecsCluster.RegisteredContainerInstancesCount)
// The number of tasks in the cluster that are in the RUNNING state.
log.Printf("\t\t\tRunningTasksCount: %d.", *ecsCluster.RunningTasksCount)
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// The status of the cluster. The valid values are ACTIVE or INACTIVE. ACTIVE
// indicates that you can register container instances with the cluster and
// the associated instances can accept tasks.
log.Printf("\t\t\tStatus: %s.", *ecsCluster.Status)
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tUsing ECS Cluster '%s'.\n", tests.Success, *ecsCluster.ClusterName)
2019-07-07 12:52:55 -08:00
}
2019-07-10 04:52:38 -08:00
// Register a new ECS task.
2019-07-08 12:21:22 -08:00
var taskDef *ecs.TaskDefinition
{
2019-07-10 04:52:38 -08:00
log.Println("ECS - Register task definition")
2019-07-08 12:21:22 -08:00
// List of placeholders that can be used in task definition and replaced on deployment.
placeholders := map[string]string{
"{SERVICE}": req.ServiceName,
"{RELEASE_IMAGE}": req.ReleaseImage,
"{ECS_CLUSTER}": req.EcsClusterName,
"{ECS_SERVICE}": req.EcsServiceName,
"{AWS_REGION}": req.AwsCreds.Region,
"{AWS_LOGS_GROUP}": req.CloudWatchLogGroupName,
"{AWS_S3_BUCKET_PRIVATE}": req.S3BucketPrivateName,
"{AWS_S3_BUCKET_PUBLIC}": req.S3BucketPublicName,
"{ENV}": req.Env,
"{DATADOG_APIKEY}": datadogApiKey,
"{DATADOG_ESSENTIAL}": "true",
"{HTTP_HOST}": "0.0.0.0:80",
"{HTTPS_HOST}": "", // Not enabled by default
"{HTTPS_ENABLED}": "false",
"{APP_PROJECT}": req.ProjectName,
"{APP_BASE_URL}": "", // Not set by default, requires a hostname to be defined.
"{HOST_PRIMARY}": req.ServiceHostPrimary,
"{HOST_NAMES}": strings.Join(req.ServiceHostNames, ","),
2019-07-10 00:17:35 -08:00
2019-07-14 19:13:09 -08:00
"{STATIC_FILES_S3_ENABLED}": "false",
2019-07-15 00:37:07 -08:00
"{STATIC_FILES_S3_PREFIX}": req.StaticFilesS3Prefix,
2019-07-14 19:13:09 -08:00
"{STATIC_FILES_CLOUDFRONT_ENABLED}": "false",
"{STATIC_FILES_IMG_RESIZE_ENABLED}": "false",
2019-07-10 00:17:35 -08:00
"{CACHE_HOST}": "", // Not enabled by default
2019-07-11 00:58:45 -08:00
"{DB_HOST}": "",
"{DB_USER}": "",
"{DB_PASS}": "",
"{DB_DATABASE}": "",
"{DB_DRIVER}": "",
2019-07-10 04:11:39 -08:00
"{DB_DISABLE_TLS}": "",
2019-07-10 00:17:35 -08:00
2019-07-11 14:46:05 -08:00
"{ROUTE53_ZONES}": "",
2019-07-11 00:58:45 -08:00
"{ROUTE53_UPDATE_TASK_IPS}": "false",
2019-07-10 00:17:35 -08:00
// Directly map GitLab CICD env variables set during deploy.
2019-07-11 00:58:45 -08:00
"{CI_COMMIT_REF_NAME}": os.Getenv("CI_COMMIT_REF_NAME"),
"{CI_COMMIT_REF_SLUG}": os.Getenv("CI_COMMIT_REF_SLUG"),
"{CI_COMMIT_SHA}": os.Getenv("CI_COMMIT_SHA"),
"{CI_COMMIT_TAG}": os.Getenv("CI_COMMIT_TAG"),
"{CI_COMMIT_TITLE}": jsonEncodeStringValue(os.Getenv("CI_COMMIT_TITLE")),
"{CI_COMMIT_DESCRIPTION}": jsonEncodeStringValue(os.Getenv("CI_COMMIT_DESCRIPTION")),
2019-07-11 00:58:45 -08:00
"{CI_COMMIT_JOB_ID}": os.Getenv("CI_COMMIT_JOB_ID"),
"{CI_COMMIT_JOB_URL}": os.Getenv("CI_COMMIT_JOB_URL"),
"{CI_COMMIT_PIPELINE_ID}": os.Getenv("CI_COMMIT_PIPELINE_ID"),
2019-07-10 00:17:35 -08:00
"{CI_COMMIT_PIPELINE_URL}": os.Getenv("CI_COMMIT_PIPELINE_URL"),
}
// 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}"] = "false"
}
// For HTTPS support.
if req.EnableHTTPS {
placeholders["{HTTPS_ENABLED}"] = "true"
// When there is no Elastic Load Balancer, we need to terminate HTTPS on the app.
if !req.EnableEcsElb {
placeholders["{HTTPS_HOST}"] = "0.0.0.0:443"
}
2019-07-10 00:17:35 -08:00
}
// When a domain name if defined for the service, set the App Base URL. Default to HTTPS if enabled.
2019-07-12 23:28:53 -08:00
if req.ServiceHostPrimary != "" {
2019-07-10 00:17:35 -08:00
var appSchema string
if req.EnableHTTPS {
appSchema = "https"
} else {
appSchema = "http"
}
2019-07-12 23:28:53 -08:00
placeholders["{APP_BASE_URL}"] = fmt.Sprintf("%s://%s/", appSchema, req.ServiceHostPrimary)
2019-07-10 00:17:35 -08:00
}
2019-07-14 19:13:09 -08:00
// Static files served from S3.
if req.StaticFilesS3Enable {
placeholders["{STATIC_FILES_S3_ENABLED}"] = "true"
}
// Static files served from CloudFront.
2019-07-14 20:13:56 -08:00
if req.CloudfrontPublic != nil {
2019-07-14 19:13:09 -08:00
placeholders["{STATIC_FILES_CLOUDFRONT_ENABLED}"] = "true"
}
// Support for resizing static images files to be responsive.
if req.StaticFilesImgResizeEnable {
placeholders["{STATIC_FILES_IMG_RESIZE_ENABLED}"] = "true"
}
2019-07-10 04:11:39 -08:00
// When db is set, update the placeholders.
if db != nil {
placeholders["{DB_HOST}"] = db.Host
placeholders["{DB_USER}"] = db.User
placeholders["{DB_PASS}"] = db.Pass
placeholders["{DB_DATABASE}"] = db.Database
placeholders["{DB_DRIVER}"] = db.Driver
if db.DisableTLS {
placeholders["{DB_DISABLE_TLS}"] = "true"
} else {
placeholders["{DB_DISABLE_TLS}"] = "false"
}
}
// When cache cluster is set, set the host and port.
2019-07-10 00:17:35 -08:00
if cacheCluster != nil {
var cacheHost string
if cacheCluster.ConfigurationEndpoint != nil {
// Works for memcache.
cacheHost = fmt.Sprintf("%s:%d", *cacheCluster.ConfigurationEndpoint.Address, *cacheCluster.ConfigurationEndpoint.Port)
} else if len(cacheCluster.CacheNodes) > 0 {
// Works for redis.
cacheHost = fmt.Sprintf("%s:%d", *cacheCluster.CacheNodes[0].Endpoint.Address, *cacheCluster.CacheNodes[0].Endpoint.Port)
} else {
return errors.New("Unable to determine cache host from cache cluster")
}
placeholders["{CACHE_HOST}"] = cacheHost
2019-07-08 12:21:22 -08:00
}
2019-07-07 12:52:55 -08:00
2019-07-11 00:58:45 -08:00
// Append the Route53 Zones as an env var to be used by the service for maintaining A records when new tasks
// are spun up or down.
if len(zoneArecNames) > 0 {
dat, err := json.Marshal(zoneArecNames)
if err != nil {
return errors.Wrapf(err, "failed to json marshal zones")
}
placeholders["{ROUTE53_ZONES}"] = base64.RawURLEncoding.EncodeToString(dat)
// When no Elastic Load Balance is used, tasks need to be able to directly update the Route 53 records.
if !req.EnableEcsElb {
placeholders["{ROUTE53_UPDATE_TASK_IPS}"] = "true"
}
}
2019-07-08 12:21:22 -08:00
// Loop through all the placeholders and create a list of keys to search json.
var pks []string
for k, _ := range placeholders {
pks = append(pks, k)
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// Generate new regular expression for finding placeholders.
expr := "(" + strings.Join(pks, "|") + ")"
r, err := regexp.Compile(expr)
if err != nil {
return err
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// Read the defined json task definition.
dat, err := EcsReadTaskDefinition(req.ServiceDir, req.Env)
2019-07-08 12:21:22 -08:00
if err != nil {
return err
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// Replace placeholders used in the JSON task definition.
{
jsonStr := string(dat)
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
matches := r.FindAllString(jsonStr, -1)
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
if len(matches) > 0 {
log.Println("\t\tUpdating placeholders.")
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
replaced := make(map[string]bool)
for _, m := range matches {
if replaced[m] {
continue
}
replaced[m] = true
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
newVal := placeholders[m]
log.Printf("\t\t\t%s -> %s", m, newVal)
jsonStr = strings.Replace(jsonStr, m, newVal, -1)
}
}
dat = []byte(jsonStr)
}
log.Println("\t\tParse JSON to task definition.")
taskDefInput, err := parseTaskDefinitionInput(dat)
2019-07-07 12:52:55 -08:00
if err != nil {
return err
}
2019-07-08 19:13:41 -08:00
// If a task definition value is empty, populate it with the default value.
2019-07-08 12:21:22 -08:00
if taskDefInput.Family == nil || *taskDefInput.Family == "" {
taskDefInput.Family = &req.ServiceName
2019-07-08 12:21:22 -08:00
}
if len(taskDefInput.ContainerDefinitions) > 0 {
if taskDefInput.ContainerDefinitions[0].Name == nil || *taskDefInput.ContainerDefinitions[0].Name == "" {
taskDefInput.ContainerDefinitions[0].Name = &req.EcsServiceName
2019-07-08 12:21:22 -08:00
}
if taskDefInput.ContainerDefinitions[0].Image == nil || *taskDefInput.ContainerDefinitions[0].Image == "" {
taskDefInput.ContainerDefinitions[0].Image = &req.ReleaseImage
2019-07-08 12:21:22 -08:00
}
}
log.Printf("\t\t\tFamily: %s", *taskDefInput.Family)
log.Printf("\t\t\tExecutionRoleArn: %s", *taskDefInput.ExecutionRoleArn)
if taskDefInput.TaskRoleArn != nil {
log.Printf("\t\t\tTaskRoleArn: %s", *taskDefInput.TaskRoleArn)
}
if taskDefInput.NetworkMode != nil {
log.Printf("\t\t\tNetworkMode: %s", *taskDefInput.NetworkMode)
}
log.Printf("\t\t\tTaskDefinitions: %d", len(taskDefInput.ContainerDefinitions))
// If memory or cpu for the task is not set, need to compute from container definitions.
if (taskDefInput.Cpu == nil || *taskDefInput.Cpu == "") || (taskDefInput.Memory == nil || *taskDefInput.Memory == "") {
log.Println("\t\tCompute CPU and Memory for task definition.")
var (
totalMemory int64
totalCpu int64
2019-07-08 12:21:22 -08:00
)
for _, c := range taskDefInput.ContainerDefinitions {
if c.Memory != nil {
totalMemory = totalMemory + *c.Memory
} else if c.MemoryReservation != nil {
totalMemory = totalMemory + *c.MemoryReservation
} else {
totalMemory = totalMemory + 1
}
if c.Cpu != nil {
totalCpu = totalCpu + *c.Cpu
} else {
totalCpu = totalCpu + 1
}
}
log.Printf("\t\t\tContainer Definitions has defined total memory %d and cpu %d", totalMemory, totalCpu)
2019-07-13 20:50:00 -08:00
// The selected memory and CPU for ECS Fargate is determined by the made available by AWS.
// For more information, reference the section "Task and CPU Memory" on this page:
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html
// If your service deployment encounters the ECS error: Invalid CPU or Memory Value Specified
// reference this page and the values below may need to be updated accordingly.
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html
2019-07-08 12:21:22 -08:00
var (
selectedMemory int64
selectedCpu int64
2019-07-08 12:21:22 -08:00
)
if totalMemory < 8192 {
if totalMemory > 7168 {
selectedMemory = 8192
if totalCpu >= 2048 {
selectedCpu = 4096
2019-07-08 12:21:22 -08:00
} else if totalCpu >= 1024 {
selectedCpu = 2048
} else {
selectedCpu = 1024
}
} else if totalMemory > 6144 {
selectedMemory = 7168
2019-07-08 12:21:22 -08:00
if totalCpu >= 2048 {
selectedCpu = 4096
} else if totalCpu >= 1024 {
2019-07-08 12:21:22 -08:00
selectedCpu = 2048
} else {
selectedCpu = 1024
}
} else if totalMemory > 5120 || totalCpu >= 1024 {
selectedMemory = 6144
2019-07-08 12:21:22 -08:00
if totalCpu >= 2048 {
selectedCpu = 4096
} else if totalCpu >= 1024 {
2019-07-08 12:21:22 -08:00
selectedCpu = 2048
} else {
selectedCpu = 1024
}
} else if totalMemory > 4096 {
selectedMemory = 5120
2019-07-08 12:21:22 -08:00
if totalCpu >= 512 {
selectedCpu = 1024
} else {
selectedCpu = 512
}
} else if totalMemory > 3072 {
selectedMemory = 4096
2019-07-08 12:21:22 -08:00
if totalCpu >= 512 {
selectedCpu = 1024
} else {
selectedCpu = 512
}
} else if totalMemory > 2048 || totalCpu >= 512 {
selectedMemory = 3072
2019-07-08 12:21:22 -08:00
if totalCpu >= 512 {
selectedCpu = 1024
} else {
selectedCpu = 512
}
} else if totalMemory > 1024 || totalCpu >= 256 {
selectedMemory = 2048
2019-07-08 12:21:22 -08:00
if totalCpu >= 256 {
if totalCpu >= 512 {
selectedCpu = 1024
} else {
selectedCpu = 512
}
} else {
selectedCpu = 256
}
} else if totalMemory > 512 {
selectedMemory = 1024
2019-07-08 12:21:22 -08:00
if totalCpu >= 256 {
selectedCpu = 512
} else {
selectedCpu = 256
}
} else {
selectedMemory = 512
selectedCpu = 256
2019-07-08 12:21:22 -08:00
}
}
log.Printf("\t\t\tSelected memory %d and cpu %d", selectedMemory, selectedCpu)
taskDefInput.Memory = aws.String(strconv.Itoa(int(selectedMemory)))
taskDefInput.Cpu = aws.String(strconv.Itoa(int(selectedCpu)))
}
log.Printf("\t%s\tLoaded task definition complete.\n", tests.Success)
// The execution role is the IAM role that executes ECS actions such as pulling the image and storing the
// application logs in cloudwatch.
if taskDefInput.ExecutionRoleArn == nil || *taskDefInput.ExecutionRoleArn == "" {
svc := iam.New(req.awsSession())
2019-07-08 12:21:22 -08:00
// Find or create role for ExecutionRoleArn.
{
log.Printf("\tAppend ExecutionRoleArn to task definition input for role %s.", req.EcsExecutionRoleName)
res, err := svc.GetRole(&iam.GetRoleInput{
RoleName: aws.String(req.EcsExecutionRoleName),
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to find task role '%s'", req.EcsExecutionRoleName)
}
}
if res.Role != nil {
taskDefInput.ExecutionRoleArn = res.Role.Arn
log.Printf("\t\t\tFound role '%s'", *taskDefInput.ExecutionRoleArn)
} else {
// If no repository was found, create one.
2019-07-11 00:58:45 -08:00
res, err := svc.CreateRole(req.EcsExecutionRole)
2019-07-08 12:21:22 -08:00
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create task role '%s'", req.EcsExecutionRoleName)
2019-07-08 12:21:22 -08:00
}
taskDefInput.ExecutionRoleArn = res.Role.Arn
log.Printf("\t\t\tCreated role '%s'", *taskDefInput.ExecutionRoleArn)
}
2019-07-08 12:21:22 -08:00
2019-07-11 00:58:45 -08:00
for _, policyArn := range req.EcsExecutionRolePolicyArns {
_, err = svc.AttachRolePolicy(&iam.AttachRolePolicyInput{
PolicyArn: aws.String(policyArn),
RoleName: aws.String(req.EcsExecutionRoleName),
2019-07-08 12:21:22 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to attach policy '%s' to task role '%s'", policyArn, req.EcsExecutionRoleName)
2019-07-08 19:13:41 -08:00
}
log.Printf("\t\t\t\tAttached Policy '%s'", policyArn)
2019-07-08 19:13:41 -08:00
}
log.Printf("\t%s\tExecutionRoleArn updated.\n", tests.Success)
2019-07-08 19:13:41 -08:00
}
2019-07-08 12:21:22 -08:00
}
// The task role is the IAM role used by the task itself to access other AWS Services. To access services
// like S3, SQS, etc then those permissions would need to be covered by the TaskRole.
if taskDefInput.TaskRoleArn == nil || *taskDefInput.TaskRoleArn == "" {
svc := iam.New(req.awsSession())
// Find or create the default service policy.
var policyArn string
{
log.Printf("\tFind default service policy %s.", req.EcsTaskPolicyName)
var policyVersionId string
err = svc.ListPoliciesPages(&iam.ListPoliciesInput{}, func(res *iam.ListPoliciesOutput, lastPage bool) bool {
for _, p := range res.Policies {
if *p.PolicyName == req.EcsTaskPolicyName {
policyArn = *p.Arn
policyVersionId = *p.DefaultVersionId
return false
2019-07-08 12:21:22 -08:00
}
}
return !lastPage
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to list IAM policies")
}
2019-07-08 12:21:22 -08:00
if policyArn != "" {
log.Printf("\t\t\tFound policy '%s' versionId '%s'", policyArn, policyVersionId)
2019-07-08 12:21:22 -08:00
res, err := svc.GetPolicyVersion(&iam.GetPolicyVersionInput{
PolicyArn: aws.String(policyArn),
VersionId: aws.String(policyVersionId),
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to read policy '%s' version '%s'", req.EcsTaskPolicyName, policyVersionId)
2019-07-08 19:13:41 -08:00
}
}
2019-07-08 12:21:22 -08:00
// The policy document returned in this structure is URL-encoded compliant with
// RFC 3986 (https://tools.ietf.org/html/rfc3986). You can use a URL decoding
// method to convert the policy back to plain JSON text.
curJson, err := url.QueryUnescape(*res.PolicyVersion.Document)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to url unescape policy document - %s", string(*res.PolicyVersion.Document))
}
2019-07-08 12:21:22 -08:00
// Compare policy documents and add any missing actions for each statement by matching Sid.
var curDoc IamPolicyDocument
err = json.Unmarshal([]byte(curJson), &curDoc)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to json decode policy document - %s", string(curJson))
}
2019-07-08 12:21:22 -08:00
var updateDoc bool
2019-07-10 16:24:10 -08:00
for _, baseStmt := range req.EcsTaskPolicyDocument.Statement {
var found bool
for curIdx, curStmt := range curDoc.Statement {
if baseStmt.Sid != curStmt.Sid {
continue
}
2019-07-08 12:21:22 -08:00
found = true
2019-07-08 12:21:22 -08:00
for _, baseAction := range baseStmt.Action {
var hasAction bool
for _, curAction := range curStmt.Action {
if baseAction == curAction {
hasAction = true
break
2019-07-08 19:13:41 -08:00
}
2019-07-08 12:21:22 -08:00
}
if !hasAction {
log.Printf("\t\t\t\tAdded new action %s for '%s'", curStmt.Sid)
curStmt.Action = append(curStmt.Action, baseAction)
curDoc.Statement[curIdx] = curStmt
updateDoc = true
}
2019-07-08 12:21:22 -08:00
}
}
if !found {
log.Printf("\t\t\t\tAdded new statement '%s'", baseStmt.Sid)
curDoc.Statement = append(curDoc.Statement, baseStmt)
updateDoc = true
2019-07-08 19:13:41 -08:00
}
}
if updateDoc {
dat, err := json.Marshal(curDoc)
2019-07-08 12:21:22 -08:00
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to json encode policy document")
2019-07-08 12:21:22 -08:00
}
_, err = svc.CreatePolicyVersion(&iam.CreatePolicyVersionInput{
PolicyArn: aws.String(policyArn),
2019-07-08 12:21:22 -08:00
PolicyDocument: aws.String(string(dat)),
SetAsDefault: aws.Bool(true),
2019-07-08 12:21:22 -08:00
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to read policy '%s' version '%s'", req.EcsTaskPolicyName, policyVersionId)
}
2019-07-08 12:21:22 -08:00
}
}
} else {
2019-07-10 16:24:10 -08:00
dat, err := json.Marshal(req.EcsTaskPolicyDocument)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrap(err, "Failed to json encode policy document")
2019-07-08 12:21:22 -08:00
}
2019-07-10 16:24:10 -08:00
req.EcsTaskPolicy.PolicyDocument = aws.String(string(dat))
2019-07-08 12:21:22 -08:00
// If no repository was found, create one.
2019-07-10 16:24:10 -08:00
res, err := svc.CreatePolicy(req.EcsTaskPolicy)
2019-07-08 19:13:41 -08:00
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create task policy '%s'", req.EcsTaskPolicyName)
2019-07-08 12:21:22 -08:00
}
policyArn = *res.Policy.Arn
2019-07-08 19:13:41 -08:00
log.Printf("\t\t\tCreated policy '%s'", policyArn)
}
log.Printf("\t%s\tConfigured default service policy.\n", tests.Success)
}
// Find or create role for TaskRoleArn.
{
log.Printf("\tAppend TaskRoleArn to task definition input for role %s.", req.EcsTaskRoleName)
res, err := svc.GetRole(&iam.GetRoleInput{
RoleName: aws.String(req.EcsTaskRoleName),
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to find task role '%s'", req.EcsTaskRoleName)
2019-07-08 19:13:41 -08:00
}
}
2019-07-08 12:21:22 -08:00
if res.Role != nil {
taskDefInput.TaskRoleArn = res.Role.Arn
log.Printf("\t\t\tFound role '%s'", *taskDefInput.TaskRoleArn)
} else {
// If no repository was found, create one.
2019-07-11 00:58:45 -08:00
res, err := svc.CreateRole(req.EcsTaskRole)
2019-07-08 12:21:22 -08:00
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create task role '%s'", req.EcsTaskRoleName)
2019-07-08 12:21:22 -08:00
}
taskDefInput.TaskRoleArn = res.Role.Arn
log.Printf("\t\t\tCreated role '%s'", *taskDefInput.TaskRoleArn)
//_, err = svc.UpdateAssumeRolePolicy(&iam.UpdateAssumeRolePolicyInput{
// PolicyDocument: ,
// RoleName: aws.String(roleName),
//})
//if err != nil {
// return errors.Wrapf(err, "failed to create task role '%s'", roleName)
//}
}
2019-07-08 12:21:22 -08:00
_, err = svc.AttachRolePolicy(&iam.AttachRolePolicyInput{
PolicyArn: aws.String(policyArn),
RoleName: aws.String(req.EcsTaskRoleName),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to attach policy '%s' to task role '%s'", policyArn, req.EcsTaskRoleName)
2019-07-08 12:21:22 -08:00
}
log.Printf("\t%s\tTaskRoleArn updated.\n", tests.Success)
2019-07-08 12:21:22 -08:00
}
}
log.Println("\tRegister new task definition.")
2019-07-08 19:13:41 -08:00
{
svc := ecs.New(req.awsSession())
2019-07-07 12:52:55 -08:00
2019-07-08 19:13:41 -08:00
// Registers a new task.
res, err := svc.RegisterTaskDefinition(taskDefInput)
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to register task definition '%s'", *taskDefInput.Family)
2019-07-08 19:13:41 -08:00
}
taskDef = res.TaskDefinition
log.Printf("\t\tRegistered: %s.", *taskDef.TaskDefinitionArn)
log.Printf("\t\t\tRevision: %d.", *taskDef.Revision)
log.Printf("\t\t\tStatus: %s.", *taskDef.Status)
2019-07-07 12:52:55 -08:00
2019-07-08 19:13:41 -08:00
log.Printf("\t%s\tTask definition registered.\n", tests.Success)
}
}
2019-07-07 12:52:55 -08:00
2019-07-10 04:52:38 -08:00
// Try to find AWS ECS Service by name. This does not error on not found, but results are used to determine if
// the full creation process of a service needs to be executed.
2019-07-08 12:21:22 -08:00
var ecsService *ecs.Service
{
2019-07-10 04:52:38 -08:00
log.Println("ECS - Find Service")
svc := ecs.New(req.awsSession())
2019-07-07 12:52:55 -08:00
2019-07-13 20:50:00 -08:00
// Find service by ECS cluster and service name.
2019-07-08 12:21:22 -08:00
res, err := svc.DescribeServices(&ecs.DescribeServicesInput{
Cluster: ecsCluster.ClusterArn,
Services: []*string{aws.String(req.EcsServiceName)},
2019-07-08 12:21:22 -08:00
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != ecs.ErrCodeServiceNotFoundException {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to describe service '%s'", req.EcsServiceName)
2019-07-08 12:21:22 -08:00
}
} else if len(res.Services) > 0 {
2019-07-08 12:21:22 -08:00
ecsService = res.Services[0]
log.Printf("\t\tFound: %s.", *ecsService.ServiceArn)
// The desired number of instantiations of the task definition to keep running
// on the service. This value is specified when the service is created with
// CreateService, and it can be modified with UpdateService.
log.Printf("\t\t\tDesiredCount: %d.", *ecsService.DesiredCount)
// The number of tasks in the cluster that are in the PENDING state.
log.Printf("\t\t\tPendingCount: %d.", *ecsService.PendingCount)
// The number of tasks in the cluster that are in the RUNNING state.
log.Printf("\t\t\tRunningCount: %d.", *ecsService.RunningCount)
// The status of the service. The valid values are ACTIVE, DRAINING, or INACTIVE.
log.Printf("\t\t\tStatus: %s.", *ecsService.Status)
log.Printf("\t%s\tUsing ECS Service '%s'.\n", tests.Success, *ecsService.ServiceName)
} else {
log.Printf("\t%s\tExisting ECS Service not found.\n", tests.Success)
}
2019-07-07 12:52:55 -08:00
}
// Check to see if the service should be re-created instead of updated.
if ecsService != nil {
var (
recreateService bool
forceDelete bool
)
if req.RecreateService {
// Flag was included to force recreate.
recreateService = true
forceDelete = true
} else if req.EnableEcsElb && (ecsService.LoadBalancers == nil || len(ecsService.LoadBalancers) == 0) {
2019-07-10 16:24:10 -08:00
// Service was created without ELB and now ELB is enabled.
recreateService = true
} else if !req.EnableEcsElb && (ecsService.LoadBalancers != nil && len(ecsService.LoadBalancers) > 0) {
// Service was created with ELB and now ELB is disabled.
recreateService = true
2019-07-10 16:24:10 -08:00
} else if sdService != nil && sdService.Arn != nil && (ecsService.ServiceRegistries == nil || len(ecsService.ServiceRegistries) == 0) {
// Service was created without Service Discovery and now Service Discovery is enabled.
recreateService = true
} else if (sdService == nil || sdService.Arn == nil) && (ecsService.ServiceRegistries != nil && len(ecsService.ServiceRegistries) > 0) {
// Service was created with Service Discovery and now Service Discovery is disabled.
recreateService = true
}
2019-07-13 20:50:00 -08:00
// If determined from above that service needs to be recreated.
if recreateService {
2019-07-13 20:50:00 -08:00
// Needs to delete any associated services on ECS first before it can be recreated.
log.Println("ECS - Delete Service")
svc := ecs.New(req.awsSession())
2019-07-13 20:50:00 -08:00
// The service cannot be stopped while it is scaled above 0.
2019-07-11 14:46:05 -08:00
if ecsService.DesiredCount != nil && *ecsService.DesiredCount > 0 {
2019-07-10 16:24:10 -08:00
log.Println("\t\tScaling service down to zero.")
_, err := svc.UpdateService(&ecs.UpdateServiceInput{
2019-07-11 14:46:05 -08:00
Cluster: ecsService.ClusterArn,
Service: ecsService.ServiceArn,
2019-07-10 16:24:10 -08:00
DesiredCount: aws.Int64(int64(0)),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to update service '%s'", ecsService.ServiceName)
2019-07-10 16:24:10 -08:00
}
2019-07-13 20:50:00 -08:00
// It may take some time for the service to scale down, so need to wait.
2019-07-10 16:24:10 -08:00
log.Println("\t\tWait for the service to scale down.")
err = svc.WaitUntilServicesStable(&ecs.DescribeServicesInput{
2019-07-11 14:46:05 -08:00
Cluster: ecsCluster.ClusterArn,
2019-07-10 16:24:10 -08:00
Services: aws.StringSlice([]string{*ecsService.ServiceArn}),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to wait for service '%s' to enter stable state", *ecsService.ServiceName)
2019-07-10 16:24:10 -08:00
}
}
2019-07-13 20:50:00 -08:00
// Once task count is 0 for the service, then can delete it.
2019-07-10 16:24:10 -08:00
log.Println("\t\tDelete Service.")
res, err := svc.DeleteService(&ecs.DeleteServiceInput{
Cluster: ecsService.ClusterArn,
Service: ecsService.ServiceArn,
// If true, allows you to delete a service even if it has not been scaled down
// to zero tasks. It is only necessary to use this if the service is using the
// REPLICA scheduling strategy.
Force: aws.Bool(forceDelete),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to delete service '%s'", ecsService.ServiceName)
}
2019-07-10 16:24:10 -08:00
ecsService = res.Service
2019-07-11 00:58:45 -08:00
log.Println("\t\tWait for the service to be deleted.")
err = svc.WaitUntilServicesInactive(&ecs.DescribeServicesInput{
2019-07-11 14:46:05 -08:00
Cluster: ecsCluster.ClusterArn,
2019-07-11 00:58:45 -08:00
Services: aws.StringSlice([]string{*ecsService.ServiceArn}),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to wait for service '%s' to enter stable state", *ecsService.ServiceName)
2019-07-11 00:58:45 -08:00
}
// Manually mark the ECS has inactive since WaitUntilServicesInactive was executed.
ecsService.Status = aws.String("INACTIVE")
log.Printf("\t%s\tDelete Service.\n", tests.Success)
}
}
2019-07-13 20:50:00 -08:00
// If the service exists on ECS, update the service, else create a new service.
2019-07-08 19:13:41 -08:00
if ecsService != nil && *ecsService.Status != "INACTIVE" {
2019-07-08 12:21:22 -08:00
log.Println("ECS - Update Service")
svc := ecs.New(req.awsSession())
2019-07-08 12:21:22 -08:00
2019-07-11 00:58:45 -08:00
var desiredCount int64
if req.EcsServiceDesiredCount > 0 {
desiredCount = req.EcsServiceDesiredCount
} else {
// Maintain the current count set on the existing service.
desiredCount := *ecsService.DesiredCount
// If the desired count is zero because it was spun down for termination of staging env, update to launch
// with at least once task running for the service.
if desiredCount == 0 {
desiredCount = 1
}
2019-07-07 12:52:55 -08:00
}
2019-07-10 00:17:35 -08:00
updateRes, err := svc.UpdateService(&ecs.UpdateServiceInput{
Cluster: ecsCluster.ClusterName,
Service: ecsService.ServiceName,
DesiredCount: aws.Int64(desiredCount),
HealthCheckGracePeriodSeconds: ecsService.HealthCheckGracePeriodSeconds,
TaskDefinition: taskDef.TaskDefinitionArn,
2019-07-08 19:13:41 -08:00
2019-07-08 12:21:22 -08:00
// Whether to force a new deployment of the service. Deployments are not forced
// by default. You can use this option to trigger a new deployment with no service
// definition changes. For example, you can update a service's tasks to use
// a newer Docker image with the same image/tag combination (my_image:latest)
// or to roll Fargate tasks onto a newer platform version.
ForceNewDeployment: aws.Bool(false),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to update service '%s'", *ecsService.ServiceName)
2019-07-07 12:52:55 -08:00
}
2019-07-10 00:17:35 -08:00
ecsService = updateRes.Service
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tUpdated ECS Service '%s'.\n", tests.Success, *ecsService.ServiceName)
2019-07-08 12:21:22 -08:00
} else {
2019-07-08 19:13:41 -08:00
2019-07-13 20:50:00 -08:00
// If not service exists on ECS, then create it.
2019-07-08 12:21:22 -08:00
log.Println("ECS - Create Service")
2019-07-08 19:13:41 -08:00
{
2019-07-08 12:21:22 -08:00
svc := ecs.New(req.awsSession())
2019-07-08 19:13:41 -08:00
var assignPublicIp *string
var healthCheckGracePeriodSeconds *int64
2019-07-08 19:13:41 -08:00
if len(ecsELBs) == 0 {
assignPublicIp = aws.String("ENABLED")
} else {
assignPublicIp = aws.String("DISABLED")
healthCheckGracePeriodSeconds = req.EscServiceHealthCheckGracePeriodSeconds
2019-07-08 19:13:41 -08:00
}
2019-07-13 20:50:00 -08:00
// When ELB is enabled and get the following error when using the default VPC.
2019-07-11 14:15:29 -08:00
// Status reason CannotPullContainerError:
// Error response from daemon:
// Get https://888955683113.dkr.ecr.us-west-2.amazonaws.com/v2/:
// net/http: request canceled while waiting for connection
// (Client.Timeout exceeded while awaiting headers)
assignPublicIp = aws.String("ENABLED")
serviceInput := &ecs.CreateServiceInput{
2019-07-08 19:13:41 -08:00
// The short name or full Amazon Resource Name (ARN) of the cluster that your
// service is running on. If you do not specify a cluster, the default cluster
// is assumed.
Cluster: ecsCluster.ClusterName,
// The name of your service. Up to 255 letters (uppercase and lowercase), numbers,
// and hyphens are allowed. Service names must be unique within a cluster, but
// you can have similarly named services in multiple clusters within a Region
// or across multiple Regions.
//
// ServiceName is a required field
ServiceName: aws.String(req.EcsServiceName),
2019-07-08 19:13:41 -08:00
// Optional deployment parameters that control how many tasks run during the
// deployment and the ordering of stopping and starting tasks.
DeploymentConfiguration: &ecs.DeploymentConfiguration{
2019-07-08 19:13:41 -08:00
// Refer to documentation for flags.ecsServiceMaximumPercent
MaximumPercent: req.EcsServiceMaximumPercent,
2019-07-08 19:13:41 -08:00
// Refer to documentation for flags.ecsServiceMinimumHealthyPercent
MinimumHealthyPercent: req.EcsServiceMinimumHealthyPercent,
2019-07-08 19:13:41 -08:00
},
// Refer to documentation for flags.ecsServiceDesiredCount.
DesiredCount: aws.Int64(req.EcsServiceDesiredCount),
2019-07-08 19:13:41 -08:00
// Specifies whether to enable Amazon ECS managed tags for the tasks within
// the service. For more information, see Tagging Your Amazon ECS Resources
// (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-using-tags.html)
// in the Amazon Elastic Container Service Developer Guide.
EnableECSManagedTags: aws.Bool(false),
// The period of time, in seconds, that the Amazon ECS service scheduler should
// ignore unhealthy Elastic Load Balancing target health checks after a task
// has first started. This is only valid if your service is configured to use
// a load balancer. If your service's tasks take a while to start and respond
// to Elastic Load Balancing health checks, you can specify a health check grace
// period of up to 2,147,483,647 seconds. During that time, the ECS service
// scheduler ignores health check status. This grace period can prevent the
// ECS service scheduler from marking tasks as unhealthy and stopping them before
// they have time to come up.
HealthCheckGracePeriodSeconds: healthCheckGracePeriodSeconds,
2019-07-08 19:13:41 -08:00
// The launch type on which to run your service. For more information, see Amazon
// ECS Launch Types (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html)
// in the Amazon Elastic Container Service Developer Guide.
LaunchType: aws.String("FARGATE"),
// A load balancer object representing the load balancer to use with your service.
LoadBalancers: ecsELBs,
// The network configuration for the service. This parameter is required for
// task definitions that use the awsvpc network mode to receive their own elastic
// network interface, and it is not supported for other network modes. For more
// information, see Task Networking (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html)
// in the Amazon Elastic Container Service Developer Guide.
NetworkConfiguration: &ecs.NetworkConfiguration{
AwsvpcConfiguration: &ecs.AwsVpcConfiguration{
// Whether the task's elastic network interface receives a public IP address.
// The default value is DISABLED.
AssignPublicIp: assignPublicIp,
// The security groups associated with the task or service. If you do not specify
// a security group, the default security group for the VPC is used. There is
// a limit of 5 security groups that can be specified per AwsVpcConfiguration.
// All specified security groups must be from the same VPC.
SecurityGroups: aws.StringSlice([]string{securityGroupId}),
2019-07-08 19:13:41 -08:00
// The subnets associated with the task or service. There is a limit of 16 subnets
// that can be specified per AwsVpcConfiguration.
// All specified subnets must be from the same VPC.
// Subnets is a required field
2019-07-11 02:52:10 -08:00
Subnets: aws.StringSlice(projectSubnetsIDs),
2019-07-08 19:13:41 -08:00
},
},
// The family and revision (family:revision) or full ARN of the task definition
// to run in your service. If a revision is not specified, the latest ACTIVE
// revision is used. If you modify the task definition with UpdateService, Amazon
// ECS spawns a task with the new version of the task definition and then stops
// an old task after the new version is running.
TaskDefinition: taskDef.TaskDefinitionArn,
// The metadata that you apply to the service to help you categorize and organize
// them. Each tag consists of a key and an optional value, both of which you
// define. When a service is deleted, the tags are deleted as well. Tag keys
// can have a maximum character length of 128 characters, and tag values can
// have a maximum length of 256 characters.
Tags: []*ecs.Tag{
&ecs.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
&ecs.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
2019-07-08 19:13:41 -08:00
},
}
2019-07-10 16:24:10 -08:00
// Add the Service Discovery registry to the ECS service.
if sdService != nil {
if serviceInput.ServiceRegistries == nil {
serviceInput.ServiceRegistries = []*ecs.ServiceRegistry{}
}
serviceInput.ServiceRegistries = append(serviceInput.ServiceRegistries, &ecs.ServiceRegistry{
RegistryArn: sdService.Arn,
})
}
createRes, err := svc.CreateService(serviceInput)
// If tags aren't enabled for the account, try the request again without them.
// https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2/
if err != nil && strings.Contains(err.Error(), "ARN and resource ID format must be enabled") {
serviceInput.Tags = nil
createRes, err = svc.CreateService(serviceInput)
}
2019-07-08 19:13:41 -08:00
if err != nil {
2019-07-13 20:50:00 -08:00
return errors.Wrapf(err, "Failed to create service '%s'", req.EcsServiceName)
2019-07-08 19:13:41 -08:00
}
ecsService = createRes.Service
log.Printf("\t%s\tCreated ECS Service '%s'.\n", tests.Success, *ecsService.ServiceName)
2019-07-08 19:13:41 -08:00
}
2019-07-07 12:52:55 -08:00
}
2019-07-14 19:13:09 -08:00
// When static files are enabled to be to stored on S3, we need to upload all of them.
if req.StaticFilesS3Enable {
2019-07-14 23:53:31 -08:00
log.Println("\tUpload static files to public S3 bucket")
2019-07-14 19:13:09 -08:00
staticDir := filepath.Join(req.ServiceDir, "static")
err := SyncPublicS3Files(req.awsSession(), req.S3BucketPublicName, req.StaticFilesS3Prefix, staticDir)
if err != nil {
2019-07-14 21:26:19 -08:00
return errors.Wrapf(err, "Failed to sync static files from %s to s3://%s/%s", staticDir, req.S3BucketPublicName, req.StaticFilesS3Prefix)
2019-07-14 19:13:09 -08:00
}
2019-07-14 23:53:31 -08:00
log.Printf("\t%s\tFiles uploaded to s3://%s/%s.\n", tests.Success, req.S3BucketPublicName, req.StaticFilesS3Prefix)
2019-07-14 19:13:09 -08:00
}
2019-07-10 04:52:38 -08:00
// Wait for the updated or created service to enter a stable state.
2019-07-10 00:17:35 -08:00
{
2019-07-10 04:52:38 -08:00
log.Println("\tWaiting for service to enter stable state.")
2019-07-10 00:17:35 -08:00
// Helper method to get the logs from cloudwatch for a specific task ID.
getTaskLogs := func(taskId string) ([]string, error) {
if req.S3BucketPrivateName == "" {
// No private S3 bucket defined so unable to export logs streams.
return []string{}, nil
}
// Stream name generated by ECS for the awslogs driver.
logStreamName := fmt.Sprintf("ecs/%s/%s", *ecsService.ServiceName, taskId)
2019-07-13 20:50:00 -08:00
// Define S3 key prefix used to export the stream logs to.
2019-07-10 00:17:35 -08:00
s3KeyPrefix := filepath.Join(req.S3BucketTempPrefix, "logs/cloudwatchlogs/exports", req.CloudWatchLogGroupName)
var downloadPrefix string
{
svc := cloudwatchlogs.New(req.awsSession())
createRes, err := svc.CreateExportTask(&cloudwatchlogs.CreateExportTaskInput{
2019-07-11 14:46:05 -08:00
LogGroupName: aws.String(req.CloudWatchLogGroupName),
2019-07-10 00:17:35 -08:00
LogStreamNamePrefix: aws.String(logStreamName),
//TaskName: aws.String(taskId),
2019-07-11 14:46:05 -08:00
Destination: aws.String(req.S3BucketPrivateName),
2019-07-10 00:17:35 -08:00
DestinationPrefix: aws.String(s3KeyPrefix),
2019-07-11 14:46:05 -08:00
From: aws.Int64(startTime.UTC().AddDate(0, 0, -1).UnixNano() / int64(time.Millisecond)),
To: aws.Int64(time.Now().UTC().AddDate(0, 0, 1).UnixNano() / int64(time.Millisecond)),
2019-07-10 00:17:35 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return []string{}, errors.Wrapf(err, "Failed to create export task for from log group '%s' with stream name prefix '%s'", req.CloudWatchLogGroupName, logStreamName)
2019-07-10 00:17:35 -08:00
}
exportTaskId := *createRes.TaskId
for {
descRes, err := svc.DescribeExportTasks(&cloudwatchlogs.DescribeExportTasksInput{
TaskId: aws.String(exportTaskId),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return []string{}, errors.Wrapf(err, "Failed to describe export task '%s' for from log group '%s' with stream name prefix '%s'", exportTaskId, req.CloudWatchLogGroupName, logStreamName)
2019-07-10 00:17:35 -08:00
}
2019-07-11 14:46:05 -08:00
taskStatus := *descRes.ExportTasks[0].Status.Code
2019-07-10 00:17:35 -08:00
2019-07-11 14:46:05 -08:00
if taskStatus == "COMPLETED" {
2019-07-10 00:17:35 -08:00
downloadPrefix = filepath.Join(s3KeyPrefix, exportTaskId) + "/"
break
} else if taskStatus == "CANCELLED" || taskStatus == "FAILED" {
break
}
time.Sleep(time.Second * 5)
}
}
2019-07-13 20:50:00 -08:00
// If downloadPrefix is set, then get logs from corresponding file for service.
2019-07-10 00:17:35 -08:00
var logLines []string
if downloadPrefix != "" {
svc := s3.New(req.awsSession())
var s3Keys []string
err := svc.ListObjectsPages(&s3.ListObjectsInput{
Bucket: aws.String(req.S3BucketPrivateName),
Prefix: aws.String(downloadPrefix),
},
func(res *s3.ListObjectsOutput, lastPage bool) bool {
for _, obj := range res.Contents {
s3Keys = append(s3Keys, *obj.Key)
}
return !lastPage
})
if err != nil {
2019-07-13 20:50:00 -08:00
return []string{}, errors.Wrapf(err, "Failed to list objects from s3 bucket '%s' with prefix '%s'", req.S3BucketPrivateName, downloadPrefix)
2019-07-10 00:17:35 -08:00
}
2019-07-13 20:50:00 -08:00
// Iterate trough S3 keys and get logs from file.
2019-07-10 00:17:35 -08:00
for _, s3Key := range s3Keys {
res, err := svc.GetObject(&s3.GetObjectInput{
Bucket: aws.String(req.S3BucketPrivateName),
2019-07-11 14:46:05 -08:00
Key: aws.String(s3Key),
2019-07-10 00:17:35 -08:00
})
if err != nil {
2019-07-13 20:50:00 -08:00
return []string{}, errors.Wrapf(err, "Failed to get object '%s' from s3 bucket", s3Key, req.S3BucketPrivateName)
2019-07-10 00:17:35 -08:00
}
2019-07-11 14:46:05 -08:00
r, _ := gzip.NewReader(res.Body)
2019-07-10 00:17:35 -08:00
dat, err := ioutil.ReadAll(r)
res.Body.Close()
if err != nil {
return []string{}, errors.Wrapf(err, "failed to read object '%s' from s3 bucket", s3Key, req.S3BucketPrivateName)
}
2019-07-13 20:50:00 -08:00
// Iterate through file by line break and add each line to array of logs.
2019-07-10 00:17:35 -08:00
for _, l := range strings.Split(string(dat), "\n") {
l = strings.TrimSpace(l)
if l == "" {
continue
}
logLines = append(logLines, l)
}
}
}
return logLines, nil
}
// Helper method to display tasks errors that failed to start while we wait for the service to stable state.
taskLogLines := make(map[string][]string)
checkTasks := func() (bool, error) {
svc := ecs.New(req.awsSession())
serviceTaskRes, err := svc.ListTasks(&ecs.ListTasksInput{
2019-07-11 14:46:05 -08:00
Cluster: aws.String(req.EcsClusterName),
ServiceName: aws.String(req.EcsServiceName),
2019-07-10 00:17:35 -08:00
DesiredStatus: aws.String("STOPPED"),
})
if err != nil {
2019-07-13 20:50:00 -08:00
return false, errors.Wrapf(err, "Failed to list tasks for cluster '%s' service '%s'", req.EcsClusterName, req.EcsServiceName)
2019-07-10 00:17:35 -08:00
}
if len(serviceTaskRes.TaskArns) == 0 {
return false, nil
}
taskRes, err := svc.DescribeTasks(&ecs.DescribeTasksInput{
Cluster: aws.String(req.EcsClusterName),
Tasks: serviceTaskRes.TaskArns,
})
if err != nil {
2019-07-13 20:50:00 -08:00
return false, errors.Wrapf(err, "Failed to describe %d tasks for cluster '%s'", len(serviceTaskRes.TaskArns), req.EcsClusterName)
2019-07-10 00:17:35 -08:00
}
var failures []*ecs.Failure
var stoppedCnt int64
for _, t := range taskRes.Tasks {
if *t.TaskDefinitionArn != *taskDef.TaskDefinitionArn || t.TaskArn == nil {
continue
}
stoppedCnt = stoppedCnt + 1
taskId := filepath.Base(*t.TaskArn)
log.Printf("\t\t\tTask %s stopped\n", *t.TaskArn)
for _, tc := range t.Containers {
if tc.ExitCode != nil && tc.Reason != nil {
log.Printf("\t\t\tContainer %s exited with %d - %s.\n", *tc.Name, *tc.ExitCode, *tc.Reason)
} else if tc.ExitCode != nil {
log.Printf("\t\t\tContainer %s exited with %d.\n", *tc.Name, *tc.ExitCode)
2019-07-11 14:46:05 -08:00
} else {
2019-07-10 00:17:35 -08:00
log.Printf("\t\t\tContainer %s exited.\n", *tc.Name)
}
}
// Avoid exporting the logs multiple times.
logLines, ok := taskLogLines[taskId]
if !ok {
logLines, err = getTaskLogs(taskId)
if err != nil {
2019-07-13 20:50:00 -08:00
return false, errors.Wrapf(err, "Failed to get logs for task %s for cluster '%s'", *t.TaskArn, req.EcsClusterName)
2019-07-10 00:17:35 -08:00
}
taskLogLines[taskId] = logLines
}
if len(logLines) > 0 {
log.Printf("\t\t\tTask Logs:\n")
for _, l := range logLines {
log.Printf("\t\t\t\t%s\n", l)
}
}
2019-07-11 00:58:45 -08:00
if t.StopCode != nil && t.StoppedReason != nil {
log.Printf("\t%s\tTask %s stopped with %s - %s.\n", tests.Failed, *t.TaskArn, *t.StopCode, *t.StoppedReason)
} else if t.StopCode != nil {
log.Printf("\t%s\tTask %s stopped with %s.\n", tests.Failed, *t.TaskArn, *t.StopCode)
2019-07-11 14:46:05 -08:00
} else {
2019-07-11 00:58:45 -08:00
log.Printf("\t%s\tTask %s stopped.\n", tests.Failed, *t.TaskArn)
}
2019-07-10 00:17:35 -08:00
// Limit failures to only the current task definition.
for _, f := range taskRes.Failures {
if *f.Arn == *t.TaskArn {
failures = append(failures, f)
}
}
}
if len(failures) > 0 {
for _, t := range failures {
log.Printf("\t%s\tTask %s failed with %s.\n", tests.Failed, *t.Arn, *t.Reason)
}
}
// If the number of stopped tasks with the current task def match the desired count for the service,
// then we no longer need to continue to check the status of the tasks.
if stoppedCnt == *ecsService.DesiredCount {
return true, nil
}
return false, nil
}
// New wait group with only a count of one, this will allow the first go worker to exit to cancel both.
checkErr := make(chan error, 1)
// Check the status of the service tasks and print out info for debugging.
ticker := time.NewTicker(10 * time.Second)
go func() {
for {
select {
2019-07-11 14:46:05 -08:00
case <-ticker.C:
2019-07-10 00:17:35 -08:00
stop, err := checkTasks()
if err != nil {
log.Printf("\t%s\tFailed to check tasks.\n%+v\n", tests.Failed, err)
}
if stop {
2019-07-13 20:50:00 -08:00
checkErr <- errors.New("All tasks for service are stopped")
2019-07-10 00:17:35 -08:00
return
}
}
}
}()
// Use the AWS ECS method to check for the service to be stable.
go func() {
svc := ecs.New(req.awsSession())
err := svc.WaitUntilServicesStable(&ecs.DescribeServicesInput{
2019-07-11 14:46:05 -08:00
Cluster: ecsCluster.ClusterArn,
2019-07-10 00:17:35 -08:00
Services: aws.StringSlice([]string{*ecsService.ServiceArn}),
})
if err != nil {
2019-07-13 20:50:00 -08:00
checkErr <- errors.Wrapf(err, "Failed to wait for service '%s' to enter stable state", *ecsService.ServiceName)
2019-07-10 00:17:35 -08:00
} else {
// All done.
checkErr <- nil
}
}()
2019-07-11 14:46:05 -08:00
if err := <-checkErr; err != nil {
2019-07-10 00:17:35 -08:00
log.Printf("\t%s\tFailed to check tasks.\n%+v\n", tests.Failed, err)
2019-07-14 21:26:19 -08:00
return err
2019-07-10 00:17:35 -08:00
}
// Wait for one of the methods to finish and then ensure the ticker is stopped.
ticker.Stop()
log.Printf("\t%s\tService running.\n", tests.Success)
}
2019-07-08 12:21:22 -08:00
return nil
}