2019-07-14 14:55:34 -08:00
package cicd
2019-07-07 12:52:55 -08:00
import (
2019-07-10 00:17:35 -08:00
"compress/gzip"
2019-07-09 02:21:46 -08:00
"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"
2019-07-09 02:21:46 -08:00
"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
2019-07-13 12:16:28 -08:00
"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
2019-07-14 16:16:25 -08:00
"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"
2019-07-09 02:21:46 -08:00
"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"
2019-07-14 16:16:25 -08:00
"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"
2019-07-14 14:55:34 -08:00
"github.com/urfave/cli"
2019-07-14 16:16:25 -08:00
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
2019-07-09 02:21:46 -08:00
"gopkg.in/go-playground/validator.v9"
2019-07-07 12:52:55 -08:00
)
2019-07-14 14:55:34 -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" `
2019-07-14 14:55:34 -08:00
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" `
2019-07-14 14:55:34 -08:00
}
// 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
2019-07-14 16:39:17 -08:00
GitlabRunnerEc2SecurityGroupName string ` validate:"required" `
2019-07-14 14:55:34 -08:00
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" `
2019-07-14 14:55:34 -08:00
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.
2019-07-09 02:21:46 -08:00
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." )
2019-07-09 02:21:46 -08:00
var req serviceDeployRequest
2019-07-08 12:21:22 -08:00
{
2019-07-14 15:33:23 -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
}
2019-07-09 02:21:46 -08:00
req = serviceDeployRequest {
2019-07-14 14:55:34 -08:00
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
2019-07-13 03:03:30 -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 ,
2019-07-09 02:21:46 -08:00
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-14 22:08:14 -08:00
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 ) ) )
2019-07-14 22:08:14 -08:00
allowedMethods . SetCachedMethods ( cacheMethods )
2019-07-15 00:37:07 -08:00
domainId := "S3-" + req . S3BucketPublicName
2019-07-14 22:08:14 -08:00
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-14 22:08:14 -08:00
} ,
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 {
2019-07-14 22:08:14 -08:00
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-14 22:08:14 -08:00
}
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.
2019-07-14 14:55:34 -08:00
req . EcrRepositoryName = ecrRepositoryName ( req . ProjectName )
2019-07-09 02:21:46 -08:00
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.
2019-07-09 02:21:46 -08:00
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 ) } ,
} ,
}
2019-07-09 02:21:46 -08:00
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.
2019-07-09 02:21:46 -08:00
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
2019-07-09 02:21:46 -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" ,
}
2019-07-09 02:21:46 -08:00
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 ) } ,
} ,
}
2019-07-09 02:21:46 -08:00
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
}
2019-07-09 02:21:46 -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" ,
2019-07-15 12:52:17 -08:00
"s3:ListObjects" ,
"s3:PutObject" ,
"s3:PutObjectAcl" ,
"cloudfront:ListDistributions" ,
2019-07-10 16:24:10 -08:00
"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-08-02 15:03:32 -08:00
"ses:ListIdentities" ,
2019-07-11 00:58:45 -08:00
"secretsmanager:ListSecretVersionIds" ,
"secretsmanager:GetSecretValue" ,
"secretsmanager:CreateSecret" ,
"secretsmanager:UpdateSecret" ,
2019-07-13 03:03:30 -08:00
"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.
2019-07-09 02:21:46 -08:00
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 ) ,
} ,
}
2019-07-09 02:21:46 -08:00
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.
2019-07-09 02:21:46 -08:00
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-09 02:21:46 -08:00
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
2019-07-14 16:39:17 -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"
2019-07-09 02:21:46 -08:00
// 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 ) } ,
} ,
}
2019-07-09 02:21:46 -08:00
log . Printf ( "\t\t\tSet ELB Name to '%s'." , req . ElbLoadBalancerName )
2019-07-11 00:58:45 -08:00
2019-07-11 14:42:20 -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 ) ,
2019-07-11 14:42:20 -08:00
// 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-09 02:21:46 -08:00
}
2019-07-08 19:13:41 -08:00
// Set ECS configs based on specified env.
if flags . Env == "prod" {
2019-07-09 02:21:46 -08:00
req . EcsServiceMinimumHealthyPercent = aws . Int64 ( 100 )
req . EcsServiceMaximumPercent = aws . Int64 ( 200 )
2019-07-08 19:13:41 -08:00
2019-07-09 02:21:46 -08:00
req . ElbDeregistrationDelay = aws . Int ( 300 )
2019-07-08 19:13:41 -08:00
} else {
2019-07-09 02:21:46 -08:00
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
2019-07-09 02:21:46 -08:00
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 {
2019-07-14 15:33:23 -08:00
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" ) ,
2019-07-15 18:49:28 -08:00
MasterUserPassword : aws . String ( uuid . NewRandom ( ) . String ( ) ) ,
2019-07-11 14:46:05 -08:00
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 )
}
2019-07-09 02:21:46 -08:00
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." )
2019-07-09 02:21:46 -08:00
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.
2019-07-09 02:21:46 -08:00
var awsRepo * ecr . Repository
2019-07-08 12:21:22 -08:00
descRes , err := svc . DescribeRepositories ( & ecr . DescribeRepositoriesInput {
2019-07-09 02:21:46 -08:00
RepositoryNames : [ ] * string { aws . String ( req . EcrRepositoryName ) } ,
2019-07-08 12:21:22 -08:00
} )
if err != nil {
2019-07-14 14:55:34 -08:00
// 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
}
2019-07-14 14:55:34 -08:00
log . Printf ( "\t\tFound: %s." , * awsRepo . RepositoryArn )
2019-07-08 12:21:22 -08:00
2019-07-14 14:55:34 -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
2019-07-09 02:21:46 -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
2019-07-09 02:21:46 -08:00
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 == "" {
2019-07-14 15:33:23 -08:00
prefixedSecretId := secretID ( req . ProjectName , req . Env , "datadog" )
2019-07-08 12:21:22 -08:00
var err error
2019-07-09 02:21:46 -08:00
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 {
2019-07-09 02:21:46 -08:00
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"
2019-07-09 02:21:46 -08:00
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" )
2019-07-09 02:21:46 -08:00
svc := cloudwatchlogs . New ( req . awsSession ( ) )
2019-07-08 19:13:41 -08:00
// If no log group was found, create one.
2019-07-09 02:21:46 -08:00
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 {
2019-07-09 02:21:46 -08:00
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
2019-07-09 02:21:46 -08:00
log . Printf ( "\t\tFound: %s." , req . CloudWatchLogGroupName )
2019-07-08 19:13:41 -08:00
} else {
2019-07-09 02:21:46 -08:00
log . Printf ( "\t\tCreated: %s." , req . CloudWatchLogGroupName )
2019-07-08 12:21:22 -08:00
}
2019-07-09 02:21:46 -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.
2019-07-14 16:39:17 -08:00
var runnerSgId string
2019-07-10 00:17:35 -08:00
err := svc . DescribeSecurityGroupsPages ( & ec2 . DescribeSecurityGroupsInput {
2019-07-14 16:39:17 -08:00
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
2019-07-14 16:39:17 -08:00
} 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
}
2019-07-14 16:39:17 -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.
2019-07-14 15:33:23 -08:00
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" )
2019-07-14 16:16:25 -08:00
// 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 { }
2019-07-13 03:03:30 -08:00
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
2019-07-11 14:42:20 -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 {
2019-07-11 14:42:20 -08:00
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 14:42:20 -08:00
}
}
2019-07-11 02:52:10 -08:00
2019-07-11 14:42:20 -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
2019-07-11 14:42:20 -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 )
2019-07-11 14:42:20 -08:00
}
targetGroup = createRes . TargetGroups [ 0 ]
2019-07-11 02:52:10 -08:00
2019-07-11 14:42:20 -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
}
2019-07-11 14:42:20 -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
2019-07-11 14:42:20 -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
}
2019-07-11 14:42:20 -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 {
2019-07-11 14:42:20 -08:00
"HTTP" : 80 ,
}
if req . EnableHTTPS {
listenerPorts [ "HTTPS" ] = 443
}
2019-07-11 02:52:10 -08:00
2019-07-11 14:42:20 -08:00
for listenerProtocol , listenerPort := range listenerPorts {
2019-07-11 02:52:10 -08:00
var foundListener bool
for _ , cl := range curListeners {
2019-07-11 14:42:20 -08:00
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
2019-07-11 14:42:20 -08:00
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
2019-07-11 14:42:20 -08:00
Protocol : aws . String ( listenerProtocol ) ,
2019-07-11 02:52:10 -08:00
}
2019-07-11 14:42:20 -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
}
2019-07-11 14:42:20 -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" )
2019-07-09 02:21:46 -08:00
svc := ecs . New ( req . awsSession ( ) )
2019-07-08 19:13:41 -08:00
descRes , err := svc . DescribeClusters ( & ecs . DescribeClustersInput {
2019-07-09 02:21:46 -08:00
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
}
2019-07-09 02:21:46 -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
2019-07-09 02:21:46 -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 {
2019-07-13 03:03:30 -08:00
"{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
2019-07-13 12:16:28 -08:00
"{HTTPS_ENABLED}" : "false" ,
2019-07-13 03:03:30 -08:00
"{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" ) ,
2019-07-14 16:39:17 -08:00
"{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"
}
2019-07-13 12:16:28 -08:00
// 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.
2019-07-14 00:41:58 -08:00
if ! req . EnableEcsElb {
2019-07-13 12:16:28 -08:00
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.
2019-07-09 02:21:46 -08:00
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 == "" {
2019-07-09 02:21:46 -08:00
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 == "" {
2019-07-09 02:21:46 -08:00
taskDefInput . ContainerDefinitions [ 0 ] . Name = & req . EcsServiceName
2019-07-08 12:21:22 -08:00
}
if taskDefInput . ContainerDefinitions [ 0 ] . Image == nil || * taskDefInput . ContainerDefinitions [ 0 ] . Image == "" {
2019-07-09 02:21:46 -08:00
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
2019-07-09 02:21:46 -08:00
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
2019-07-09 02:21:46 -08:00
selectedCpu int64
2019-07-08 12:21:22 -08:00
)
if totalMemory < 8192 {
if totalMemory > 7168 {
selectedMemory = 8192
if totalCpu >= 2048 {
2019-07-09 02:21:46 -08:00
selectedCpu = 4096
2019-07-08 12:21:22 -08:00
} else if totalCpu >= 1024 {
selectedCpu = 2048
} else {
selectedCpu = 1024
}
} else if totalMemory > 6144 {
2019-07-09 02:21:46 -08:00
selectedMemory = 7168
2019-07-08 12:21:22 -08:00
if totalCpu >= 2048 {
2019-07-09 02:21:46 -08:00
selectedCpu = 4096
} else if totalCpu >= 1024 {
2019-07-08 12:21:22 -08:00
selectedCpu = 2048
} else {
selectedCpu = 1024
}
2019-07-09 02:21:46 -08:00
} else if totalMemory > 5120 || totalCpu >= 1024 {
selectedMemory = 6144
2019-07-08 12:21:22 -08:00
if totalCpu >= 2048 {
2019-07-09 02:21:46 -08:00
selectedCpu = 4096
} else if totalCpu >= 1024 {
2019-07-08 12:21:22 -08:00
selectedCpu = 2048
} else {
selectedCpu = 1024
}
} else if totalMemory > 4096 {
2019-07-09 02:21:46 -08:00
selectedMemory = 5120
2019-07-08 12:21:22 -08:00
if totalCpu >= 512 {
selectedCpu = 1024
} else {
selectedCpu = 512
}
} else if totalMemory > 3072 {
2019-07-09 02:21:46 -08:00
selectedMemory = 4096
2019-07-08 12:21:22 -08:00
if totalCpu >= 512 {
selectedCpu = 1024
} else {
selectedCpu = 512
}
2019-07-09 02:21:46 -08:00
} else if totalMemory > 2048 || totalCpu >= 512 {
selectedMemory = 3072
2019-07-08 12:21:22 -08:00
if totalCpu >= 512 {
selectedCpu = 1024
} else {
selectedCpu = 512
}
2019-07-09 02:21:46 -08:00
} 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
}
2019-07-09 02:21:46 -08:00
} else if totalMemory > 512 {
selectedMemory = 1024
2019-07-08 12:21:22 -08:00
if totalCpu >= 256 {
selectedCpu = 512
} else {
selectedCpu = 256
}
} else {
2019-07-09 02:21:46 -08:00
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 == "" {
2019-07-09 02:21:46 -08:00
svc := iam . New ( req . awsSession ( ) )
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -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 )
2019-07-09 02:21:46 -08:00
}
}
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
2019-07-09 02:21:46 -08:00
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 {
2019-07-09 02:21:46 -08:00
_ , 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
}
2019-07-09 02:21:46 -08:00
log . Printf ( "\t\t\t\tAttached Policy '%s'" , policyArn )
2019-07-08 19:13:41 -08:00
}
2019-07-09 02:21:46 -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 == "" {
2019-07-09 02:21:46 -08:00
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
}
}
2019-07-09 02:21:46 -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-09 02:21:46 -08:00
}
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -08:00
if policyArn != "" {
log . Printf ( "\t\t\tFound policy '%s' versionId '%s'" , policyArn , policyVersionId )
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -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-09 02:21:46 -08:00
}
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -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-09 02:21:46 -08:00
}
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -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-09 02:21:46 -08:00
}
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -08:00
var updateDoc bool
2019-07-10 16:24:10 -08:00
for _ , baseStmt := range req . EcsTaskPolicyDocument . Statement {
2019-07-09 02:21:46 -08:00
var found bool
for curIdx , curStmt := range curDoc . Statement {
if baseStmt . Sid != curStmt . Sid {
continue
}
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -08:00
found = true
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -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
}
2019-07-09 02:21:46 -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
}
}
2019-07-09 02:21:46 -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
}
2019-07-09 02:21:46 -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
}
2019-07-09 02:21:46 -08:00
_ , err = svc . CreatePolicyVersion ( & iam . CreatePolicyVersionInput {
PolicyArn : aws . String ( policyArn ) ,
2019-07-08 12:21:22 -08:00
PolicyDocument : aws . String ( string ( dat ) ) ,
2019-07-09 02:21:46 -08:00
SetAsDefault : aws . Bool ( true ) ,
2019-07-08 12:21:22 -08:00
} )
if err != nil {
2019-07-09 02:21:46 -08:00
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-09 02:21:46 -08:00
}
2019-07-08 12:21:22 -08:00
}
2019-07-09 02:21:46 -08:00
}
} else {
2019-07-10 16:24:10 -08:00
dat , err := json . Marshal ( req . EcsTaskPolicyDocument )
2019-07-09 02:21:46 -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
}
2019-07-10 16:24:10 -08:00
req . EcsTaskPolicy . PolicyDocument = aws . String ( string ( dat ) )
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -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
}
2019-07-09 02:21:46 -08:00
policyArn = * res . Policy . Arn
2019-07-08 19:13:41 -08:00
2019-07-09 02:21:46 -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-09 02:21:46 -08:00
}
2019-07-08 12:21:22 -08:00
2019-07-09 02:21:46 -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
}
2019-07-09 02:21:46 -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
2019-07-09 02:21:46 -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
}
2019-07-09 02:21:46 -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
{
2019-07-09 02:21:46 -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" )
2019-07-09 02:21:46 -08:00
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 {
2019-07-09 02:21:46 -08:00
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
}
2019-07-09 02:21:46 -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
}
2019-07-09 02:21:46 -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.
2019-07-09 02:21:46 -08:00
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-09 02:21:46 -08:00
}
2019-07-13 20:50:00 -08:00
// If determined from above that service needs to be recreated.
2019-07-09 02:21:46 -08:00
if recreateService {
2019-07-13 20:50:00 -08:00
// Needs to delete any associated services on ECS first before it can be recreated.
2019-07-09 02:21:46 -08:00
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 {
2019-07-09 02:21:46 -08:00
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-09 02:21:46 -08:00
}
2019-07-10 16:24:10 -08:00
ecsService = res . Service
2019-07-09 02:21:46 -08:00
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" )
2019-07-09 02:21:46 -08:00
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" )
2019-07-09 02:21:46 -08:00
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 {
2019-07-09 02:21:46 -08:00
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
2019-07-09 02:21:46 -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
2019-07-09 02:21:46 -08:00
svc := ecs . New ( req . awsSession ( ) )
2019-07-08 19:13:41 -08:00
var assignPublicIp * string
2019-07-09 02:21:46 -08:00
var healthCheckGracePeriodSeconds * int64
2019-07-08 19:13:41 -08:00
if len ( ecsELBs ) == 0 {
assignPublicIp = aws . String ( "ENABLED" )
} else {
assignPublicIp = aws . String ( "DISABLED" )
2019-07-09 02:21:46 -08:00
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" )
2019-07-09 02:21:46 -08:00
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
2019-07-09 02:21:46 -08:00
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.
2019-07-09 02:21:46 -08:00
DeploymentConfiguration : & ecs . DeploymentConfiguration {
2019-07-08 19:13:41 -08:00
// Refer to documentation for flags.ecsServiceMaximumPercent
2019-07-09 02:21:46 -08:00
MaximumPercent : req . EcsServiceMaximumPercent ,
2019-07-08 19:13:41 -08:00
// Refer to documentation for flags.ecsServiceMinimumHealthyPercent
2019-07-09 02:21:46 -08:00
MinimumHealthyPercent : req . EcsServiceMinimumHealthyPercent ,
2019-07-08 19:13:41 -08:00
} ,
// Refer to documentation for flags.ecsServiceDesiredCount.
2019-07-09 02:21:46 -08:00
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.
2019-07-09 02:21:46 -08:00
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.
2019-07-09 02:21:46 -08:00
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 {
2019-07-09 02:21:46 -08:00
& 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-09 02:21:46 -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 ,
} )
}
2019-07-09 02:21:46 -08:00
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/
2019-07-14 16:16:25 -08:00
if err != nil && strings . Contains ( err . Error ( ) , "ARN and resource ID format must be enabled" ) {
2019-07-09 02:21:46 -08:00
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
2019-07-09 02:21:46 -08:00
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
}