package cicd

import (
	"compress/gzip"
	"context"
	"crypto/md5"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
	"geeks-accelerator/oss/saas-starter-kit/internal/schema"
	"geeks-accelerator/oss/saas-starter-kit/tools/devops/internal/retry"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/acm"
	"github.com/aws/aws-sdk-go/service/cloudfront"
	"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/aws/aws-sdk-go/service/ecr"
	"github.com/aws/aws-sdk-go/service/ecs"
	"github.com/aws/aws-sdk-go/service/elasticache"
	"github.com/aws/aws-sdk-go/service/elbv2"
	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/aws/aws-sdk-go/service/rds"
	"github.com/aws/aws-sdk-go/service/route53"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
	"github.com/aws/aws-sdk-go/service/servicediscovery"
	"github.com/bobesa/go-domain-util/domainutil"
	"github.com/iancoleman/strcase"
	"github.com/lib/pq"
	"github.com/pborman/uuid"
	"github.com/pkg/errors"
	"github.com/urfave/cli"
	sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
	sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
	"gopkg.in/go-playground/validator.v9"
)

// 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.
	EnableHTTPS              bool            `validate:"omitempty" example:"false"`
	ServiceHostPrimary       string          `validate:"omitempty" example:"example-project.com"`
	ServiceHostNames         cli.StringSlice `validate:"omitempty" example:"subdomain.example-project.com"`
	S3BucketPrivateName      string          `validate:"omitempty" example:"saas-example-project-private"`
	S3BucketPublicName       string          `validate:"omitempty" example:"saas-example-project-public"`
	S3BucketPublicCloudfront bool            `validate:"omitempty" example:"false"`

	ProjectRoot     string `validate:"omitempty" example:"."`
	ProjectName     string ` validate:"omitempty" example:"example-project"`
	DockerFile      string `validate:"omitempty" example:"./cmd/web-api/Dockerfile"`
	EnableLambdaVPC bool   `validate:"omitempty" example:"false"`
	EnableEcsElb    bool   `validate:"omitempty" example:"false"`
	IsLambda        bool   `validate:"omitempty" example:"false"`

	StaticFilesS3Enable        bool `validate:"omitempty" example:"false"`
	StaticFilesImgResizeEnable bool `validate:"omitempty" example:"false"`

	RecreateService bool `validate:"omitempty" example:"false"`
}

// serviceDeployRequest defines the details needed to execute a service deployment.
type serviceDeployRequest struct {
	*serviceRequest

	EnableHTTPS        bool     `validate:"omitempty"`
	ServiceHostPrimary string   `validate:"omitempty,required_with=EnableHTTPS,fqdn"`
	ServiceHostNames   []string `validate:"omitempty,dive,fqdn"`

	EcrRepositoryName string `validate:"required"`

	EcsClusterName string `validate:"required"`
	EcsCluster     *ecs.CreateClusterInput

	EcsServiceName                          string `validate:"required"`
	EcsServiceDesiredCount                  int64  `validate:"required"`
	EcsServiceMinimumHealthyPercent         *int64 `validate:"omitempty"`
	EcsServiceMaximumPercent                *int64 `validate:"omitempty"`
	EscServiceHealthCheckGracePeriodSeconds *int64 `validate:"omitempty"`

	EcsExecutionRoleName       string `validate:"required"`
	EcsExecutionRole           *iam.CreateRoleInput
	EcsExecutionRolePolicyArns []string `validate:"required"`

	EcsTaskRoleName string `validate:"required"`
	EcsTaskRole     *iam.CreateRoleInput

	EcsTaskPolicyName     string `validate:"required"`
	EcsTaskPolicy         *iam.CreatePolicyInput
	EcsTaskPolicyDocument IamPolicyDocument

	Ec2SecurityGroupName string `validate:"required"`
	Ec2SecurityGroup     *ec2.CreateSecurityGroupInput

	GitlabRunnerEc2SecurityGroupName string `validate:"required"`

	CloudWatchLogGroupName string `validate:"required"`
	CloudWatchLogGroup     *cloudwatchlogs.CreateLogGroupInput

	S3BucketTempPrefix      string `validate:"required_with=S3BucketPrivateName S3BucketPublicName"`
	S3BucketPrivateName     string `validate:"omitempty"`
	S3BucketPublicName      string `validate:"omitempty"`
	S3BucketPublicKeyPrefix string `validate:"omitempty"`
	S3Buckets               []S3Bucket

	CloudfrontPublic *cloudfront.DistributionConfig

	StaticFilesS3Enable        bool   `validate:"omitempty"`
	StaticFilesS3Prefix        string `validate:"omitempty"`
	StaticFilesImgResizeEnable bool   `validate:"omitempty"`

	EnableEcsElb           bool   `validate:"omitempty"`
	ElbLoadBalancerName    string `validate:"omitempty"`
	ElbDeregistrationDelay *int   `validate:"omitempty"`
	ElbLoadBalancer        *elbv2.CreateLoadBalancerInput

	ElbTargetGroupName string `validate:"omitempty"`
	ElbTargetGroup     *elbv2.CreateTargetGroupInput

	VpcPublicName    string `validate:"omitempty"`
	VpcPublic        *ec2.CreateVpcInput
	VpcPublicSubnets []*ec2.CreateSubnetInput

	EnableLambdaVPC bool `validate:"omitempty"`
	IsLambda        bool `validate:"omitempty"`
	RecreateService bool `validate:"omitempty"`

	SDNamepsace *servicediscovery.CreatePrivateDnsNamespaceInput
	SDService   *servicediscovery.CreateServiceInput

	CacheCluster          *elasticache.CreateCacheClusterInput
	CacheClusterParameter []*elasticache.ParameterNameValue

	DBCluster  *rds.CreateDBClusterInput
	DBInstance *rds.CreateDBInstanceInput

	flags ServiceDeployFlags
}

// NewServiceDeployRequest generates a new request for executing deployment of a single service for a given set of CLI flags.
func NewServiceDeployRequest(log *log.Logger, flags ServiceDeployFlags) (*serviceDeployRequest, error) {

	// Validates specified CLI flags map to struct successfully.
	log.Println("Validate flags.")
	{
		errs := validator.New().Struct(flags)
		if errs != nil {
			return nil, errs
		}
		log.Printf("\t%s\tFlags ok.", tests.Success)
	}

	// Generate a deploy request using CLI flags and AWS credentials.
	log.Println("Generate deploy request.")
	var req serviceDeployRequest
	{
		// Define new service request.
		sr := &serviceRequest{
			ServiceName: flags.ServiceName,
			Env:         flags.Env,
			ProjectRoot: flags.ProjectRoot,
			ProjectName: flags.ProjectName,
			DockerFile:  flags.DockerFile,
		}
		if err := sr.init(log); err != nil {
			return nil, err
		}

		req = serviceDeployRequest{
			serviceRequest: sr,

			EnableHTTPS:        flags.EnableHTTPS,
			ServiceHostPrimary: flags.ServiceHostPrimary,
			ServiceHostNames:   flags.ServiceHostNames,

			StaticFilesS3Enable:        flags.StaticFilesS3Enable,
			StaticFilesImgResizeEnable: flags.StaticFilesImgResizeEnable,

			S3BucketPrivateName: flags.S3BucketPrivateName,
			S3BucketPublicName:  flags.S3BucketPublicName,

			IsLambda:        flags.IsLambda,
			EnableLambdaVPC: flags.EnableLambdaVPC,
			EnableEcsElb:    flags.EnableEcsElb,
			RecreateService: flags.RecreateService,

			flags: flags,
		}

		// 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
		log.Println("\tSet defaults.")
		{
			// 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)
			}

			// 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{
				ID:     aws.String("Rule for : " + req.S3BucketTempPrefix),
				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.
			// 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"}),

								// One or more origins you want customers to be able to access the bucket from.
								//
								// AllowedOrigins is a required field
								AllowedOrigins: aws.StringSlice([]string{"*"}),

								// 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{}),

								// The time in seconds that your browser is to cache the preflight response
								// for the specified resource.
								// MaxAgeSeconds: aws.Int64(),
							},
						},
					})

				// The S3 key prefix used as the origin when cloud front is enabled.
				if req.S3BucketPublicKeyPrefix == "" {
					req.S3BucketPublicKeyPrefix = "/public"
				}

				if flags.S3BucketPublicCloudfront {
					allowedMethods := &cloudfront.AllowedMethods{
						Items: aws.StringSlice([]string{"HEAD", "GET"}),
					}
					allowedMethods.Quantity = aws.Int64(int64(len(allowedMethods.Items)))

					cacheMethods := &cloudfront.CachedMethods{
						Items: aws.StringSlice([]string{"HEAD", "GET"}),
					}
					cacheMethods.Quantity = aws.Int64(int64(len(cacheMethods.Items)))
					allowedMethods.SetCachedMethods(cacheMethods)

					domainId := "S3-" + req.S3BucketPublicName
					domainName := fmt.Sprintf("%s.s3.%s.amazonaws.com", req.S3BucketPublicName, req.AwsCreds.Region)

					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),
								},
							},
						},
					}
					origins.Quantity = aws.Int64(int64(len(origins.Items)))

					req.CloudfrontPublic = &cloudfront.DistributionConfig{
						Comment:       aws.String(""),
						Enabled:       aws.Bool(true),
						HttpVersion:   aws.String("http2"),
						IsIPV6Enabled: aws.Bool(true),
						DefaultCacheBehavior: &cloudfront.DefaultCacheBehavior{
							TargetOriginId: aws.String(domainId),
							AllowedMethods: allowedMethods,
							Compress:       aws.Bool(true),
							DefaultTTL:     aws.Int64(1209600),
							MinTTL:         aws.Int64(604800),
							MaxTTL:         aws.Int64(31536000),
							ForwardedValues: &cloudfront.ForwardedValues{
								QueryString: aws.Bool(true),
								Cookies: &cloudfront.CookiePreference{
									Forward: aws.String("none"),
								},
							},
							TrustedSigners: &cloudfront.TrustedSigners{
								Enabled:  aws.Bool(false),
								Quantity: aws.Int64(0),
							},
							ViewerProtocolPolicy: aws.String("allow-all"),
						},
						Origins: origins,
						ViewerCertificate: &cloudfront.ViewerCertificate{
							CertificateSource:            aws.String("cloudfront"),
							MinimumProtocolVersion:       aws.String("TLSv1"),
							CloudFrontDefaultCertificate: aws.Bool(true),
						},
						PriceClass:      aws.String("PriceClass_All"),
						CallerReference: aws.String("devops-deploy"),
					}
				}
			}

			// 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),

							// 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),

							// 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(`{
							"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)
						}(),
					})
			}

			// 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")
			}

			// Set default AWS ECR Repository Name.
			req.EcrRepositoryName = ecrRepositoryName(req.ProjectName)
			log.Printf("\t\t\tSet ECR Repository Name to '%s'.", req.EcrRepositoryName)

			if req.IsLambda {

			} else {

			}

			// Set default AWS ECS Cluster Name.
			req.EcsClusterName = req.ProjectName + "-" + req.Env
			req.EcsCluster = &ecs.CreateClusterInput{
				ClusterName: aws.String(req.EcsClusterName),
				Tags: []*ecs.Tag{
					&ecs.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
					&ecs.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
				},
			}
			log.Printf("\t\t\tSet ECS Cluster Name to '%s'.", req.EcsClusterName)

			// Set default AWS ECS Service Name.
			req.EcsServiceName = req.ServiceName + "-" + req.Env
			log.Printf("\t\t\tSet ECS Service Name to '%s'.", req.EcsServiceName)

			// Set default AWS ECS Execution Role Name.
			req.EcsExecutionRoleName = fmt.Sprintf("ecsExecutionRole%s%s", req.ProjectNameCamel(), strcase.ToCamel(req.Env))
			req.EcsExecutionRole = &iam.CreateRoleInput{
				RoleName:                 aws.String(req.EcsExecutionRoleName),
				Description:              aws.String(fmt.Sprintf("Provides access to other AWS service resources that are required to run Amazon ECS tasks for %s. ", req.ProjectName)),
				AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ecs-tasks.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}"),
				Tags: []*iam.Tag{
					&iam.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
					&iam.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
				},
			}
			req.EcsExecutionRolePolicyArns = []string{
				"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
			}
			log.Printf("\t\t\tSet ECS Execution Role Name to '%s'.", req.EcsExecutionRoleName)

			// Set default AWS ECS Task Role Name.
			req.EcsTaskRoleName = fmt.Sprintf("ecsTaskRole%s%s", req.ProjectNameCamel(), strcase.ToCamel(req.Env))
			req.EcsTaskRole = &iam.CreateRoleInput{
				RoleName:                 aws.String(req.EcsTaskRoleName),
				Description:              aws.String(fmt.Sprintf("Allows ECS tasks for %s to call AWS services on your behalf.", req.ProjectName)),
				AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ecs-tasks.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}"),
				Tags: []*iam.Tag{
					&iam.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
					&iam.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
				},
			}
			log.Printf("\t\t\tSet ECS Task Role Name to '%s'.", req.EcsTaskRoleName)

			// Set default AWS ECS Task Policy Name.
			req.EcsTaskPolicyName = fmt.Sprintf("%s%sServices", req.ProjectNameCamel(), strcase.ToCamel(req.Env))
			req.EcsTaskPolicy = &iam.CreatePolicyInput{
				PolicyName:  aws.String(req.EcsTaskPolicyName),
				Description: aws.String(fmt.Sprintf("Defines access for %s services. ", req.ProjectName)),
			}
			log.Printf("\t\t\tSet ECS Task Policy Name to '%s'.", req.EcsTaskPolicyName)

			// 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:ListBucket",
							"s3:HeadBucket",
							"cloudfront:ListDistributions",
							"ec2:DescribeNetworkInterfaces",
							"ec2:DeleteNetworkInterface",
							"ecs:ListTasks",
							"ecs:DescribeServices",
							"ecs:DescribeTasks",
							"ec2:DescribeNetworkInterfaces",
							"route53:ListHostedZones",
							"route53:ListResourceRecordSets",
							"route53:ChangeResourceRecordSets",
							"ecs:UpdateService",
							"ses:SendEmail",
							"ses:ListIdentities",
							"ses:GetAccountSendingEnabled",
							"secretsmanager:ListSecretVersionIds",
							"secretsmanager:GetSecretValue",
							"secretsmanager:CreateSecret",
							"secretsmanager:UpdateSecret",
							"secretsmanager:RestoreSecret",
							"secretsmanager:DeleteSecret",
						},
						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: "*",
					},
				},
			}

			if req.S3BucketPublicName != "" || req.S3BucketPrivateName != "" {
				var bpr []string
				if req.S3BucketPublicName != "" {
					bpr = append(bpr, "arn:aws:s3:::"+req.S3BucketPublicName)
					bpr = append(bpr, "arn:aws:s3:::"+req.S3BucketPublicName+"/*")
				}
				if req.S3BucketPrivateName != "" {
					bpr = append(bpr, "arn:aws:s3:::"+req.S3BucketPrivateName)
					bpr = append(bpr, "arn:aws:s3:::"+req.S3BucketPrivateName+"/*")
				}

				bp := IamStatementEntry{
					Sid:    "S3BucketAccess",
					Effect: "Allow",
					Action: []string{
						"s3:ListObjects",
						"s3:PutObject",
						"s3:PutObjectAcl",
						"s3:GetObject",
						"s3:HeadObject",
					},
					Resource: bpr,
				}

				req.EcsTaskPolicyDocument.Statement = append(req.EcsTaskPolicyDocument.Statement, bp)
			}

			// Set default Cloudwatch Log Group Name.
			req.CloudWatchLogGroupName = fmt.Sprintf("logs/env_%s/aws/ecs/cluster_%s/service_%s", req.Env, req.EcsClusterName, req.ServiceName)
			req.CloudWatchLogGroup = &cloudwatchlogs.CreateLogGroupInput{
				LogGroupName: aws.String(req.CloudWatchLogGroupName),
				Tags: map[string]*string{
					awsTagNameProject: aws.String(req.ProjectName),
					awsTagNameEnv:     aws.String(req.Env),
				},
			}
			log.Printf("\t\t\tSet CloudWatch Log Group Name to '%s'.", req.CloudWatchLogGroupName)

			// Set default EC2 Security Group Name.
			req.Ec2SecurityGroupName = req.EcsClusterName
			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),

				// 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)

			// Set the name of the EC2 Security Group used by the gitlab runner. This is used to ensure the security
			// group defined above has access to the RDS cluster/instance and can thus handle schema migrations.
			req.GitlabRunnerEc2SecurityGroupName = "gitlab-runner"

			// Set default ELB Load Balancer Name when ELB is enabled.
			if req.EnableEcsElb {
				if !strings.Contains(req.EcsClusterName, req.Env) && !strings.Contains(req.ServiceName, req.Env) {
					// When a custom cluster name is provided and/or service name, ensure the ELB contains the current env.
					req.ElbLoadBalancerName = fmt.Sprintf("%s-%s-%s", req.EcsClusterName, req.ServiceName, req.Env)
				} else {
					// Default value when when custom cluster/service name is supplied.
					req.ElbLoadBalancerName = fmt.Sprintf("%s-%s", req.EcsClusterName, req.ServiceName)
				}

				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).
					IpAddressType: aws.String("ipv4"),
					// 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.
					Scheme: aws.String("internet-facing"),
					// The type of load balancer.
					Type: aws.String("application"),
					// One or more tags to assign to the load balancer.
					Tags: []*elbv2.Tag{
						&elbv2.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
						&elbv2.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
					},
				}
				log.Printf("\t\t\tSet ELB Name to '%s'.", req.ElbLoadBalancerName)

				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
					Name: aws.String(req.ElbTargetGroupName),

					// The port on which the targets receive traffic. This port is used unless you
					// specify a port override when registering the target. If the target is a Lambda
					// function, this parameter does not apply.
					Port: aws.Int64(80),

					// The protocol to use for routing traffic to the targets. For Application Load
					// Balancers, the supported protocols are HTTP and HTTPS. For Network Load Balancers,
					// the supported protocols are TCP, TLS, UDP, or TCP_UDP. A TCP_UDP listener
					// must be associated with a TCP_UDP target group. If the target is a Lambda
					// function, this parameter does not apply.
					Protocol: aws.String("HTTP"),

					// Indicates whether health checks are enabled. If the target type is lambda,
					// health checks are disabled by default but can be enabled. If the target type
					// is instance or ip, health checks are always enabled and cannot be disabled.
					HealthCheckEnabled: aws.Bool(true),

					// The approximate amount of time, in seconds, between health checks of an individual
					// target. For HTTP and HTTPS health checks, the range is 5–300 seconds. For
					// TCP health checks, the supported values are 10 and 30 seconds. If the target
					// type is instance or ip, the default is 30 seconds. If the target type is
					// lambda, the default is 35 seconds.
					HealthCheckIntervalSeconds: aws.Int64(30),

					// [HTTP/HTTPS health checks] The ping path that is the destination on the targets
					// for health checks. The default is /.
					HealthCheckPath: aws.String("/ping"),

					// The protocol the load balancer uses when performing health checks on targets.
					// For Application Load Balancers, the default is HTTP. For Network Load Balancers,
					// the default is TCP. The TCP protocol is supported for health checks only
					// if the protocol of the target group is TCP, TLS, UDP, or TCP_UDP. The TLS,
					// UDP, and TCP_UDP protocols are not supported for health checks.
					HealthCheckProtocol: aws.String("HTTP"),

					// The amount of time, in seconds, during which no response from a target means
					// a failed health check. For target groups with a protocol of HTTP or HTTPS,
					// the default is 5 seconds. For target groups with a protocol of TCP or TLS,
					// this value must be 6 seconds for HTTP health checks and 10 seconds for TCP
					// and HTTPS health checks. If the target type is lambda, the default is 30
					// seconds.
					HealthCheckTimeoutSeconds: aws.Int64(5),

					// The number of consecutive health checks successes required before considering
					// an unhealthy target healthy. For target groups with a protocol of HTTP or
					// HTTPS, the default is 5. For target groups with a protocol of TCP or TLS,
					// the default is 3. If the target type is lambda, the default is 5.
					HealthyThresholdCount: aws.Int64(3),

					// The number of consecutive health check failures required before considering
					// a target unhealthy. For target groups with a protocol of HTTP or HTTPS, the
					// default is 2. For target groups with a protocol of TCP or TLS, this value
					// must be the same as the healthy threshold count. If the target type is lambda,
					// the default is 2.
					UnhealthyThresholdCount: aws.Int64(3),

					// [HTTP/HTTPS health checks] The HTTP codes to use when checking for a successful
					// response from a target.
					Matcher: &elbv2.Matcher{
						HttpCode: aws.String("200"),
					},

					// The type of target that you must specify when registering targets with this
					// target group. You can't specify targets for a target group using more than
					// one target type.
					//
					//    * instance - Targets are specified by instance ID. This is the default
					//    value. If the target group protocol is UDP or TCP_UDP, the target type
					//    must be instance.
					//
					//    * ip - Targets are specified by IP address. You can specify IP addresses
					//    from the subnets of the virtual private cloud (VPC) for the target group,
					//    the RFC 1918 range (10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16), and
					//    the RFC 6598 range (100.64.0.0/10). You can't specify publicly routable
					//    IP addresses.
					//
					//    * lambda - The target groups contains a single Lambda function.
					TargetType: aws.String("ip"),
				}
				log.Printf("\t\t\tSet ELB Target Group Name to '%s'.", req.ElbTargetGroupName)
			}

			// Set ECS configs based on specified env.
			if flags.Env == "prod" {
				req.EcsServiceMinimumHealthyPercent = aws.Int64(100)
				req.EcsServiceMaximumPercent = aws.Int64(200)

				req.ElbDeregistrationDelay = aws.Int(300)
			} else {
				req.EcsServiceMinimumHealthyPercent = aws.Int64(100)
				req.EcsServiceMaximumPercent = aws.Int64(200)

				// force staging to deploy immediately without waiting for connections to drain
				req.ElbDeregistrationDelay = aws.Int(0)
			}
			if req.EcsServiceDesiredCount == 0 {
				req.EcsServiceDesiredCount = 1
			}

			req.EscServiceHealthCheckGracePeriodSeconds = aws.Int64(60)

			// Service Discovery Namespace settings.
			req.SDNamepsace = &servicediscovery.CreatePrivateDnsNamespaceInput{
				Name:        aws.String(req.EcsClusterName),
				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.
				CreatorRequestId: aws.String("devops-deploy"),
			}

			// Service Discovery Service settings.
			req.SDService = &servicediscovery.CreateServiceInput{
				Name:        aws.String(req.EcsServiceName),
				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.
				CreatorRequestId: aws.String("devops-deploy"),
			}

			// Elastic Cache settings for a Redis cache cluster. Could defined different settings by env.
			req.CacheCluster = &elasticache.CreateCacheClusterInput{
				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),
			}

			// 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{
					ParameterName:  aws.String("maxmemory-policy"),
					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.
			req.DBInstance = &rds.CreateDBInstanceInput{
				DBInstanceIdentifier:      aws.String(dBInstanceIdentifier(req.ProjectName, req.Env)),
				DBName:                    aws.String("shared"),
				Engine:                    aws.String("postgres"),
				MasterUsername:            aws.String("god"),
				MasterUserPassword:        aws.String(uuid.NewRandom().String()),
				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),
				EnablePerformanceInsights: aws.Bool(false),
				AutoMinorVersionUpgrade:   aws.Bool(true),
				CopyTagsToSnapshot:        aws.Bool(true),
				Tags: []*rds.Tag{
					{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
					{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
				},
			}

			log.Printf("\t%s\tDefaults set.", tests.Success)
		}

		r, err := regexp.Compile(`^(\d+)`)
		if err != nil {
			return nil, errors.WithStack(err)
		}

		// Workaround for domains that start with a numeric value like 8north.com
		// Validation fails with error: failed on the 'fqdn' tag
		origServiceHostPrimary := req.ServiceHostPrimary
		matches := r.FindAllString(req.ServiceHostPrimary, -1)
		if len(matches) > 0 {
			for _, m := range matches {
				req.ServiceHostPrimary = strings.Replace(req.ServiceHostPrimary, m, "X", -1)
			}
		}

		log.Println("\tValidate request.")
		errs := validator.New().Struct(req)
		if errs != nil {
			return nil, errs
		}

		// Reset the primary domain after validation is completed.
		req.ServiceHostPrimary = origServiceHostPrimary

		log.Printf("\t%s\tNew request generated.", tests.Success)
	}

	return &req, nil
}

// Run is the main entrypoint for deploying a service for a given target environment.
func ServiceDeploy(log *log.Logger, ctx context.Context, req *serviceDeployRequest) error {

	startTime := time.Now()

	// Load the AWS ECR repository. Try to find by name else create new one.
	{
		log.Println("ECR - Get or create repository.")

		svc := ecr.New(req.awsSession())

		// First try to find ECR repository by name.
		var awsRepo *ecr.Repository
		descRes, err := svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{
			RepositoryNames: []*string{aws.String(req.EcrRepositoryName)},
		})
		if err != nil {
			// The repository should have been created by build or manually created and should exist at this point.
			return errors.Wrapf(err, "Failed to describe repository '%s'.", req.EcrRepositoryName)
		} else if len(descRes.Repositories) > 0 {
			awsRepo = descRes.Repositories[0]
		}
		log.Printf("\t\tFound: %s.", *awsRepo.RepositoryArn)

		req.ReleaseImage = releaseImage(req.Env, req.ServiceName, *awsRepo.RepositoryUri)
		if err != nil {
			return err
		}

		log.Printf("\t\trelease image: %s", req.ReleaseImage)
		log.Printf("\t%s\tRelease image valid.", tests.Success)
	}

	// 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.
	var datadogApiKey string
	{
		log.Println("Datadog - Get API Key")

		// Load Datadog API key which can be either stored in an environment variable or in AWS Secrets Manager.
		// 1. Check env vars for [DEV|STAGE|PROD]_DD_API_KEY and DD_API_KEY
		datadogApiKey = getTargetEnv(req.Env, "DD_API_KEY")

		// 2. Check AWS Secrets Manager for datadog entry prefixed with target environment.
		if datadogApiKey == "" {
			prefixedSecretId := secretID(req.ProjectName, req.Env, "datadog")
			var err error
			datadogApiKey, err = GetAwsSecretValue(req.AwsCreds, prefixedSecretId)
			if err != nil {
				if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
					return err
				}
			}
		}

		// 3. Check AWS Secrets Manager for Datadog entry.
		if datadogApiKey == "" {
			secretId := "DATADOG"
			var err error
			datadogApiKey, err = GetAwsSecretValue(req.AwsCreds, secretId)
			if err != nil {
				if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
					return err
				}
			}
		}

		if datadogApiKey != "" {
			log.Printf("\t%s\tAPI Key set.\n", tests.Success)
		} else {
			log.Printf("\t%s\tAPI Key NOT set.\n", tests.Failed)
		}
	}

	// Helper function to tag ECS resources.
	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}),
			Tags:      ec2Tags,
		})
		if err != nil {
			return errors.Wrapf(err, "failed to create tags for %s", resource)
		}

		return nil
	}
	_ = ec2TagResource

	// Try to find the AWS Cloudwatch Log Group by name or create new one.
	{
		log.Println("CloudWatch Logs - Get or Create Log Group")

		svc := cloudwatchlogs.New(req.awsSession())

		// If no log group was found, create one.
		var err error
		_, err = svc.CreateLogGroup(req.CloudWatchLogGroup)
		if err != nil {
			if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != cloudwatchlogs.ErrCodeResourceAlreadyExistsException {
				return errors.Wrapf(err, "failed to create log group '%s'", req.CloudWatchLogGroupName)
			}

			log.Printf("\t\tFound: %s.", req.CloudWatchLogGroupName)
		} else {
			log.Printf("\t\tCreated: %s.", req.CloudWatchLogGroupName)
		}

		log.Printf("\t%s\tUsing Log Group '%s'.\n", tests.Success, req.CloudWatchLogGroupName)
	}

	// Try to find the AWS S3 Buckets by names or create new ones.
	{
		log.Println("S3 - Setup Buckets")

		svc := s3.New(req.awsSession())

		// 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.
		log.Println("\tGet or Create S3 Buckets")
		for _, bucket := range req.S3Buckets {
			_, err := svc.CreateBucket(bucket.Input)
			if err != nil {
				if aerr, ok := err.(awserr.Error); !ok || (aerr.Code() != s3.ErrCodeBucketAlreadyExists && aerr.Code() != s3.ErrCodeBucketAlreadyOwnedByYou) {
					return errors.Wrapf(err, "failed to create s3 bucket '%s'", bucket.Name)
				}

				// If bucket found during create, returns it.
				log.Printf("\t\tFound: %s.", bucket.Name)
			} else {

				// If no bucket found during create, create new one.
				log.Printf("\t\tCreated: %s.", bucket.Name)
			}
		}

		// 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.
		log.Println("\tWait for S3 Buckets to exist")
		for _, bucket := range req.S3Buckets {
			log.Printf("\t\t%s", bucket.Name)

			err := svc.WaitUntilBucketExists(&s3.HeadBucketInput{
				Bucket: aws.String(bucket.Name),
			})
			if err != nil {
				return errors.Wrapf(err, "Failed to wait for s3 bucket '%s' to exist", bucket.Name)
			}
			log.Printf("\t\t\tExists")
		}

		// Loop through each S3 bucket and configure policies.
		log.Println("\tConfiguring each S3 Bucket")
		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,
					},
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to configure lifecycle rule for s3 bucket '%s'", bucket.Name)
				}

				for _, r := range bucket.LifecycleRules {
					log.Printf("\t\t\tAdded lifecycle '%s'", *r.ID)
				}
			}

			// Add all the defined CORS rules for the bucket.
			if len(bucket.CORSRules) > 0 {
				_, err := svc.PutBucketCors(&s3.PutBucketCorsInput{
					Bucket: aws.String(bucket.Name),
					CORSConfiguration: &s3.CORSConfiguration{
						CORSRules: bucket.CORSRules,
					},
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to put CORS on s3 bucket '%s'", bucket.Name)
				}
				log.Printf("\t\t\tUpdated CORS")
			}

			// 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,
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to block public access for s3 bucket '%s'", bucket.Name)
				}
				log.Printf("\t\t\tBlocked public access")
			}

			// 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),
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to put bucket policy for s3 bucket '%s'", bucket.Name)
				}
				log.Printf("\t\t\tUpdated bucket policy")
			}
		}
		log.Printf("\t%s\tS3 buckets configured successfully.\n", tests.Success)
	}

	if req.CloudfrontPublic != nil {
		log.Println("Cloudfront - Setup Distribution")

		svc := cloudfront.New(req.awsSession())

		_, err := svc.CreateDistribution(&cloudfront.CreateDistributionInput{
			DistributionConfig: req.CloudfrontPublic,
		})
		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)
		}
	}

	// Find the default VPC and associated subnets.
	// Custom subnets outside of the default VPC are not currently supported.
	var projectSubnetsIDs []string
	var projectVpcId string
	{
		log.Println("EC2 - Find Subnets")

		svc := ec2.New(req.awsSession())

		log.Println("\t\tFind all subnets are that default for each availability zone.")

		// Find all subnets that are default for each availability zone.
		var subnets []*ec2.Subnet
		err := svc.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{}, func(res *ec2.DescribeSubnetsOutput, lastPage bool) bool {
			for _, s := range res.Subnets {
				if *s.DefaultForAz {
					subnets = append(subnets, s)
				}
			}
			return !lastPage
		})
		if err != nil {
			return errors.Wrap(err, "Failed to find default subnets")
		}

		// 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.
		if len(subnets) == 0 {
			return errors.New("Failed to find any subnets, expected at least 1")
		}

		// Iterate through subnets and make sure they belong to the same VPC as the project.
		for _, s := range subnets {
			if s.VpcId == nil {
				continue
			}
			if projectVpcId == "" {
				projectVpcId = *s.VpcId
			} else if projectVpcId != *s.VpcId {
				return errors.Errorf("Invalid subnet %s, all subnets should belong to the same VPC, expected %s, got %s", *s.SubnetId, projectVpcId, *s.VpcId)
			}

			projectSubnetsIDs = append(projectSubnetsIDs, *s.SubnetId)
			log.Printf("\t\t\t%s", *s.SubnetId)
		}

		log.Printf("\t\tFound %d subnets.\n", len(subnets))
	}

	// Try to find the AWS Security Group by name or create a new one.
	var securityGroupId string
	{
		log.Println("EC2 - Find Security Group")

		svc := ec2.New(req.awsSession())

		log.Printf("\t\tFind security group '%s'.\n", req.Ec2SecurityGroupName)

		// Link the ID of the VPC.
		req.Ec2SecurityGroup.VpcId = aws.String(projectVpcId)

		// Find all the security groups and then parse the group name to get the Id of the security group.
		var runnerSgId string
		err := svc.DescribeSecurityGroupsPages(&ec2.DescribeSecurityGroupsInput{
			GroupNames: aws.StringSlice([]string{req.Ec2SecurityGroupName, req.GitlabRunnerEc2SecurityGroupName}),
		}, func(res *ec2.DescribeSecurityGroupsOutput, lastPage bool) bool {
			for _, s := range res.SecurityGroups {
				if *s.GroupName == req.Ec2SecurityGroupName {
					securityGroupId = *s.GroupId
				} else if *s.GroupName == req.GitlabRunnerEc2SecurityGroupName {
					runnerSgId = *s.GroupId
				}
			}
			return !lastPage
		})
		if err != nil {
			if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != "InvalidGroup.NotFound" {
				return errors.Wrapf(err, "Failed to find security group '%s'", req.Ec2SecurityGroupName)
			}
		}

		if securityGroupId == "" {
			// If no security group was found, create one.
			createRes, err := svc.CreateSecurityGroup(req.Ec2SecurityGroup)
			if err != nil {
				return errors.Wrapf(err, "Failed to create security group '%s'", req.Ec2SecurityGroupName)
			}
			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),
			},
		}

		// When not using an Elastic Load Balancer, services need to support direct access via HTTPS.
		// HTTPS is terminated via the web server and not on the Load Balancer.
		if req.EnableHTTPS {
			// 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),
			})
		}

		// 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),
			})
		}

		// 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" {
					return errors.Wrapf(err, "Failed to add ingress for security group '%s'", req.Ec2SecurityGroupName)
				}
			}
		}

		log.Printf("\t%s\tUsing Security Group '%s'.\n", tests.Success, req.Ec2SecurityGroupName)
	}

	// 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.
	var dbCluster *rds.DBCluster
	if req.DBCluster != nil {
		log.Println("RDS - Get or Create Database Cluster")

		svc := rds.New(req.awsSession())

		// Try to find a RDS database cluster using cluster identifier.
		descRes, err := svc.DescribeDBClusters(&rds.DescribeDBClustersInput{
			DBClusterIdentifier: req.DBCluster.DBClusterIdentifier,
		})
		if err != nil {
			if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != rds.ErrCodeDBClusterNotFoundFault {
				return errors.Wrapf(err, "Failed to describe database cluster '%s'", *req.DBCluster.DBClusterIdentifier)
			}
		} 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 {
				return errors.Wrapf(err, "Failed to create cluster '%s'", *req.DBCluster.DBClusterIdentifier)
			}
			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)
	}

	// 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.
	var db *DB
	if req.DBInstance != nil {
		log.Println("RDS - Get or Create Database Instance")

		// Secret ID used to store the DB username and password across deploys.
		dbSecretId := secretID(req.ProjectName, req.Env, *req.DBInstance.DBInstanceIdentifier)

		// 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 {
					return errors.Wrapf(err, "Failed to get value for secret id %s", dbSecretId)
				}
			} else {
				err = json.Unmarshal([]byte(*res.SecretString), &db)
				if err != nil {
					return errors.Wrap(err, "Failed to json decode db credentials")
				}
			}
		}

		// Init a new RDS client.
		svc := rds.New(req.awsSession())

		// Always set the VPC Security Group ID dynamically with the one created/found previously.
		req.DBInstance.VpcSecurityGroupIds = aws.StringSlice([]string{securityGroupId})

		// When a DB cluster exists, add the identifier to the instance input. This is for creating databases with
		// the storage engine of AWS Aurora.
		if dbCluster != nil {
			req.DBInstance.DBClusterIdentifier = dbCluster.DBClusterIdentifier
		} else {
			req.DBInstance.DBClusterIdentifier = nil
		}

		// Try to find an existing DB instance with the same identifier.
		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 {
				return errors.Wrapf(err, "Failed to describe database instance '%s'", *req.DBInstance.DBInstanceIdentifier)
			}
		} else if len(descRes.DBInstances) > 0 {
			dbInstance = descRes.DBInstances[0]
		}

		// No DB instance was found, so create a new one.
		if dbInstance == nil {
			if db == nil {
				// 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())
					}
				}

				// Only set the password right now,
				// all other configuration details will be set after the database instance is created.
				db = &DB{
					Pass: *req.DBInstance.MasterUserPassword,
				}

				// Store the secret first in the event that create fails.
				{
					// Json encode the db details to be stored as secret text.
					dat, err := json.Marshal(db)
					if err != nil {
						return errors.Wrap(err, "Failed to marshal db credentials")
					}

					// Create the new entry in AWS Secret Manager with the database password.
					sm := secretsmanager.New(req.awsSession())
					_, err = sm.CreateSecret(&secretsmanager.CreateSecretInput{
						Name:         aws.String(dbSecretId),
						SecretString: aws.String(string(dat)),
					})
					if err != nil {
						/*
							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")
							}*/
						return errors.Wrap(err, "Failed to create new secret with db credentials")
					}
					log.Printf("\t\tStored Secret\n")
				}
			} else {
				req.DBInstance.MasterUserPassword = aws.String(db.Pass)
			}

			// If no cluster was found, create one.
			createRes, err := svc.CreateDBInstance(req.DBInstance)
			if err != nil {
				return errors.Wrapf(err, "Failed to create instance '%s'", *req.DBInstance.DBInstanceIdentifier)
			}
			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)

		// If the instance is not active because it was recently created, wait for it to become active.
		if *dbInstance.DBInstanceStatus != "available" {
			log.Printf("\t\tWait for instance to become available.")
			err = svc.WaitUntilDBInstanceAvailable(&rds.DescribeDBInstancesInput{
				DBInstanceIdentifier: dbInstance.DBInstanceIdentifier,
			})
			if err != nil {
				return errors.Wrapf(err, "Failed to wait for database instance '%s' to enter available state", *req.DBInstance.DBInstanceIdentifier)
			}
		}

		// 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
		// match the current instance endpoint.
		curHost := fmt.Sprintf("%s:%d", *dbInstance.Endpoint.Address, *dbInstance.Endpoint.Port)
		if curHost != db.Host {

			// Copy the instance details to the DB struct.
			db.Host = curHost
			db.User = *dbInstance.MasterUsername
			db.Database = *dbInstance.DBName
			db.Driver = *dbInstance.Engine
			db.DisableTLS = false

			// Json encode the DB details to be stored as text via AWS Secrets Manager.
			dat, err := json.Marshal(db)
			if err != nil {
				return errors.Wrap(err, "Failed to marshal db credentials")
			}

			// Update the current AWS Secret.
			sm := secretsmanager.New(req.awsSession())
			_, 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")
			}
			log.Printf("\t\tUpdate Secret\n")

			// Ensure the newly created database is seeded.
			log.Printf("\t\tOpen database connection")
			// Register informs the sqlxtrace package of the driver that we will be using in our program.
			// It uses a default service name, in the below case "postgres.db". To use a custom service
			// name use RegisterWithServiceName.
			sqltrace.Register(db.Driver, &pq.Driver{}, sqltrace.WithServiceName("devops:migrate"))
			masterDb, err := sqlxtrace.Open(db.Driver, db.URL())
			if err != nil {
				return errors.WithStack(err)
			}
			defer masterDb.Close()

			// Start the database migrations.
			log.Printf("\t\tStart migrations.")
			if err = schema.Migrate(ctx, masterDb, log, false); err != nil {
				return errors.WithStack(err)
			}
			log.Printf("\t\tFinished migrations.")
		}

		log.Printf("\t%s\tUsing DB Instance '%s'.\n", tests.Success, *dbInstance.DBInstanceIdentifier)
	}

	// Setup AWS Elastic Cache cluster for Redis.
	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())

		// 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]
		}

		if cacheCluster == nil {
			// If no repository was found, create one.
			createRes, err := svc.CreateCacheCluster(req.CacheCluster)
			if err != nil {
				return errors.Wrapf(err, "failed to create cluster '%s'", *req.CacheCluster.CacheClusterId)
			}
			cacheCluster = createRes.CacheCluster

			/*
				// TODO: Tag cache cluster, ARN for the cache cluster when it is not readily available.
				_, 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)

		// If the cache cluster is not active because it was recently created, wait for it to become active.
		if *cacheCluster.CacheClusterStatus != "available" {
			log.Printf("\t\tWhat for cluster to become available.")
			err = svc.WaitUntilCacheClusterAvailable(&elasticache.DescribeCacheClustersInput{
				CacheClusterId: req.CacheCluster.CacheClusterId,
			})
			if err != nil {
				return errors.Wrapf(err, "Failed to wait for cache cluster '%s' to enter available state", req.CacheCluster.CacheClusterId)
			}
		}

		// 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)

			// 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 {
						return errors.Wrapf(err, "Failed to describe cache parameter group '%s'", *req.CacheCluster.CacheClusterId)
					}
				}

				log.Printf("\t\tCreated custom Cache Parameter Group : %s", customCacheParameterGroupName)
				_, err = svc.CreateCacheParameterGroup(&elasticache.CreateCacheParameterGroupInput{
					CacheParameterGroupFamily: descRes.CacheParameterGroups[0].CacheParameterGroupFamily,
					CacheParameterGroupName:   aws.String(customCacheParameterGroupName),
					Description:               aws.String(fmt.Sprintf("Customized default parameter group for %s %s", *cacheCluster.Engine, *cacheCluster.EngineVersion)),
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to cache parameter group '%s'", customCacheParameterGroupName)
				}

				log.Printf("\t\tSet Cache Parameter Group : %s", customCacheParameterGroupName)
				updateRes, err := svc.ModifyCacheCluster(&elasticache.ModifyCacheClusterInput{
					CacheClusterId:          cacheCluster.CacheClusterId,
					CacheParameterGroupName: aws.String(customCacheParameterGroupName),
				})
				if err != nil {
					return errors.Wrapf(err, "Failed modify cache parameter group '%s' for cache cluster '%s'", customCacheParameterGroupName, *cacheCluster.CacheClusterId)
				}
				cacheCluster = updateRes.CacheCluster
			}

			// 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,
					ParameterNameValues:     req.CacheClusterParameter,
				})
				if err != nil {
					return errors.Wrapf(err, "failed to modify cache parameter group '%s'", *cacheCluster.CacheParameterGroup.CacheParameterGroupName)
				}

				for _, p := range req.CacheClusterParameter {
					log.Printf("\t\t\tSet '%s' to '%s'", *p.ParameterName, *p.ParameterValue)
				}
			}
		}

		// 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]
			}
		}

		log.Printf("\t%s\tDone setting up Cache Cluster '%s' successfully.\n", tests.Success, *cacheCluster.CacheClusterId)
	}

	// Route 53 zone lookup when hostname is set. Supports both top level domains or sub domains.
	var zoneArecNames = map[string][]string{}
	if req.ServiceHostPrimary != "" {
		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 {
			return errors.Wrap(err, "Failed list route 53 hosted zones")
		}

		// Generate a slice with the primary domain name and include all the alternative domain names.
		lookupDomains := []string{}
		if req.ServiceHostPrimary != "" {
			lookupDomains = append(lookupDomains, req.ServiceHostPrimary)
		}
		for _, dn := range req.ServiceHostNames {
			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 {
					// Found a matching zone or have to search all possibilities!
					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
					CallerReference: aws.String("devops-deploy"),
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to create route 53 hosted zone '%s' for domain '%s'", zoneName, dn)
				}
				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 {
				return nil, errors.Wrap(err, "Failed to list namespaces")
			}

			return found, nil
		}

		sdNamespace, err := listNamespaces()
		if err != nil {
			return err
		}

		if sdNamespace == nil {
			// Link the namespace to the VPC.
			req.SDNamepsace.Vpc = aws.String(projectVpcId)

			log.Println("\t\tCreate private namespace.")

			// If no namespace was found, create one.
			createRes, err := svc.CreatePrivateDnsNamespace(req.SDNamepsace)
			if err != nil {
				return errors.Wrapf(err, "Failed to create namespace '%s'", *req.SDNamepsace.Name)
			}
			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" {
					err = errors.Errorf("Operation failed")
					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 {
				return errors.Wrapf(err, "Failed to get operation for namespace '%s'", *req.SDNamepsace.Name)
			}

			// 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)
	}

	// 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 {

		// If HTTPS enabled on ELB, then need to find ARN certificates first.
		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 {
						if *cert.DomainName == req.ServiceHostPrimary {
							certificateArn = *cert.CertificateArn
							return false
						}
					}
					return !lastPage
				})
			if err != nil {
				return errors.Wrapf(err, "failed to list certificates for '%s'", req.ServiceHostPrimary)
			}

			if certificateArn == "" {
				// Create hash of all the domain names to be used to mark unique requests.
				idempotencyToken := req.ServiceHostPrimary + "|" + strings.Join(req.ServiceHostNames, "|")
				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
					DomainName: aws.String(req.ServiceHostPrimary),

					// 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).
					SubjectAlternativeNames: aws.StringSlice(req.ServiceHostNames),

					// 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 {
					return errors.Wrapf(err, "failed to create certificate '%s'", req.ServiceHostPrimary)
				}
				certificateArn = *createRes.CertificateArn

				log.Printf("\t\tCreated certificate '%s'", req.ServiceHostPrimary)
			} else {
				log.Printf("\t\tFound certificate '%s'", req.ServiceHostPrimary)
			}

			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)
						}
					}
				}
			}

			log.Printf("\t%s\tUsing ACM Certicate '%s'.\n", tests.Success, certificateArn)
		}

		log.Println("EC2 - Find Elastic Load Balance")
		{
			svc := elbv2.New(req.awsSession())

			// Try to find load balancer given a name.
			err := svc.DescribeLoadBalancersPages(&elbv2.DescribeLoadBalancersInput{
				Names: []*string{aws.String(req.ElbLoadBalancerName)},
			}, func(res *elbv2.DescribeLoadBalancersOutput, lastPage bool) bool {
				// Loop through the results to find the match ELB.
				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 {
					return errors.Wrapf(err, "Failed to describe load balancer '%s'", req.ElbLoadBalancerName)
				}
			}

			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 {
					return errors.Wrapf(err, "Failed to create load balancer '%s'", req.ElbLoadBalancerName)
				}
				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 {
					return errors.Wrapf(err, "Failed to find listeners for load balancer '%s'", req.ElbLoadBalancerName)
				}
				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)

			var targetGroup *elbv2.TargetGroup
			err = svc.DescribeTargetGroupsPages(&elbv2.DescribeTargetGroupsInput{
				LoadBalancerArn: elb.LoadBalancerArn,
			}, func(res *elbv2.DescribeTargetGroupsOutput, lastPage bool) bool {
				for _, tg := range res.TargetGroups {
					if *tg.TargetGroupName == req.ElbTargetGroupName {
						targetGroup = tg
						return false
					}
				}
				return !lastPage
			})
			if err != nil {
				if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != elbv2.ErrCodeTargetGroupNotFoundException {
					return errors.Wrapf(err, "Failed to describe target group '%s'", req.ElbTargetGroupName)
				}
			}

			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)

				// If no target group was found, create one.
				createRes, err := svc.CreateTargetGroup(req.ElbTargetGroup)
				if err != nil {
					return errors.Wrapf(err, "Failed to create target group '%s'", req.ElbTargetGroupName)
				}
				targetGroup = createRes.TargetGroups[0]

				log.Printf("\t\tAdded target group: %s.", *targetGroup.TargetGroupArn)
			} else {
				log.Printf("\t\tHas target group: %s.", *targetGroup.TargetGroupArn)
			}

			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"),

							// The value of the attribute.
							Value: aws.String(strconv.Itoa(*req.ElbDeregistrationDelay)),
						},
					},
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to modify target group '%s' attributes", req.ElbTargetGroupName)
				}

				log.Printf("\t\t\tSet sttributes.")
			}

			listenerPorts := map[string]int64{
				"HTTP": 80,
			}
			if req.EnableHTTPS {
				listenerPorts["HTTPS"] = 443
			}

			for listenerProtocol, listenerPort := range listenerPorts {

				var foundListener bool
				for _, cl := range curListeners {
					if *cl.Port == listenerPort {
						foundListener = true
						break
					}
				}

				if !foundListener {
					listenerInput := &elbv2.CreateListenerInput{
						// The actions for the default rule. The rule must include one forward action
						// or one or more fixed-response actions.
						//
						// If the action type is forward, you specify a target group. The protocol of
						// the target group must be HTTP or HTTPS for an Application Load Balancer.
						// The protocol of the target group must be TCP, TLS, UDP, or TCP_UDP for a
						// Network Load Balancer.
						//
						// DefaultActions is a required field
						DefaultActions: []*elbv2.Action{
							&elbv2.Action{
								// The type of action. Each rule must include exactly one of the following types
								// of actions: forward, fixed-response, or redirect.
								//
								// Type is a required field
								Type: aws.String("forward"),

								// The Amazon Resource Name (ARN) of the target group. Specify only when Type
								// is forward.
								TargetGroupArn: targetGroup.TargetGroupArn,
							},
						},

						// The Amazon Resource Name (ARN) of the load balancer.
						//
						// LoadBalancerArn is a required field
						LoadBalancerArn: elb.LoadBalancerArn,

						// The port on which the load balancer is listening.
						//
						// Port is a required field
						Port: aws.Int64(listenerPort),

						// The protocol for connections from clients to the load balancer. For Application
						// Load Balancers, the supported protocols are HTTP and HTTPS. For Network Load
						// Balancers, the supported protocols are TCP, TLS, UDP, and TCP_UDP.
						//
						// Protocol is a required field
						Protocol: aws.String(listenerProtocol),
					}

					if listenerProtocol == "HTTPS" {
						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 {
						return errors.Wrapf(err, "Failed to create listener '%s'", req.ElbLoadBalancerName)
					}

					log.Printf("\t\t\tAdded Listener: %s.", *createRes.Listeners[0].ListenerArn)
				}
			}

			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,
			})

			{
				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{
								Name: aws.String(aName),
								Type: aws.String("A"),
								AliasTarget: &route53.AliasTarget{
									HostedZoneId:         elb.CanonicalHostedZoneId,
									DNSName:              elb.DNSName,
									EvaluateTargetHealth: aws.Bool(true),
								},
							},
						})
					}

					log.Printf("\tUpdated '%s'.\n", zoneId)
					_, err := svc.ChangeResourceRecordSets(input)
					if err != nil {
						return errors.Wrapf(err, "Failed to update A records for zone '%s'", zoneId)
					}
				}
			}

			log.Printf("\t%s\tUsing ELB '%s'.\n", tests.Success, *elb.LoadBalancerName)
		}
	}

	// Try to find AWS ECS Cluster by name or create new one.
	var ecsCluster *ecs.Cluster
	{
		log.Println("ECS - Get or Create Cluster")

		svc := ecs.New(req.awsSession())

		descRes, err := svc.DescribeClusters(&ecs.DescribeClustersInput{
			Clusters: []*string{aws.String(req.EcsClusterName)},
		})
		if err != nil {
			if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != ecs.ErrCodeClusterNotFoundException {
				return errors.Wrapf(err, "Failed to describe cluster '%s'", req.EcsClusterName)
			}
		} else if len(descRes.Clusters) > 0 {
			ecsCluster = descRes.Clusters[0]
		}

		if ecsCluster == nil {
			// If no repository was found, create one.
			createRes, err := svc.CreateCluster(req.EcsCluster)
			if err != nil {
				return errors.Wrapf(err, "Failed to create cluster '%s'", req.EcsClusterName)
			}
			ecsCluster = createRes.Cluster

			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)
		}

		// 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)

		log.Printf("\t%s\tUsing ECS Cluster '%s'.\n", tests.Success, *ecsCluster.ClusterName)
	}

	// Register a new ECS task.
	var taskDef *ecs.TaskDefinition
	{
		log.Println("ECS - Register task definition")

		// List of placeholders that can be used in task definition and replaced on deployment.
		placeholders := map[string]string{
			"{SERVICE}":               req.ServiceName,
			"{RELEASE_IMAGE}":         req.ReleaseImage,
			"{ECS_CLUSTER}":           req.EcsClusterName,
			"{ECS_SERVICE}":           req.EcsServiceName,
			"{AWS_REGION}":            req.AwsCreds.Region,
			"{AWS_LOGS_GROUP}":        req.CloudWatchLogGroupName,
			"{AWS_S3_BUCKET_PRIVATE}": req.S3BucketPrivateName,
			"{AWS_S3_BUCKET_PUBLIC}":  req.S3BucketPublicName,
			"{ENV}":                   req.Env,
			"{DATADOG_APIKEY}":        datadogApiKey,
			"{DATADOG_ESSENTIAL}":     "true",
			"{HTTP_HOST}":             "0.0.0.0:80",
			"{HTTPS_HOST}":            "", // Not enabled by default
			"{HTTPS_ENABLED}":         "false",

			"{APP_PROJECT}":  req.ProjectName,
			"{APP_BASE_URL}": "", // Not set by default, requires a hostname to be defined.
			"{HOST_PRIMARY}": req.ServiceHostPrimary,
			"{HOST_NAMES}":   strings.Join(req.ServiceHostNames, ","),

			"{STATIC_FILES_S3_ENABLED}":         "false",
			"{STATIC_FILES_S3_PREFIX}":          req.StaticFilesS3Prefix,
			"{STATIC_FILES_CLOUDFRONT_ENABLED}": "false",
			"{STATIC_FILES_IMG_RESIZE_ENABLED}": "false",

			"{CACHE_HOST}": "", // Not enabled by default

			"{DB_HOST}":        "",
			"{DB_USER}":        "",
			"{DB_PASS}":        "",
			"{DB_DATABASE}":    "",
			"{DB_DRIVER}":      "",
			"{DB_DISABLE_TLS}": "",

			"{ROUTE53_ZONES}":           "",
			"{ROUTE53_UPDATE_TASK_IPS}": "false",

			// Directly map GitLab CICD env variables set during deploy.
			"{CI_COMMIT_REF_NAME}":     os.Getenv("CI_COMMIT_REF_NAME"),
			"{CI_COMMIT_REF_SLUG}":     os.Getenv("CI_COMMIT_REF_SLUG"),
			"{CI_COMMIT_SHA}":          os.Getenv("CI_COMMIT_SHA"),
			"{CI_COMMIT_TAG}":          os.Getenv("CI_COMMIT_TAG"),
			"{CI_COMMIT_TITLE}":        jsonEncodeStringValue(os.Getenv("CI_COMMIT_TITLE")),
			"{CI_COMMIT_DESCRIPTION}":  jsonEncodeStringValue(os.Getenv("CI_COMMIT_DESCRIPTION")),
			"{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"),
			"{CI_COMMIT_PIPELINE_URL}": os.Getenv("CI_COMMIT_PIPELINE_URL"),
		}

		// When the datadog API key is empty, don't force the container to be essential have have the whole task fail.
		if datadogApiKey == "" {
			placeholders["{DATADOG_ESSENTIAL}"] = "false"
		}

		// For HTTPS support.
		if req.EnableHTTPS {
			placeholders["{HTTPS_ENABLED}"] = "true"

			// When there is no Elastic Load Balancer, we need to terminate HTTPS on the app.
			if !req.EnableEcsElb {
				placeholders["{HTTPS_HOST}"] = "0.0.0.0:443"
			}
		}

		// When a domain name if defined for the service, set the App Base URL. Default to HTTPS if enabled.
		if req.ServiceHostPrimary != "" {
			var appSchema string
			if req.EnableHTTPS {
				appSchema = "https"
			} else {
				appSchema = "http"
			}

			placeholders["{APP_BASE_URL}"] = fmt.Sprintf("%s://%s/", appSchema, req.ServiceHostPrimary)
		}

		// Static files served from S3.
		if req.StaticFilesS3Enable {
			placeholders["{STATIC_FILES_S3_ENABLED}"] = "true"
		}

		// Static files served from CloudFront.
		if req.CloudfrontPublic != nil {
			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"
		}

		// 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.
		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
		}

		// 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"
			}
		}

		// 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)
		}

		// Read the defined json task definition.
		dat, err := EcsReadTaskDefinition(req.ServiceDir, req.Env)
		if err != nil {
			return err
		}
		jsonStr := string(dat)

		// Replace placeholders used in the JSON task definition.
		{
			// Generate new regular expression for finding placeholders.
			expr := "(" + strings.Join(pks, "|") + ")"
			r, err := regexp.Compile(expr)
			if err != nil {
				return err
			}

			matches := r.FindAllString(jsonStr, -1)

			if len(matches) > 0 {
				log.Println("\t\tUpdating placeholders.")

				replaced := make(map[string]bool)
				for _, m := range matches {
					if replaced[m] {
						continue
					}
					replaced[m] = true

					newVal := placeholders[m]
					log.Printf("\t\t\t%s -> %s", m, newVal)
					jsonStr = strings.Replace(jsonStr, m, newVal, -1)
				}
			}
		}

		// Replace placeholders defined in task def but not here from env vars.
		{
			r, err := regexp.Compile(`{\b(\w*)\b}`)
			if err != nil {
				return err
			}

			matches := r.FindAllString(jsonStr, -1)
			if len(matches) > 0 {
				log.Println("\t\tSearching for placeholders in env variables.")

				replaced := make(map[string]bool)
				for _, m := range matches {
					if replaced[m] {
						continue
					}
					replaced[m] = true

					envKey := strings.Trim(m, "{}")
					newVal := os.Getenv(envKey)
					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)
		if err != nil {
			return err
		}

		// If a task definition value is empty, populate it with the default value.
		if taskDefInput.Family == nil || *taskDefInput.Family == "" {
			taskDefInput.Family = &req.ServiceName
		}
		if len(taskDefInput.ContainerDefinitions) > 0 {
			if taskDefInput.ContainerDefinitions[0].Name == nil || *taskDefInput.ContainerDefinitions[0].Name == "" {
				taskDefInput.ContainerDefinitions[0].Name = &req.EcsServiceName
			}
			if taskDefInput.ContainerDefinitions[0].Image == nil || *taskDefInput.ContainerDefinitions[0].Image == "" {
				taskDefInput.ContainerDefinitions[0].Image = &req.ReleaseImage
			}
		}

		log.Printf("\t\t\tFamily: %s", *taskDefInput.Family)
		log.Printf("\t\t\tExecutionRoleArn: %s", *taskDefInput.ExecutionRoleArn)

		if taskDefInput.TaskRoleArn != nil {
			log.Printf("\t\t\tTaskRoleArn: %s", *taskDefInput.TaskRoleArn)
		}
		if taskDefInput.NetworkMode != nil {
			log.Printf("\t\t\tNetworkMode: %s", *taskDefInput.NetworkMode)
		}
		log.Printf("\t\t\tTaskDefinitions: %d", len(taskDefInput.ContainerDefinitions))

		// If memory or cpu for the task is not set, need to compute from container definitions.
		if (taskDefInput.Cpu == nil || *taskDefInput.Cpu == "") || (taskDefInput.Memory == nil || *taskDefInput.Memory == "") {
			log.Println("\t\tCompute CPU and Memory for task definition.")

			var (
				totalMemory int64
				totalCpu    int64
			)
			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)

			// 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

			var (
				selectedMemory int64
				selectedCpu    int64
			)
			if totalMemory < 8192 {
				if totalMemory > 7168 {
					selectedMemory = 8192

					if totalCpu >= 2048 {
						selectedCpu = 4096
					} else if totalCpu >= 1024 {
						selectedCpu = 2048
					} else {
						selectedCpu = 1024
					}
				} else if totalMemory > 6144 {
					selectedMemory = 7168

					if totalCpu >= 2048 {
						selectedCpu = 4096
					} else if totalCpu >= 1024 {
						selectedCpu = 2048
					} else {
						selectedCpu = 1024
					}
				} else if totalMemory > 5120 || totalCpu >= 1024 {
					selectedMemory = 6144

					if totalCpu >= 2048 {
						selectedCpu = 4096
					} else if totalCpu >= 1024 {
						selectedCpu = 2048
					} else {
						selectedCpu = 1024
					}
				} else if totalMemory > 4096 {
					selectedMemory = 5120

					if totalCpu >= 512 {
						selectedCpu = 1024
					} else {
						selectedCpu = 512
					}
				} else if totalMemory > 3072 {
					selectedMemory = 4096

					if totalCpu >= 512 {
						selectedCpu = 1024
					} else {
						selectedCpu = 512
					}
				} else if totalMemory > 2048 || totalCpu >= 512 {
					selectedMemory = 3072

					if totalCpu >= 512 {
						selectedCpu = 1024
					} else {
						selectedCpu = 512
					}
				} else if totalMemory > 1024 || totalCpu >= 256 {
					selectedMemory = 2048

					if totalCpu >= 256 {
						if totalCpu >= 512 {
							selectedCpu = 1024
						} else {
							selectedCpu = 512
						}
					} else {
						selectedCpu = 256
					}
				} else if totalMemory > 512 {
					selectedMemory = 1024

					if totalCpu >= 256 {
						selectedCpu = 512
					} else {
						selectedCpu = 256
					}
				} else {
					selectedMemory = 512
					selectedCpu = 256
				}
			}
			log.Printf("\t\t\tSelected memory %d and cpu %d", selectedMemory, selectedCpu)
			taskDefInput.Memory = aws.String(strconv.Itoa(int(selectedMemory)))
			taskDefInput.Cpu = aws.String(strconv.Itoa(int(selectedCpu)))
		}

		log.Printf("\t%s\tLoaded task definition complete.\n", tests.Success)

		// The execution role is the IAM role that executes ECS actions such as pulling the image and storing the
		// application logs in cloudwatch.
		if taskDefInput.ExecutionRoleArn == nil || *taskDefInput.ExecutionRoleArn == "" {

			svc := iam.New(req.awsSession())

			// 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 {
						return errors.Wrapf(err, "Failed to find task role '%s'", req.EcsExecutionRoleName)
					}
				}

				if res.Role != nil {
					taskDefInput.ExecutionRoleArn = res.Role.Arn
					log.Printf("\t\t\tFound role '%s'", *taskDefInput.ExecutionRoleArn)
				} else {
					// If no repository was found, create one.
					res, err := svc.CreateRole(req.EcsExecutionRole)
					if err != nil {
						return errors.Wrapf(err, "Failed to create task role '%s'", req.EcsExecutionRoleName)
					}
					taskDefInput.ExecutionRoleArn = res.Role.Arn
					log.Printf("\t\t\tCreated role '%s'", *taskDefInput.ExecutionRoleArn)
				}

				for _, policyArn := range req.EcsExecutionRolePolicyArns {
					_, err = svc.AttachRolePolicy(&iam.AttachRolePolicyInput{
						PolicyArn: aws.String(policyArn),
						RoleName:  aws.String(req.EcsExecutionRoleName),
					})
					if err != nil {
						return errors.Wrapf(err, "Failed to attach policy '%s' to task role '%s'", policyArn, req.EcsExecutionRoleName)
					}
					log.Printf("\t\t\t\tAttached Policy '%s'", policyArn)
				}

				log.Printf("\t%s\tExecutionRoleArn updated.\n", tests.Success)
			}
		}

		// The task role is the IAM role used by the task itself to access other AWS Services. To access services
		// like S3, SQS, etc then those permissions would need to be covered by the TaskRole.
		if taskDefInput.TaskRoleArn == nil || *taskDefInput.TaskRoleArn == "" {
			svc := iam.New(req.awsSession())

			// Find or create the default service policy.
			var policyArn string
			{
				log.Printf("\tFind default service policy %s.", req.EcsTaskPolicyName)

				var policyVersionId string
				err = svc.ListPoliciesPages(&iam.ListPoliciesInput{}, func(res *iam.ListPoliciesOutput, lastPage bool) bool {
					for _, p := range res.Policies {
						if *p.PolicyName == req.EcsTaskPolicyName {
							policyArn = *p.Arn
							policyVersionId = *p.DefaultVersionId
							return false
						}
					}

					return !lastPage
				})
				if err != nil {
					return errors.Wrap(err, "Failed to list IAM policies")
				}

				if policyArn != "" {
					log.Printf("\t\t\tFound policy '%s' versionId '%s'", policyArn, policyVersionId)

					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 {
							return errors.Wrapf(err, "Failed to read policy '%s' version '%s'", req.EcsTaskPolicyName, policyVersionId)
						}
					}

					// 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 {
						return errors.Wrapf(err, "Failed to url unescape policy document - %s", string(*res.PolicyVersion.Document))
					}

					// 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 {
						return errors.Wrapf(err, "Failed to json decode policy document - %s", string(curJson))
					}

					var updateDoc bool
					for _, baseStmt := range req.EcsTaskPolicyDocument.Statement {
						var found bool
						for curIdx, curStmt := range curDoc.Statement {
							if baseStmt.Sid != curStmt.Sid {
								continue
							}

							found = true

							for _, baseAction := range baseStmt.Action {
								var hasAction bool
								for _, curAction := range curStmt.Action {
									if baseAction == curAction {
										hasAction = true
										break
									}
								}

								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
								}
							}
						}

						if !found {
							log.Printf("\t\t\t\tAdded new statement '%s'", baseStmt.Sid)
							curDoc.Statement = append(curDoc.Statement, baseStmt)
							updateDoc = true
						}
					}

					if updateDoc {
						dat, err := json.Marshal(curDoc)
						if err != nil {
							return errors.Wrap(err, "Failed to json encode policy document")
						}

						_, err = svc.CreatePolicyVersion(&iam.CreatePolicyVersionInput{
							PolicyArn:      aws.String(policyArn),
							PolicyDocument: aws.String(string(dat)),
							SetAsDefault:   aws.Bool(true),
						})
						if err != nil {
							if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
								return errors.Wrapf(err, "Failed to read policy '%s' version '%s'", req.EcsTaskPolicyName, policyVersionId)
							}
						}
					}
				} else {
					dat, err := json.Marshal(req.EcsTaskPolicyDocument)
					if err != nil {
						return errors.Wrap(err, "Failed to json encode policy document")
					}
					req.EcsTaskPolicy.PolicyDocument = aws.String(string(dat))

					// If no repository was found, create one.
					res, err := svc.CreatePolicy(req.EcsTaskPolicy)
					if err != nil {
						return errors.Wrapf(err, "Failed to create task policy '%s'", req.EcsTaskPolicyName)
					}

					policyArn = *res.Policy.Arn

					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 {
						return errors.Wrapf(err, "Failed to find task role '%s'", req.EcsTaskRoleName)
					}
				}

				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.
					res, err := svc.CreateRole(req.EcsTaskRole)
					if err != nil {
						return errors.Wrapf(err, "Failed to create task role '%s'", req.EcsTaskRoleName)
					}
					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)
					//}
				}

				_, err = svc.AttachRolePolicy(&iam.AttachRolePolicyInput{
					PolicyArn: aws.String(policyArn),
					RoleName:  aws.String(req.EcsTaskRoleName),
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to attach policy '%s' to task role '%s'", policyArn, req.EcsTaskRoleName)
				}

				log.Printf("\t%s\tTaskRoleArn updated.\n", tests.Success)
			}
		}

		log.Println("\tRegister new task definition.")
		{
			svc := ecs.New(req.awsSession())

			// Registers a new task.
			res, err := svc.RegisterTaskDefinition(taskDefInput)
			if err != nil {
				return errors.Wrapf(err, "Failed to register task definition '%s'", *taskDefInput.Family)
			}
			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)

			log.Printf("\t%s\tTask definition registered.\n", tests.Success)
		}
	}

	// 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.
	var ecsService *ecs.Service
	{
		log.Println("ECS - Find Service")

		svc := ecs.New(req.awsSession())

		// Find service by ECS cluster and service name.
		res, err := svc.DescribeServices(&ecs.DescribeServicesInput{
			Cluster:  ecsCluster.ClusterArn,
			Services: []*string{aws.String(req.EcsServiceName)},
		})
		if err != nil {
			if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != ecs.ErrCodeServiceNotFoundException {
				return errors.Wrapf(err, "Failed to describe service '%s'", req.EcsServiceName)
			}
		} else if len(res.Services) > 0 {
			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)
		}
	}

	// 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) {
			// Service was created without ELB and now ELB is enabled.
			recreateService = true
		} else if !req.EnableEcsElb && (ecsService.LoadBalancers != nil && len(ecsService.LoadBalancers) > 0) {
			// Service was created with ELB and now ELB is disabled.
			recreateService = true
		} 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
		}

		// If determined from above that service needs to be recreated.
		if recreateService {

			// Needs to delete any associated services on ECS first before it can be recreated.
			log.Println("ECS - Delete Service")

			svc := ecs.New(req.awsSession())

			// The service cannot be stopped while it is scaled above 0.
			if ecsService.DesiredCount != nil && *ecsService.DesiredCount > 0 {
				log.Println("\t\tScaling service down to zero.")
				_, err := svc.UpdateService(&ecs.UpdateServiceInput{
					Cluster:      ecsService.ClusterArn,
					Service:      ecsService.ServiceArn,
					DesiredCount: aws.Int64(int64(0)),
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to update service '%s'", ecsService.ServiceName)
				}

				// It may take some time for the service to scale down, so need to wait.
				log.Println("\t\tWait for the service to scale down.")
				err = svc.WaitUntilServicesStable(&ecs.DescribeServicesInput{
					Cluster:  ecsCluster.ClusterArn,
					Services: aws.StringSlice([]string{*ecsService.ServiceArn}),
				})
				if err != nil {
					return errors.Wrapf(err, "Failed to wait for service '%s' to enter stable state", *ecsService.ServiceName)
				}
			}

			// Once task count is 0 for the service, then can delete it.
			log.Println("\t\tDelete Service.")
			res, err := svc.DeleteService(&ecs.DeleteServiceInput{
				Cluster: ecsService.ClusterArn,
				Service: ecsService.ServiceArn,

				// If true, allows you to delete a service even if it has not been scaled down
				// to zero tasks. It is only necessary to use this if the service is using the
				// REPLICA scheduling strategy.
				Force: aws.Bool(forceDelete),
			})
			if err != nil {
				return errors.Wrapf(err, "Failed to delete service '%s'", ecsService.ServiceName)
			}
			ecsService = res.Service

			log.Println("\t\tWait for the service to be deleted.")
			err = svc.WaitUntilServicesInactive(&ecs.DescribeServicesInput{
				Cluster:  ecsCluster.ClusterArn,
				Services: aws.StringSlice([]string{*ecsService.ServiceArn}),
			})
			if err != nil {
				return errors.Wrapf(err, "Failed to wait for service '%s' to enter stable state", *ecsService.ServiceName)
			}

			// Manually mark the ECS has inactive since WaitUntilServicesInactive was executed.
			ecsService.Status = aws.String("INACTIVE")

			log.Printf("\t%s\tDelete Service.\n", tests.Success)
		}
	}

	// If the service exists on ECS, update the service, else create a new service.
	if ecsService != nil && *ecsService.Status != "INACTIVE" {
		log.Println("ECS - Update Service")

		svc := ecs.New(req.awsSession())

		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
			}
		}

		updateRes, err := svc.UpdateService(&ecs.UpdateServiceInput{
			Cluster:                       ecsCluster.ClusterName,
			Service:                       ecsService.ServiceName,
			DesiredCount:                  aws.Int64(desiredCount),
			HealthCheckGracePeriodSeconds: ecsService.HealthCheckGracePeriodSeconds,
			TaskDefinition:                taskDef.TaskDefinitionArn,

			// 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 {
			return errors.Wrapf(err, "Failed to update service '%s'", *ecsService.ServiceName)
		}
		ecsService = updateRes.Service

		log.Printf("\t%s\tUpdated ECS Service '%s'.\n", tests.Success, *ecsService.ServiceName)
	} else {

		// If not service exists on ECS, then create it.
		log.Println("ECS - Create Service")
		{

			svc := ecs.New(req.awsSession())

			var assignPublicIp *string
			var healthCheckGracePeriodSeconds *int64
			if len(ecsELBs) == 0 {
				assignPublicIp = aws.String("ENABLED")
			} else {
				assignPublicIp = aws.String("DISABLED")
				healthCheckGracePeriodSeconds = req.EscServiceHealthCheckGracePeriodSeconds
			}

			// When ELB is enabled and get the following error when using the default VPC.
			// 	Status reason 	CannotPullContainerError:
			// 		Error response from daemon:
			// 			Get https://888955683113.dkr.ecr.us-west-2.amazonaws.com/v2/:
			// 				net/http: request canceled while waiting for connection
			// 				(Client.Timeout exceeded while awaiting headers)
			assignPublicIp = aws.String("ENABLED")

			serviceInput := &ecs.CreateServiceInput{
				// The short name or full Amazon Resource Name (ARN) of the cluster that your
				// service is running on. If you do not specify a cluster, the default cluster
				// is assumed.
				Cluster: ecsCluster.ClusterName,

				// The name of your service. Up to 255 letters (uppercase and lowercase), numbers,
				// and hyphens are allowed. Service names must be unique within a cluster, but
				// you can have similarly named services in multiple clusters within a Region
				// or across multiple Regions.
				//
				// ServiceName is a required field
				ServiceName: aws.String(req.EcsServiceName),

				// Optional deployment parameters that control how many tasks run during the
				// deployment and the ordering of stopping and starting tasks.
				DeploymentConfiguration: &ecs.DeploymentConfiguration{
					// Refer to documentation for flags.ecsServiceMaximumPercent
					MaximumPercent: req.EcsServiceMaximumPercent,
					// Refer to documentation for flags.ecsServiceMinimumHealthyPercent
					MinimumHealthyPercent: req.EcsServiceMinimumHealthyPercent,
				},

				// Refer to documentation for flags.ecsServiceDesiredCount.
				DesiredCount: aws.Int64(req.EcsServiceDesiredCount),

				// Specifies whether to enable Amazon ECS managed tags for the tasks within
				// the service. For more information, see Tagging Your Amazon ECS Resources
				// (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-using-tags.html)
				// in the Amazon Elastic Container Service Developer Guide.
				EnableECSManagedTags: aws.Bool(false),

				// The period of time, in seconds, that the Amazon ECS service scheduler should
				// ignore unhealthy Elastic Load Balancing target health checks after a task
				// has first started. This is only valid if your service is configured to use
				// a load balancer. If your service's tasks take a while to start and respond
				// to Elastic Load Balancing health checks, you can specify a health check grace
				// period of up to 2,147,483,647 seconds. During that time, the ECS service
				// scheduler ignores health check status. This grace period can prevent the
				// ECS service scheduler from marking tasks as unhealthy and stopping them before
				// they have time to come up.
				HealthCheckGracePeriodSeconds: healthCheckGracePeriodSeconds,

				// The launch type on which to run your service. For more information, see Amazon
				// ECS Launch Types (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html)
				// in the Amazon Elastic Container Service Developer Guide.
				LaunchType: aws.String("FARGATE"),

				// A load balancer object representing the load balancer to use with your service.
				LoadBalancers: ecsELBs,

				// The network configuration for the service. This parameter is required for
				// task definitions that use the awsvpc network mode to receive their own elastic
				// network interface, and it is not supported for other network modes. For more
				// information, see Task Networking (https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html)
				// in the Amazon Elastic Container Service Developer Guide.
				NetworkConfiguration: &ecs.NetworkConfiguration{
					AwsvpcConfiguration: &ecs.AwsVpcConfiguration{
						// Whether the task's elastic network interface receives a public IP address.
						// The default value is DISABLED.
						AssignPublicIp: assignPublicIp,

						// The security groups associated with the task or service. If you do not specify
						// a security group, the default security group for the VPC is used. There is
						// a limit of 5 security groups that can be specified per AwsVpcConfiguration.
						// All specified security groups must be from the same VPC.
						SecurityGroups: aws.StringSlice([]string{securityGroupId}),

						// 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
						Subnets: aws.StringSlice(projectSubnetsIDs),
					},
				},

				// The family and revision (family:revision) or full ARN of the task definition
				// to run in your service. If a revision is not specified, the latest ACTIVE
				// revision is used. If you modify the task definition with UpdateService, Amazon
				// ECS spawns a task with the new version of the task definition and then stops
				// an old task after the new version is running.
				TaskDefinition: taskDef.TaskDefinitionArn,

				// The metadata that you apply to the service to help you categorize and organize
				// them. Each tag consists of a key and an optional value, both of which you
				// define. When a service is deleted, the tags are deleted as well. Tag keys
				// can have a maximum character length of 128 characters, and tag values can
				// have a maximum length of 256 characters.
				Tags: []*ecs.Tag{
					&ecs.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(req.ProjectName)},
					&ecs.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(req.Env)},
				},
			}

			// Add the Service Discovery registry to the ECS service.
			if sdService != nil {
				if serviceInput.ServiceRegistries == nil {
					serviceInput.ServiceRegistries = []*ecs.ServiceRegistry{}
				}
				serviceInput.ServiceRegistries = append(serviceInput.ServiceRegistries, &ecs.ServiceRegistry{
					RegistryArn: sdService.Arn,
				})
			}

			createRes, err := svc.CreateService(serviceInput)

			// If tags aren't enabled for the account, try the request again without them.
			// https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2/
			if err != nil && strings.Contains(err.Error(), "ARN and resource ID format must be enabled") {
				serviceInput.Tags = nil
				createRes, err = svc.CreateService(serviceInput)
			}

			if err != nil {
				return errors.Wrapf(err, "Failed to create service '%s'", req.EcsServiceName)
			}
			ecsService = createRes.Service

			log.Printf("\t%s\tCreated ECS Service '%s'.\n", tests.Success, *ecsService.ServiceName)
		}
	}

	// When static files are enabled to be to stored on S3, we need to upload all of them.
	if req.StaticFilesS3Enable {
		log.Println("\tUpload static files to public S3 bucket")

		staticDir := filepath.Join(req.ServiceDir, "static")

		if _, err := os.Stat(staticDir); err != nil {
			return errors.Wrapf(err, "Static directory '%s' does not exist.", staticDir)
		}

		err := SyncPublicS3Files(req.awsSession(), req.S3BucketPublicName, req.StaticFilesS3Prefix, staticDir)
		if err != nil {
			return errors.Wrapf(err, "Failed to sync static files from %s to s3://%s/%s", staticDir, req.S3BucketPublicName, req.StaticFilesS3Prefix)
		}

		log.Printf("\t%s\tFiles uploaded to s3://%s/%s.\n", tests.Success, req.S3BucketPublicName, req.StaticFilesS3Prefix)
	}

	// Wait for the updated or created service to enter a stable state.
	{
		log.Println("\tWaiting for service to enter stable state.")

		// 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)

			// Define S3 key prefix used to export the stream logs to.
			s3KeyPrefix := filepath.Join(req.S3BucketTempPrefix, "logs/cloudwatchlogs/exports", req.CloudWatchLogGroupName)

			var downloadPrefix string
			{
				svc := cloudwatchlogs.New(req.awsSession())

				createRes, err := svc.CreateExportTask(&cloudwatchlogs.CreateExportTaskInput{
					LogGroupName:        aws.String(req.CloudWatchLogGroupName),
					LogStreamNamePrefix: aws.String(logStreamName),
					//TaskName: aws.String(taskId),
					Destination:       aws.String(req.S3BucketPrivateName),
					DestinationPrefix: aws.String(s3KeyPrefix),
					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)),
				})
				if err != nil {
					return []string{}, errors.Wrapf(err, "Failed to create export task for from log group '%s' with stream name prefix '%s'", req.CloudWatchLogGroupName, logStreamName)
				}
				exportTaskId := *createRes.TaskId

				for {
					descRes, err := svc.DescribeExportTasks(&cloudwatchlogs.DescribeExportTasksInput{
						TaskId: aws.String(exportTaskId),
					})
					if err != nil {
						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)
					}
					taskStatus := *descRes.ExportTasks[0].Status.Code

					if taskStatus == "COMPLETED" {
						downloadPrefix = filepath.Join(s3KeyPrefix, exportTaskId) + "/"
						break
					} else if taskStatus == "CANCELLED" || taskStatus == "FAILED" {
						break
					}
					time.Sleep(time.Second * 5)
				}
			}

			// If downloadPrefix is set, then get logs from corresponding file for service.
			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 {
					return []string{}, errors.Wrapf(err, "Failed to list objects from s3 bucket '%s' with prefix '%s'", req.S3BucketPrivateName, downloadPrefix)
				}

				// Iterate trough S3 keys and get logs from file.
				for _, s3Key := range s3Keys {
					res, err := svc.GetObject(&s3.GetObjectInput{
						Bucket: aws.String(req.S3BucketPrivateName),
						Key:    aws.String(s3Key),
					})
					if err != nil {
						return []string{}, errors.Wrapf(err, "Failed to get object '%s' from s3 bucket", s3Key, req.S3BucketPrivateName)
					}
					r, _ := gzip.NewReader(res.Body)
					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)
					}

					// Iterate through file by line break and add each line to array of logs.
					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{
				Cluster:       aws.String(req.EcsClusterName),
				ServiceName:   aws.String(req.EcsServiceName),
				DesiredStatus: aws.String("STOPPED"),
			})
			if err != nil {
				return false, errors.Wrapf(err, "Failed to list tasks for cluster '%s' service '%s'", req.EcsClusterName, req.EcsServiceName)
			}

			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 {
				return false, errors.Wrapf(err, "Failed to describe %d tasks for cluster '%s'", len(serviceTaskRes.TaskArns), req.EcsClusterName)
			}

			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)
					} else {
						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 {
						return false, errors.Wrapf(err, "Failed to get logs for task %s for cluster '%s'", *t.TaskArn, req.EcsClusterName)
					}
					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)
					}
				}

				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)
				} else {
					log.Printf("\t%s\tTask %s stopped.\n", tests.Failed, *t.TaskArn)
				}

				// 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 {
				case <-ticker.C:
					stop, err := checkTasks()
					if err != nil {
						log.Printf("\t%s\tFailed to check tasks.\n%+v\n", tests.Failed, err)
					}

					if stop {
						checkErr <- errors.New("All tasks for service are stopped")
						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{
				Cluster:  ecsCluster.ClusterArn,
				Services: aws.StringSlice([]string{*ecsService.ServiceArn}),
			})
			if err != nil {
				checkErr <- errors.Wrapf(err, "Failed to wait for service '%s' to enter stable state", *ecsService.ServiceName)
			} else {
				// All done.
				checkErr <- nil
			}
		}()

		if err := <-checkErr; err != nil {
			log.Printf("\t%s\tFailed to check tasks.\n%+v\n", tests.Failed, err)
			return err
		}

		// 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)
	}

	return nil
}
