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

1685 lines
60 KiB
Go
Raw Normal View History

2019-07-07 12:52:55 -08:00
package devops
import (
2019-07-08 12:21:22 -08:00
"encoding/json"
2019-07-07 12:52:55 -08:00
"fmt"
2019-07-08 19:13:41 -08:00
"gopkg.in/go-playground/validator.v9"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
2019-07-07 12:52:55 -08:00
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
2019-07-08 12:21:22 -08:00
"github.com/aws/aws-sdk-go/aws"
2019-07-07 12:52:55 -08:00
"github.com/aws/aws-sdk-go/aws/awserr"
2019-07-08 19:13:41 -08:00
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2"
2019-07-08 12:21:22 -08:00
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/aws/aws-sdk-go/service/ecs"
2019-07-08 19:13:41 -08:00
"github.com/aws/aws-sdk-go/service/elbv2"
2019-07-08 12:21:22 -08:00
"github.com/aws/aws-sdk-go/service/iam"
2019-07-07 12:52:55 -08:00
"github.com/aws/aws-sdk-go/service/secretsmanager"
2019-07-08 12:21:22 -08:00
"github.com/iancoleman/strcase"
2019-07-07 12:52:55 -08:00
"github.com/pkg/errors"
)
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
var requiredDeployIamPermissions = []string{
"secretsmanager:GetSecretValue",
"ecr:GetAuthorizationToken",
"ecr:ListImages",
"ecr:DescribeRepositories",
"ecr:CreateRepository",
"ecs:CreateCluster",
"ecs:DescribeClusters",
"esc:RegisterTaskDefinition",
"cloudwatchlogs:DescribeLogGroups",
"cloudwatchlogs:CreateLogGroup",
"iam:CreateServiceLinkedRole",
"iam:PutRolePolicy",
2019-07-08 12:21:22 -08:00
}
2019-07-07 12:52:55 -08:00
// requiredCmdsBuild proves a list of required executables for completing build.
var requiredCmdsDeploy = [][]string{
[]string{"docker", "version", "-f", "{{.Client.Version}}"},
}
2019-07-08 19:13:41 -08:00
// NewServiceDeployRequest generated a new request for executing deploy for a given set of flags.
func NewServiceDeployRequest(log *log.Logger, flags *ServiceDeployFlags) (*serviceDeployRequest, error) {
2019-07-07 12:52:55 -08:00
2019-07-08 19:13:41 -08:00
log.Println("Validate flags.")
{
errs := validator.New().Struct(flags)
if errs != nil {
return nil, errs
}
log.Printf("\t%s\tFlags ok.", tests.Success)
}
2019-07-07 12:52:55 -08:00
2019-07-08 19:13:41 -08:00
log.Println("\tVerify AWS credentials.")
var awsCreds *AwsCredentials
{
var err error
awsCreds, err = GetAwsCredentials(flags.Env)
2019-07-07 12:52:55 -08:00
if err != nil {
2019-07-08 19:13:41 -08:00
return nil, err
2019-07-07 12:52:55 -08:00
}
2019-07-08 19:13:41 -08:00
log.Printf("\t\t\tAccessKeyID: %s", awsCreds.AccessKeyID)
log.Printf("\t\t\tRegion: %s", awsCreds.Region)
log.Printf("\t%s\tAWS credentials valid.", tests.Success)
2019-07-07 12:52:55 -08:00
}
2019-07-08 19:13:41 -08:00
log.Println("Generate deploy request.")
var req *serviceDeployRequest
2019-07-08 12:21:22 -08:00
{
2019-07-08 19:13:41 -08:00
req = &serviceDeployRequest{
// Required flags.
serviceName: flags.ServiceName,
env: flags.Env,
awsCreds: awsCreds,
// Optional flags.
projectRoot: flags.ProjectRoot,
projectName : flags.ProjectName,
dockerFile: flags.DockerFile,
enableLambdaVPC: flags.EnableLambdaVPC,
enableEcsElb : flags.EnableEcsElb,
noBuild: flags.NoBuild,
noDeploy: flags.NoDeploy,
noCache: flags.NoCache,
noPush: flags.NoPush,
}
2019-07-08 12:21:22 -08:00
// When project root directory is empty or set to current working path, then search for the project root by locating
// the go.mod file.
2019-07-08 19:13:41 -08:00
log.Println("\tDetermining the project root directory.")
{
if flags.ProjectRoot == "" || flags.ProjectRoot == "." {
log.Println("\tAttempting to location project root directory from current working directory.")
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
var err error
req.goModFile, err = findProjectGoModFile()
if err != nil {
return nil, err
}
req.projectRoot = filepath.Dir(req.goModFile)
} else {
log.Println("\t\tUsing supplied project root directory.")
req.goModFile = filepath.Join(flags.ProjectRoot, "go.mod")
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
log.Printf("\t\t\tproject root: %s", req.projectRoot)
log.Printf("\t\t\tgo.mod: %s", req.goModFile )
2019-07-08 12:21:22 -08:00
}
log.Println("\tExtracting go module name from go.mod.")
2019-07-08 19:13:41 -08:00
{
var err error
req.goModName, err = loadGoModName(req.goModFile)
if err != nil {
return nil, err
}
log.Printf("\t\t\tmodule name: %s", req.goModName)
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
log.Println("\tDetermining the project name.")
2019-07-08 19:13:41 -08:00
{
if flags.ProjectName != "" {
req.projectName = flags.ProjectName
log.Printf("\t\tUse provided value.")
} else {
req.projectName = filepath.Base(req.goModName)
log.Printf("\t\tSet from go module.")
}
log.Printf("\t\t\tproject name: %s", req.projectName)
2019-07-08 12:21:22 -08:00
}
log.Println("\tAttempting to locate service directory from project root directory.")
{
2019-07-08 19:13:41 -08:00
if flags.DockerFile != "" {
req.dockerFile = flags.DockerFile
log.Printf("\t\tUse provided value.")
} else {
log.Printf("\t\tFind from project root looking for Dockerfile.")
var err error
req.dockerFile, err = findServiceDockerFile(req.projectRoot, req.serviceName)
if err != nil {
return nil, err
}
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
req.serviceDir = filepath.Dir(flags.DockerFile)
log.Printf("\t\t\tservice directory: %s", req.serviceDir)
log.Printf("\t\t\tdockerfile: %s", req.dockerFile)
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
2019-07-08 12:21:22 -08:00
log.Println("\tSet defaults not defined in env vars.")
{
2019-07-08 19:13:41 -08:00
// Set default AWS ECR Repository Name.
req.ecrRepositoryName = req.projectName
log.Printf("\t\t\tSet ECR Repository Name to '%s'.", req.ecrRepositoryName)
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
// Set default AWS ECR Regsistry Max Images.
req.ecrRepositoryMaxImages = defaultAwsRegistryMaxImages
log.Printf("\t\t\tSet ECR Regsistry Max Images to '%d'.", req.ecrRepositoryMaxImages)
// Set default AWS ECS Cluster Name.
req.ecsClusterName = req.projectName + "-" + 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)
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
// Set default Cloudwatch Log Group Name.
req.cloudWatchLogGroupName = fmt.Sprintf("logs/env_%s/aws/ecs/cluster_%s/service_%s", req.env, req.ecsClusterName, req.serviceName)
log.Printf("\t\t\tSet CloudWatch Log Group Name to '%s'.", req.cloudWatchLogGroupName)
// Set default EC2 Security Group Name.
req.ec2SecurityGroupName = req.ecsClusterName
log.Printf("\t\t\tSet ECS Security Group Name to '%s'.", req.ec2SecurityGroupName)
// 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)
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
req.ecsServiceDesiredCount = 1
req.escServiceHealthCheckGracePeriodSeconds = aws.Int64(60)
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tDefaults set.", tests.Success)
}
2019-07-08 19:13:41 -08:00
log.Println("\tValidate request.")
errs := validator.New().Struct(req)
if errs != nil {
return nil, errs
}
log.Printf("\t%s\tNew request generated.", tests.Success)
}
return req, nil
}
// Run is the main entrypoint for deploying a service for a given target env.
func ServiceDeploy(log *log.Logger, req *serviceDeployRequest) error {
//
log.Println("Verify required commands are installed.")
for _, cmdVals := range requiredCmdsDeploy {
cmd := exec.Command(cmdVals[0], cmdVals[1:]...)
cmd.Env = os.Environ()
out, err := cmd.CombinedOutput()
if err != nil {
return errors.WithMessagef(err, "failed to execute %s - %s\n%s", strings.Join(cmdVals, " "), string(out))
}
log.Printf("\t%s\t%s - %s", tests.Success, cmdVals[0], string(out))
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
// Pull the current env variables to be passed in for command execution.
envVars := EnvVars(os.Environ())
// Load the ECR repository.
log.Println("ECR - Get or create repository.")
var awsRepo *ecr.Repository
{
svc := ecr.New(awsCreds.Session())
descRes, err := svc.DescribeRepositories(&ecr.DescribeRepositoriesInput{
RepositoryNames: []*string{aws.String(awsCreds.EcrRepositoryName)},
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != ecr.ErrCodeRepositoryNotFoundException {
return errors.Wrapf(err, "failed to describe repository '%s'", awsCreds.EcrRepositoryName)
}
} else if len(descRes.Repositories) > 0 {
awsRepo = descRes.Repositories[0]
2019-07-08 19:13:41 -08:00
}
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
if awsRepo == nil {
// If no repository was found, create one.
createRes, err := svc.CreateRepository(&ecr.CreateRepositoryInput{
RepositoryName: aws.String(awsCreds.EcrRepositoryName),
Tags: []*ecr.Tag{
&ecr.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(flags.ProjectName)},
&ecr.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(flags.Env)},
},
})
if err != nil {
return errors.Wrapf(err, "failed to create repository '%s'", awsCreds.EcrRepositoryName)
}
awsRepo = createRes.Repository
log.Printf("\t\tCreated: %s.", *awsRepo.RepositoryArn)
} else {
2019-07-08 12:21:22 -08:00
log.Printf("\t\tFound: %s.", *awsRepo.RepositoryArn)
log.Println("\t\tChecking old ECR images.")
delIds, err := EcrPurgeImages(awsCreds)
if err != nil {
return err
}
// If there are image IDs to delete, delete them.
if len(delIds) > 0 {
log.Printf("\t\tDeleted %d images that exceeded limit of %d", len(delIds), awsCreds.EcrRepositoryMaxImages)
for _, imgId := range delIds {
log.Printf("\t\t\t%s", *imgId.ImageTag)
}
}
}
if len(flags.BuildTags) > 0 {
if flags.ReleaseImage == "" {
flags.ReleaseImage = *awsRepo.RepositoryUri + ":" + flags.BuildTags[0]
}
} else if flags.ReleaseImage == "" {
tag1 := flags.Env + "-" + flags.ServiceName
flags.BuildTags = append(flags.BuildTags, tag1)
if v := os.Getenv("CI_COMMIT_REF_NAME"); v != "" {
tag2 := tag1 + "-" + v
flags.BuildTags = append(flags.BuildTags, tag2)
flags.ReleaseImage = *awsRepo.RepositoryUri+":"+tag2
} else {
flags.ReleaseImage = *awsRepo.RepositoryUri+":"+tag1
}
}
log.Printf("\t\trelease image: %s", flags.ReleaseImage)
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
log.Printf("\t\ttags: %s", strings.Join(flags.BuildTags, " "))
log.Printf("\t%s\tRelease image valid.", tests.Success)
log.Println("ECR - Retrieve authorization token used for docker login.")
dockerLogin, err := GetEcrLogin(awsCreds)
if err != nil {
return err
}
log.Println("\t\texecute docker login")
_, err = execCmds(flags.ProjectRoot, &envVars, dockerLogin)
if err != nil {
return err
}
log.Printf("\t%s\tDocker login complete.", tests.Success)
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
// Do the docker build.
if flags.NoBuild == false {
cmdVals := []string{
"docker",
"build",
"--file=" + flags.DockerFile,
"--build-arg", "service=" + flags.ServiceName,
"--build-arg", "env=" + flags.Env,
2019-07-08 19:13:41 -08:00
"-t", flags.ReleaseImage,
2019-07-08 12:21:22 -08:00
}
// Append the build tags.
2019-07-08 19:13:41 -08:00
var builtImageTags []string
for _, t := range flags.BuildTags {
imageTag := flags.ReleaseImage+":"+t
if imageTag == flags.ReleaseImage {
// skip duplicate image tags
continue
}
2019-07-08 12:21:22 -08:00
cmdVals = append(cmdVals, "-t")
2019-07-08 19:13:41 -08:00
cmdVals = append(cmdVals, imageTag)
builtImageTags = append(builtImageTags, imageTag)
2019-07-08 12:21:22 -08:00
}
if flags.NoCache == true {
cmdVals = append(cmdVals, "--no-cache")
}
cmdVals = append(cmdVals, ".")
log.Printf("starting docker build: \n\t\t%s", strings.Join(cmdVals, " "))
out, err := execCmds(flags.ProjectRoot, &envVars, cmdVals)
if err != nil {
return err
}
// Push the newly built docker container to the registry.
if flags.NoPush == false {
2019-07-08 19:13:41 -08:00
log.Printf("\t\tpush release image %s", flags.ReleaseImage)
_, err = execCmds(flags.ProjectRoot, &envVars, []string{"docker", "push", flags.ReleaseImage})
2019-07-08 12:21:22 -08:00
if err != nil {
return err
}
// Push all the build tags.
2019-07-08 19:13:41 -08:00
for _, t := range builtImageTags {
2019-07-08 12:21:22 -08:00
log.Printf("\t\tpush tag %s", t)
2019-07-08 19:13:41 -08:00
_, err = execCmds(flags.ProjectRoot, &envVars, []string{"docker", "push", t})
2019-07-08 12:21:22 -08:00
if err != nil {
return err
}
}
}
log.Printf("\t%s\tbuild complete.\n", tests.Success)
if flags.Debug {
log.Println(string(out[0]))
}
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
// Exit and don't continue if skip deploy.
if flags.NoDeploy == true {
return nil
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
log.Println("Datadog - Get API Key")
var datadogApiKey string
{
// Load Datadog API Key which can be either stored in an env var or in AWS Secrets Manager.
// 1. Check env vars for [DEV|STAGE|PROD]_DD_API_KEY and DD_API_KEY
datadogApiKey = getTargetEnv(flags.Env, "DD_API_KEY")
// 2. Check AWS Secrets Manager for datadog entry prefixed with target env.
if datadogApiKey == "" {
prefixedSecretId := strings.ToUpper(flags.Env) + "/DATADOG"
var err error
datadogApiKey, err = GetAwsSecretValue(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"
datadogApiKey, err = GetAwsSecretValue(awsCreds, secretId)
if err != nil {
if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
return err
}
}
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
if datadogApiKey != "" {
log.Printf("\t%s\tAPI Key set.\n", tests.Success)
2019-07-07 12:52:55 -08:00
} else {
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tAPI Key NOT set.\n", tests.Failed)
2019-07-07 12:52:55 -08:00
}
}
2019-07-08 12:21:22 -08:00
log.Println("CloudWatch Logs - Get or Create Log Group")
{
2019-07-08 19:13:41 -08:00
svc := cloudwatchlogs.New(awsCreds.Session())
/*var logGroup *cloudwatchlogs.LogGroup
err := svc.DescribeLogGroupsPages(&cloudwatchlogs.DescribeLogGroupsInput{
LogGroupNamePrefix: aws.String(awsCreds.CloudWatchLogGroupName),
}, func(res *cloudwatchlogs.DescribeLogGroupsOutput, lastPage bool) bool{
for _, lg := range res.LogGroups {
if *lg.LogGroupName == awsCreds.CloudWatchLogGroupName {
logGroup = lg
return false
}
}
return !lastPage
})
if err != nil {
return errors.Wrapf(err, "failed to describe log groups for prefix '%s'", awsCreds.CloudWatchLogGroupName)
}*/
// If no log group was found, create one.
_, err = svc.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{
LogGroupName: aws.String(awsCreds.CloudWatchLogGroupName),
Tags: map[string]*string{
awsTagNameProject: aws.String(flags.ProjectName),
awsTagNameEnv: aws.String(flags.Env),
},
})
2019-07-08 12:21:22 -08:00
if err != nil {
2019-07-08 19:13:41 -08:00
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != cloudwatchlogs.ErrCodeResourceAlreadyExistsException {
return errors.Wrapf(err, "failed to create log group '%s'", awsCreds.CloudWatchLogGroupName)
}
2019-07-08 12:21:22 -08:00
log.Printf("\t\tFound: %s.", awsCreds.CloudWatchLogGroupName)
2019-07-08 19:13:41 -08:00
} else {
log.Printf("\t\tCreated: %s.", awsCreds.CloudWatchLogGroupName)
2019-07-08 12:21:22 -08:00
}
log.Printf("\t%s\tUsing Log Group '%s'.\n", tests.Success, awsCreds.CloudWatchLogGroupName)
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
log.Println("ECS - Get or Create Cluster")
var ecsCluster *ecs.Cluster
{
2019-07-08 19:13:41 -08:00
svc := ecs.New(awsCreds.Session())
descRes, err := svc.DescribeClusters(&ecs.DescribeClustersInput{
Clusters: []*string{aws.String(awsCreds.EcsClusterName)},
})
2019-07-08 12:21:22 -08:00
if err != nil {
2019-07-08 19:13:41 -08:00
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != ecs.ErrCodeClusterNotFoundException {
return errors.Wrapf(err, "failed to describe cluster '%s'", awsCreds.EcsClusterName)
}
} else if len( descRes.Clusters) > 0 {
ecsCluster = descRes.Clusters[0]
2019-07-08 12:21:22 -08:00
}
2019-07-07 12:52:55 -08:00
2019-07-08 19:13:41 -08:00
if ecsCluster == nil {
// If no repository was found, create one.
createRes, err := svc.CreateCluster(&ecs.CreateClusterInput{
ClusterName: aws.String(awsCreds.EcsClusterName),
Tags: []*ecs.Tag{
&ecs.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(flags.ProjectName)},
&ecs.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(flags.Env)},
},
})
if err != nil {
return errors.Wrapf(err, "failed to create cluster '%s'", awsCreds.EcsClusterName)
}
ecsCluster = createRes.Cluster
2019-07-08 12:21:22 -08:00
log.Printf("\t\tCreated: %s.", *ecsCluster.ClusterArn)
} else {
log.Printf("\t\tFound: %s.", *ecsCluster.ClusterArn)
// The number of services that are running on the cluster in an ACTIVE state.
// You can view these services with ListServices.
log.Printf("\t\t\tActiveServicesCount: %d.", *ecsCluster.ActiveServicesCount)
// The number of tasks in the cluster that are in the PENDING state.
log.Printf("\t\t\tPendingTasksCount: %d.", *ecsCluster.PendingTasksCount)
// The number of container instances registered into the cluster. This includes
// container instances in both ACTIVE and DRAINING status.
log.Printf("\t\t\tRegisteredContainerInstancesCount: %d.", *ecsCluster.RegisteredContainerInstancesCount)
// The number of tasks in the cluster that are in the RUNNING state.
log.Printf("\t\t\tRunningTasksCount: %d.", *ecsCluster.RunningTasksCount)
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// The status of the cluster. The valid values are ACTIVE or INACTIVE. ACTIVE
// indicates that you can register container instances with the cluster and
// the associated instances can accept tasks.
log.Printf("\t\t\tStatus: %s.", *ecsCluster.Status)
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tUsing ECS Cluster '%s'.\n", tests.Success, *ecsCluster.ClusterName)
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
log.Println("ECS - Register task definition")
var taskDef *ecs.TaskDefinition
{
// List of placeholders that can be used in task definition and replaced on deployment.
placeholders := map[string]string{
"{SERVICE}": flags.ServiceName,
2019-07-08 19:13:41 -08:00
"{RELEASE_IMAGE}": flags.ReleaseImage,
2019-07-08 12:21:22 -08:00
"{ECS_CLUSTER}": awsCreds.EcsClusterName,
"{ECS_SERVICE}": awsCreds.EcsServiceName,
2019-07-08 19:13:41 -08:00
"{AWS_REGION}": awsCreds.Region,
2019-07-08 12:21:22 -08:00
"{AWSLOGS_GROUP}": awsCreds.CloudWatchLogGroupName,
"{ENV}": flags.Env,
"{DATADOG_APIKEY}": datadogApiKey,
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// Loop through all the placeholders and create a list of keys to search json.
var pks []string
for k, _ := range placeholders {
pks = append(pks, k)
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// Generate new regular expression for finding placeholders.
expr := "(" + strings.Join(pks, "|") + ")"
r, err := regexp.Compile(expr)
if err != nil {
return err
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// Read the defined json task definition.
dat, err := EcsReadTaskDefinition(flags.ServiceDir, flags.Env)
if err != nil {
return err
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
// Replace placeholders used in the JSON task definition.
{
jsonStr := string(dat)
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
matches := r.FindAllString(jsonStr, -1)
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
if len(matches) > 0 {
log.Println("\t\tUpdating placeholders.")
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
replaced := make(map[string]bool)
for _, m := range matches {
if replaced[m] {
continue
}
replaced[m] = true
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
newVal := placeholders[m]
log.Printf("\t\t\t%s -> %s", m, newVal)
jsonStr = strings.Replace(jsonStr, m, newVal, -1)
}
}
dat = []byte(jsonStr)
}
//if flags.Debug {
// log.Println(string(dat))
//}
log.Println("\t\tParse JSON to task definition.")
taskDefInput, err := parseTaskDefinitionInput(dat)
2019-07-07 12:52:55 -08:00
if err != nil {
return err
}
2019-07-08 19:13:41 -08:00
// If a task definition value is empty, populate it with the default value.
2019-07-08 12:21:22 -08:00
if taskDefInput.Family == nil || *taskDefInput.Family == "" {
taskDefInput.Family = &flags.ServiceName
}
if len(taskDefInput.ContainerDefinitions) > 0 {
if taskDefInput.ContainerDefinitions[0].Name == nil || *taskDefInput.ContainerDefinitions[0].Name == "" {
taskDefInput.ContainerDefinitions[0].Name = &awsCreds.EcsServiceName
}
if taskDefInput.ContainerDefinitions[0].Image == nil || *taskDefInput.ContainerDefinitions[0].Image == "" {
2019-07-08 19:13:41 -08:00
taskDefInput.ContainerDefinitions[0].Image = &flags.ReleaseImage
2019-07-08 12:21:22 -08:00
}
}
//if flags.Debug {
// d, _ := json.Marshal(taskDef)
// log.Println(string(d))
//}
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)
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 == "" {
2019-07-08 19:13:41 -08:00
if flags.EcsExecutionRoleArn != "" {
taskDefInput.ExecutionRoleArn = &flags.EcsExecutionRoleArn
log.Printf("\t%s\tExecutionRoleArn updated.\n", tests.Success)
} else {
svc := iam.New(awsCreds.Session())
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
/*
res, err := svc.CreateServiceLinkedRole(&iam.CreateServiceLinkedRoleInput{
AWSServiceName: aws.String("ecs.amazonaws.com"),
Description: aws.String(""),
2019-07-08 12:21:22 -08:00
})
if err != nil {
2019-07-08 19:13:41 -08:00
return errors.Wrapf(err, "failed to register task definition '%s'", *taskDef.Family)
2019-07-08 12:21:22 -08:00
}
taskDefInput.ExecutionRoleArn = res.Role.Arn
2019-07-08 19:13:41 -08:00
*/
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
// Find or create role for ExecutionRoleArn.
{
roleName := fmt.Sprintf("ecsExecutionRole%s%s", flags.ProjectNameCamel(), strcase.ToCamel(flags.Env))
log.Printf("\tAppend ExecutionRoleArn to task definition input for role %s.", roleName)
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
res, err := svc.GetRole(&iam.GetRoleInput{
RoleName: aws.String(roleName),
2019-07-08 12:21:22 -08:00
})
if err != nil {
2019-07-08 19:13:41 -08:00
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
return errors.Wrapf(err, "failed to find task role '%s'", roleName)
}
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
if res.Role != nil {
taskDefInput.ExecutionRoleArn = res.Role.Arn
log.Printf("\t\t\tFound role '%s'", *taskDefInput.ExecutionRoleArn)
} else {
// If no repository was found, create one.
res, err := svc.CreateRole(&iam.CreateRoleInput{
RoleName: aws.String(roleName),
Description: aws.String(fmt.Sprintf("Provides access to other AWS service resources that are required to run Amazon ECS tasks for %s. ", flags.ProjectName)),
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ecs.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}"),
Tags: []*iam.Tag{
&iam.Tag{Key: aws.String(awsTagNameProject), Value: aws.String(flags.ProjectName)},
&iam.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(flags.Env)},
},
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
})
if err != nil {
return errors.Wrapf(err, "failed to create task role '%s'", roleName)
}
taskDefInput.ExecutionRoleArn = res.Role.Arn
log.Printf("\t\t\tCreated role '%s'", *taskDefInput.ExecutionRoleArn)
}
policyArns := []string{
"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
}
for _, policyArn := range policyArns {
_, err = svc.AttachRolePolicy(&iam.AttachRolePolicyInput{
PolicyArn: aws.String(policyArn),
RoleName: aws.String(roleName),
})
if err != nil {
return errors.Wrapf(err, "failed to attach policy '%s' to task role '%s'", policyArn, roleName)
}
log.Printf("\t\t\t\tAttached Policy '%s'", policyArn)
}
log.Printf("\t%s\tExecutionRoleArn updated.\n", tests.Success)
}
}
2019-07-08 12:21:22 -08:00
}
// The task role is the IAM role used by the task itself to access other AWS Services. To access services
// like S3, SQS, etc then those permissions would need to be covered by the TaskRole.
if taskDefInput.TaskRoleArn == nil || *taskDefInput.TaskRoleArn == "" {
2019-07-08 19:13:41 -08:00
if flags.EcsTaskRoleArn != "" {
taskDefInput.TaskRoleArn = &flags.EcsTaskRoleArn
log.Printf("\t%s\tTaskRoleArn updated.\n", tests.Success)
} else {
svc := iam.New(awsCreds.Session())
// Find or create the default service policy.
var policyArn string
{
policyName := fmt.Sprintf("%s%sServices", flags.ProjectNameCamel(), strcase.ToCamel(flags.Env))
log.Printf("\tFind default service policy %s.", policyName)
var policyVersionId string
err = svc.ListPoliciesPages(&iam.ListPoliciesInput{}, func(res *iam.ListPoliciesOutput, lastPage bool) bool{
for _, p := range res.Policies {
if *p.PolicyName == policyName {
policyArn = *p.Arn
policyVersionId = *p.DefaultVersionId
return false
}
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
return !lastPage
})
if err != nil {
return errors.Wrap(err, "failed to list IAM policies")
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
if policyArn != "" {
log.Printf("\t\t\tFound policy '%s' versionId '%s'", policyArn, policyVersionId)
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
res, err := svc.GetPolicyVersion(&iam.GetPolicyVersionInput{
PolicyArn: aws.String(policyArn),
VersionId: aws.String(policyVersionId),
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
return errors.Wrapf(err, "failed to read policy '%s' version '%s'", policyName, policyVersionId)
}
}
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
// Compare policy documents and add any missing actions for each statement by matching Sid.
var curDoc IamPolicyDocument
err = json.Unmarshal([]byte(*res.PolicyVersion.Document), &curDoc)
if err != nil {
return errors.Wrap(err, "failed to json decode policy document")
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
var updateDoc bool
for _, baseStmt := range baseServicePolicyDocument.Statement {
var found bool
for curIdx, curStmt := range curDoc.Statement {
if baseStmt.Sid != curStmt.Sid {
continue
}
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
for _, baseAction := range baseStmt.Action {
var hasAction bool
for _, curAction := range curStmt.Action {
if baseAction == curAction {
hasAction = true
break
}
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
if !hasAction {
log.Printf("\t\t\t\tAdded new action %s for '%s'", curStmt.Sid)
curStmt.Action = append(curStmt.Action, baseAction)
curDoc.Statement[curIdx] = curStmt
updateDoc = true
}
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
}
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
if !found {
log.Printf("\t\t\t\tAdded new statement '%s'", baseStmt.Sid)
curDoc.Statement = append(curDoc.Statement, baseStmt)
updateDoc = true
2019-07-08 12:21:22 -08:00
}
}
2019-07-08 19:13:41 -08:00
if updateDoc {
dat, err := json.Marshal(curDoc)
if err != nil {
return errors.Wrap(err, "failed to json encode policy document")
}
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
_, 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'", policyName, policyVersionId)
}
}
}
} else {
dat, err := json.Marshal(baseServicePolicyDocument)
2019-07-08 12:21:22 -08:00
if err != nil {
return errors.Wrap(err, "failed to json encode policy document")
}
2019-07-08 19:13:41 -08:00
// If no repository was found, create one.
res, err := svc.CreatePolicy(&iam.CreatePolicyInput{
PolicyName: aws.String(policyName),
Description: aws.String(fmt.Sprintf("Defines access for %s services. ", flags.ProjectName)),
2019-07-08 12:21:22 -08:00
PolicyDocument: aws.String(string(dat)),
2019-07-08 19:13:41 -08:00
2019-07-08 12:21:22 -08:00
})
if err != nil {
2019-07-08 19:13:41 -08:00
return errors.Wrapf(err, "failed to create task policy '%s'", policyName)
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
policyArn = *res.Policy.Arn
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
log.Printf("\t\t\tCreated policy '%s'", policyArn)
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
log.Printf("\t%s\tConfigured default service policy.\n", tests.Success)
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
// Find or create role for TaskRoleArn.
{
roleName := fmt.Sprintf("ecsTaskRole%s%s", flags.ProjectNameCamel(), strcase.ToCamel(flags.Env))
log.Printf("\tAppend TaskRoleArn to task definition input for role %s.", roleName)
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
res, err := svc.GetRole(&iam.GetRoleInput{
RoleName: aws.String(roleName),
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != iam.ErrCodeNoSuchEntityException {
return errors.Wrapf(err, "failed to find task role '%s'", roleName)
}
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
if res.Role != nil {
taskDefInput.TaskRoleArn = res.Role.Arn
log.Printf("\t\t\tFound role '%s'", *taskDefInput.TaskRoleArn)
} else {
// If no repository was found, create one.
res, err := svc.CreateRole(&iam.CreateRoleInput{
RoleName: aws.String(roleName),
Description: aws.String(fmt.Sprintf("Allows ECS tasks for %s to call AWS services on your behalf. ", flags.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(flags.ProjectName)},
&iam.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(flags.Env)},
},
})
if err != nil {
return errors.Wrapf(err, "failed to create task role '%s'", roleName)
}
taskDefInput.TaskRoleArn = res.Role.Arn
log.Printf("\t\t\tCreated role '%s'", *taskDefInput.TaskRoleArn)
//_, err = svc.UpdateAssumeRolePolicy(&iam.UpdateAssumeRolePolicyInput{
// PolicyDocument: ,
// RoleName: aws.String(roleName),
//})
//if err != nil {
// return errors.Wrapf(err, "failed to create task role '%s'", roleName)
//}
}
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
_, err = svc.AttachRolePolicy( &iam.AttachRolePolicyInput{
PolicyArn: aws.String(policyArn),
RoleName: aws.String(roleName),
2019-07-08 12:21:22 -08:00
})
if err != nil {
2019-07-08 19:13:41 -08:00
return errors.Wrapf(err, "failed to attach policy '%s' to task role '%s'", policyArn, roleName)
2019-07-08 12:21:22 -08:00
}
2019-07-08 19:13:41 -08:00
log.Printf("\t%s\tTaskRoleArn updated.\n", tests.Success)
2019-07-08 12:21:22 -08:00
}
}
}
log.Println("\tRegister new task definition.")
2019-07-08 19:13:41 -08:00
{
svc := ecs.New(awsCreds.Session())
2019-07-07 12:52:55 -08:00
2019-07-08 19:13:41 -08:00
// Registers a new task.
res, err := svc.RegisterTaskDefinition(taskDefInput)
if err != nil {
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)
2019-07-07 12:52:55 -08:00
2019-07-08 19:13:41 -08:00
log.Printf("\t%s\tTask definition registered.\n", tests.Success)
}
}
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
log.Println("ECS - Find Service")
var ecsService *ecs.Service
{
svc := ecs.New(awsCreds.Session())
2019-07-07 12:52:55 -08:00
2019-07-08 12:21:22 -08:00
res, err := svc.DescribeServices(&ecs.DescribeServicesInput{
Services: []*string{aws.String(awsCreds.EcsServiceName)},
})
if err != nil {
if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != ecs.ErrCodeServiceNotFoundException {
return errors.Wrapf(err, "failed to describe service '%s'", awsCreds.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)
}
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
// If the service exists update the service, else create a new service.
2019-07-08 19:13:41 -08:00
if ecsService != nil && *ecsService.Status != "INACTIVE" {
2019-07-08 12:21:22 -08:00
log.Println("ECS - Update Service")
svc := ecs.New(awsCreds.Session())
// 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.
desiredCount := *ecsService.DesiredCount
if desiredCount == 0 {
desiredCount = 1
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
_, err = svc.UpdateService(&ecs.UpdateServiceInput{
// 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,
2019-07-08 19:13:41 -08:00
2019-07-08 12:21:22 -08:00
// The name of the service to update.
Service: ecsService.ServiceName,
2019-07-08 19:13:41 -08:00
2019-07-08 12:21:22 -08:00
// The number of instantiations of the task to place and keep running in your
// service.
DesiredCount: aws.Int64(desiredCount),
2019-07-08 19:13:41 -08:00
2019-07-08 12:21:22 -08:00
// Whether to force a new deployment of the service. Deployments are not forced
// by default. You can use this option to trigger a new deployment with no service
// definition changes. For example, you can update a service's tasks to use
// a newer Docker image with the same image/tag combination (my_image:latest)
// or to roll Fargate tasks onto a newer platform version.
ForceNewDeployment: aws.Bool(false),
2019-07-08 19:13:41 -08:00
2019-07-08 12:21:22 -08:00
// 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 1,800 seconds. During that time, the ECS service scheduler
// ignores the Elastic Load Balancing 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: ecsService.HealthCheckGracePeriodSeconds,
2019-07-08 19:13:41 -08:00
2019-07-08 12:21:22 -08:00
// The family and revision (family:revision) or full ARN of the task definition
// to run in your service. If a revision is not specified, the latest ACTIVE
// revision is used. If you modify the task definition with UpdateService, Amazon
// ECS spawns a task with the new version of the task definition and then stops
// an old task after the new version is running.
TaskDefinition: taskDef.TaskDefinitionArn,
})
if err != nil {
return errors.Wrapf(err, "failed to update service '%s'", *ecsService.ServiceName)
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
log.Printf("\t%s\tUpdated ECS Service '%s'.\n", tests.Success, *ecsService.ServiceName)
} else {
2019-07-08 19:13:41 -08:00
log.Println("EC2 - Find Subnets")
var subnetsIDs []string
var vpcId string
{
svc := ec2.New(awsCreds.Session())
var subnets []*ec2.Subnet
if len(flags.Ec2SubnetIds) == 0 {
log.Println("\t\tFind all subnets are that default for each available AZ.")
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")
}
} else {
log.Println("\t\tFind all subnets for the IDs provided.")
err := svc.DescribeSubnetsPages(&ec2.DescribeSubnetsInput{
SubnetIds: aws.StringSlice(flags.Ec2SubnetIds),
}, func(res *ec2.DescribeSubnetsOutput, lastPage bool) bool {
for _, s := range res.Subnets {
subnets = append(subnets, s)
}
return !lastPage
})
if err != nil {
return errors.Wrapf(err, "failed to find subnets: %s", strings.Join(flags.Ec2SubnetIds, ", "))
} else if len(flags.Ec2SubnetIds) != len(subnets) {
return errors.Errorf("failed to find all subnets, expected %d, got %d", len(flags.Ec2SubnetIds) != len(subnets))
}
}
if len(subnets) == 0 {
return errors.New("failed to find any subnets, expected at least 1")
}
for _, s := range subnets {
if s.VpcId == nil {
continue
}
if vpcId == "" {
vpcId = *s.VpcId
} else if vpcId != *s.VpcId {
return errors.Errorf("invalid subnet %s, all subnets should belong to the same VPC, expected %s, got %s", *s.SubnetId, vpcId, *s.VpcId)
}
subnetsIDs = append(subnetsIDs, *s.SubnetId)
log.Printf("\t\t\t%s", *s.SubnetId)
}
log.Printf("\t%s\tFound %d subnets.\n", len(subnets))
}
log.Println("EC2 - Find Security Group")
{
svc := ec2.New(awsCreds.Session())
log.Printf("\t\tFind security group '%s'.\n", flags.Ec2SecurityGroupName)
var securityGroupId string
err := svc.DescribeSecurityGroupsPages(&ec2.DescribeSecurityGroupsInput{
GroupNames: aws.StringSlice([]string{flags.Ec2SecurityGroupName}),
}, func(res *ec2.DescribeSecurityGroupsOutput, lastPage bool) bool {
for _, s := range res.SecurityGroups {
if *s.GroupName == flags.Ec2SecurityGroupName {
securityGroupId = *s.GroupId
break
}
}
return !lastPage
})
if err != nil {
return errors.Wrapf(err, "failed to find security group '%s'", flags.Ec2SecurityGroupName)
}
if securityGroupId == "" {
// If no security group was found, create one.
_, err = svc.CreateSecurityGroup(&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(flags.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", flags.ProjectName, awsCreds.EcsClusterName)),
// [EC2-VPC] The ID of the VPC. Required for EC2-VPC.
VpcId : aws.String(vpcId),
})
if err != nil {
return errors.Wrapf(err, "failed to create cluster '%s'", awsCreds.EcsClusterName)
}
log.Printf("\t\tCreated: %s.", flags.Ec2SecurityGroupName)
} else {
log.Printf("\t\tFound: %s.", flags.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),
},
// Allow all services in the security group to access other services via HTTP port 80.
&ec2.AuthorizeSecurityGroupIngressInput{
IpProtocol: aws.String("tcp"),
SourceSecurityGroupName: aws.String( flags.Ec2SecurityGroupName),
FromPort: aws.Int64(80),
ToPort: aws.Int64(80),
},
}
// When we are 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 !flags.EnableElb {
// 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),
})
// Allow all services in the security group to access other services via HTTPS port 443.
ingressInputs = append(ingressInputs, &ec2.AuthorizeSecurityGroupIngressInput{
IpProtocol: aws.String("tcp"),
SourceSecurityGroupName: aws.String( flags.Ec2SecurityGroupName),
FromPort: aws.Int64(443),
ToPort: aws.Int64(443),
})
}
// Add all the default ingress to the security group.
for _, ingressInput := range ingressInputs {
_, err = svc.AuthorizeSecurityGroupIngress(ingressInput)
if err != nil {
return errors.Wrapf(err, "failed to add ingress for securuty group '%s'", flags.Ec2SecurityGroupName)
}
}
log.Printf("\t%s\tUsing Security Group '%s'.\n", tests.Success, flags.Ec2SecurityGroupName)
}
// If an Elastic Load Balancer is enabled, then ensure one exists else create one.
var ecsELBs []*ecs.LoadBalancer
if flags.EnableElb {
log.Println("EC2 - Find Elastic Load Balance")
svc := elbv2.New(awsCreds.Session())
// Set default EBL if needed.
var maintainELB bool
if flags.EnableElb && flags.ElbName == "" {
if !strings.Contains(awsCreds.EcsClusterName, flags.Env) && !strings.Contains(flags.ServiceName, flags.Env) {
// When a custom cluster name is provided and/or service name, ensure the ELB contains the current env.
flags.ElbName = fmt.Sprintf("%s-%s-%s", awsCreds.EcsClusterName, flags.ServiceName, flags.Env)
} else {
// Default value when when custom cluster/service name is supplied.
flags.ElbName = fmt.Sprintf("%s-%s", awsCreds.EcsClusterName, flags.ServiceName)
}
log.Printf("\t\t\tSet ELB Name to '%s'.", flags.ElbName)
// When not ELB name is provided and is assigned by us, we should manage the associated target groups
// and other properties.
maintainELB = true
}
var elb *elbv2.LoadBalancer
err := svc.DescribeLoadBalancersPages(&elbv2.DescribeLoadBalancersInput{
Names: []*string{aws.String(flags.ElbName)},
}, func(res *elbv2.DescribeLoadBalancersOutput, lastPage bool) bool{
for _, lb := range res.LoadBalancers {
if *lb.LoadBalancerName == flags.ElbName {
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 balance '%s'", flags.ElbName)
}
}
if elb == nil {
// If no repository was found, create one.
createRes, err := svc.CreateLoadBalancer(&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(flags.ElbName),
// [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("dualstack"),
// 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"),
// [Application Load Balancers] The IDs of the security groups for the load
// balancer.
SecurityGroups: aws.StringSlice([]string{flags.Ec2SecurityGroupName}),
// The IDs of the public subnets. You can specify only one subnet per Availability
// Zone. You must specify either subnets or subnet mappings.
// [Application Load Balancers] You must specify subnets from at least two Availability
// Zones.
Subnets: aws.StringSlice(subnetsIDs),
// 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(flags.ProjectName)},
&elbv2.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(flags.Env)},
},
})
if err != nil {
return errors.Wrapf(err, "failed to create cluster '%s'", awsCreds.EcsClusterName)
}
elb = createRes.LoadBalancers[0]
log.Printf("\t\tCreated: %s.", *elb.LoadBalancerArn)
} else {
log.Printf("\t\tFound: %s.", *elb.LoadBalancerArn)
}
// 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)
if maintainELB {
targetGroupInputs := []*elbv2.CreateTargetGroupInput{
// Default target group for HTTP via port 80.
&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(fmt.Sprintf("%s-http", *elb.LoadBalancerName)),
// 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"),
// The identifier of the virtual private cloud (VPC). If the target is a Lambda
// function, this parameter does not apply.
VpcId: aws.String(vpcId),
},
// Default target group for HTTPS via port 443.
&elbv2.CreateTargetGroupInput{
Name: aws.String(fmt.Sprintf("%s-https", *elb.LoadBalancerName)),
Port: aws.Int64(443),
Protocol: aws.String("HTTPS"),
HealthCheckEnabled: aws.Bool(true),
HealthCheckIntervalSeconds: aws.Int64(30),
HealthCheckPath: aws.String( "/ping"),
HealthCheckProtocol: aws.String("HTTPS"),
HealthCheckTimeoutSeconds: aws.Int64(5),
HealthyThresholdCount: aws.Int64(3),
UnhealthyThresholdCount: aws.Int64(3),
Matcher: &elbv2.Matcher{
HttpCode: aws.String("200"),
},
TargetType: aws.String("ip"),
VpcId: aws.String(vpcId),
},
}
for _, targetGroupInput := range targetGroupInputs {
var targetGroup *elbv2.TargetGroup
err = svc.DescribeTargetGroupsPages(&elbv2.DescribeTargetGroupsInput{
LoadBalancerArn: elb.LoadBalancerArn,
Names: []*string{aws.String(flags.ElbName)},
}, func(res *elbv2.DescribeTargetGroupsOutput, lastPage bool) bool{
for _, tg := range res.TargetGroups {
if *tg.TargetGroupName == *targetGroupInput.Name {
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'", *targetGroupInput.Name)
}
}
if targetGroup == nil {
// If no target group was found, create one.
createRes, err := svc.CreateTargetGroup(targetGroupInput)
if err != nil {
return errors.Wrapf(err, "failed to create target group '%s'", *targetGroupInput.Name)
}
targetGroup = createRes.TargetGroups[0]
log.Printf("\t\tAdded target group: %s.", *targetGroup.TargetGroupArn)
} else {
log.Printf("\t\tHas target group: %s.", *targetGroup.TargetGroupArn)
}
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(awsCreds.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,
})
if flags.elbDeregistrationDelay != -1 {
// 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(flags.elbDeregistrationDelay)),
},
},
})
if err != nil {
return errors.Wrapf(err, "failed to modify target group '%s' attributes", *targetGroupInput.Name)
}
log.Printf("\t\t\tSet sttributes.")
}
}
}
log.Printf("\t%s\tUsing ELB '%s'.\n", tests.Success, *elb.LoadBalancerName)
}
2019-07-08 12:21:22 -08:00
log.Println("ECS - Create Service")
2019-07-08 19:13:41 -08:00
{
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
svc := ecs.New(awsCreds.Session())
var assignPublicIp *string
if len(ecsELBs) == 0 {
assignPublicIp = aws.String("ENABLED")
} else {
assignPublicIp = aws.String("DISABLED")
}
createRes, err := svc.CreateService(&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(awsCreds.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: aws.Int64(int64(flags.ecsServiceMaximumPercent)),
// Refer to documentation for flags.ecsServiceMinimumHealthyPercent
MinimumHealthyPercent: aws.Int64(int64(flags.ecsServiceMinimumHealthyPercent)),
},
// Refer to documentation for flags.ecsServiceDesiredCount.
DesiredCount: aws.Int64(int64(flags.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: aws.Int64(int64(flags.escServiceHealthCheckGracePeriodSeconds)),
// 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{flags.Ec2SecurityGroupName}),
// 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(subnetsIDs),
},
},
// 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(flags.ProjectName)},
&ecs.Tag{Key: aws.String(awsTagNameEnv), Value: aws.String(flags.Env)},
},
})
if err != nil {
return errors.Wrapf(err, "failed to create service '%s'", awsCreds.EcsServiceName)
}
ecsService = createRes.Service
log.Printf("\t%s\tCreated ECS Service '%s'.\n", tests.Success, *ecsService.ServiceName)
}
2019-07-08 12:21:22 -08:00
2019-07-07 12:52:55 -08:00
}
2019-07-08 12:21:22 -08:00
2019-07-08 19:13:41 -08:00
// If Elastic cache is enabled, need to add ingress to security group
2019-07-08 12:21:22 -08:00
return nil
}