Skip to content

Instantly share code, notes, and snippets.

@AlexAtkinson
Last active November 29, 2023 19:54
Show Gist options
  • Save AlexAtkinson/5864dd20a58e40235ae66b4ac3d6713a to your computer and use it in GitHub Desktop.
Save AlexAtkinson/5864dd20a58e40235ae66b4ac3d6713a to your computer and use it in GitHub Desktop.
BASH from the PAST: Provision AWS Fargate cluster
#!/usr/bin/env bash
# --------------------------------------------------------------------------------------------------
# ./launch-stack.sh
#
# Description:
# Launches a ECS Fargate stack with blue/green deployment support.
#
# Notes:
# - This script offered as a curiosity, now, since this is now satisfied
# with Terraform.
# - Hardcoded vpc and subnet maps.
# - There were two vpcs for this project, one for web services
# and one for Adobe Experience Manager.]
# - There was a destroy script also that would go through the log output
# from this and then destroy aws assets... It got most of em.
# - Depends on awscli profiles to be setup on the host matching
# the envs available in the inputs section of this script.
#
# --------------------------------------------------------------------------------------------------
# Help
# --------------------------------------------------------------------------------------------------
show_help() {
cat << EOF
Launches a ECS Fargate stack with blue/green deployment support.
Note that B/G is not supported using CFN, CDK, or SDK, so this is just a collection of awscli commands.
Use: ${0##*/} -e {-r}
-e ENV The environment in which to deploy.
Options: dev, qa, uat, prod
-v VPC The VPC in which to deploy.
Options: ws, aem
-n NAME Stack Name
Stack names will be appended with an 8 character unique hash to help
in identifying resources associated with this stack.
TIP: Align this with the name of the project repository.
-h HELP Show this help menu.
Requirements:
Programs required to be in your \$PATH:
- bash (no Mac OSX FreeBSD sillyness)
- jq
- awscli
- AWS Access ID and Secret Access Keys for each env
- awscli profiles setup for each env
Examples:
Launch the qa stack in us-east-2
./launch-stack.sh -e qa -r us-east-2 -n best-webapp
EOF
exit 1
}
# --------------------------------------------------------------------------------------------------
# Arguments
# --------------------------------------------------------------------------------------------------
# Handle options
OPTIND=1
while getopts "he:v:n:" opt; do
case "$opt" in
h)
show_help
;;
e)
arg_e='set'
arg_e_val="$OPTARG"
;;
v)
arg_v='set'
arg_v_val="$OPTARG"
;;
n)
arg_n='set'
arg_n_val="$OPTARG"
;;
:)
echo "ERROR: Option -$OPTARG requires an argument."
show_help
;;
*)
echo "ERROR: Unknown option!"
show_help
;;
esac
done
shift $((OPTIND-1))
[ "$1" = "--" ] && shift
# --------------------------------------------------------------------------------------------------
# Functions 1/2
# --------------------------------------------------------------------------------------------------
function ask {
while true; do
if [ "${2:-}" = "Y" ]; then
prompt="Y/n"
default=Y
elif [ "${2:-}" = "N" ]; then
prompt="y/N"
default=N
elif [ "${2:-}" = "Range" ]; then
prompt="${3:-}"
default=0
else
prompt="y/n"
default=
fi
# Ask the question
read -p $"$1 [$prompt]: " reply
# Default?
if [ -z "$reply" ]; then
reply=$default
fi
# Check if the reply is valid
case "$reply" in
Y*|y*|[1-99]) return 0 ;;
N*|n*|0*) return 1 ;;
esac
done
}
# --------------------------------------------------------------------------------------------------
# Variables & User Inputs
# --------------------------------------------------------------------------------------------------
if [[ ! -n $arg_n ]]; then
read -rep $'\nEnter a name for this stack: ' arg_n_val
echo ''
fi
if ask "Will the ALB listner require an ACM certificate?" Y; then
echo ''
certRequired=true
if ask "Do you already have an ACM certificate to use with the ALB listener?" N; then
read -rep $'\nProvide the ACM certificate ARN: ' acmArn
fi
fi
if ask "$(echo -e "\nWill the ALB scheme be internal or internet-facing?\n"\\\n' 1) internal'\\\n' 2) internet-facing'\\\n\\\n'Enter Response')" Range 1-2; then
case $reply in
1) albScheme='internal';;
2) albScheme='internet-facing';;
esac
fi
read -rep $'\nEnter the TLD for this endpoint (ie: example.com): ' stackTLD
read -rep $'\nEnter the domain for this endpoint (ie: foo.example.com): ' stackDomain
echo ''
if ask "Do you already have an ECR repository with a functioning Docker image?" N; then
read -rep $'\nProvide the URI of your ECR repository: ' ecrRepoSupplied
read -rep $'\nProvide the image tag: ' ecrImageTag
fi
if [[ ! -n $arg_e ]]; then
if ask "$(echo -e "\nSelect Environment\n"\\\n' 1) Dev'\\\n' 2) QA'\\\n' 3) UAT'\\\n' 4) PROD'\\\n\\\n'Enter Response')" Range 1-4; then
case $reply in
1) arg_e_val='dev';;
2) arg_e_val='qa';;
3) arg_e_val='uat';;
4) arg_e_val='prod';;
esac
fi
fi
if [[ ! -n $arg_v ]]; then
if ask "$(echo -e "\nSelect VPC\n"\\\n' 1) WebServices'\\\n' 2) AEM'\\\n\\\n'Enter Response')" Range 1-2; then
case $reply in
1) arg_v_val=Ws;;
2) arg_v_val=Aem;;
esac
fi
fi
if ask "$(echo -e "\nSelect Target Group \e[04mtraffic\e[0m protocol\n"\\\n' 1) HTTP'\\\n' 2) HTTPS'\\\n' 3) TCP'\\\n' 4) TLS'\\\n' 5) UDP'\\\n' 6) TCP_UDP'\\\n\\\n'Enter Response')" Range 1-6; then
case $reply in
1) trafficProto=HTTP; taskProto=tcp;;
2) trafficProto=HTTPS; taskProto=tcp;;
3) trafficProto=TCP; taskProto=tcp;;
4) trafficProto=TLS; taskProto=tcp;;
5) trafficProto=UDP; taskProto=udp;;
6) trafficProto=TCP_UDP; taskProto=udp;;
esac
fi
if [[ $trafficProto == HTTPS ]]; then
hcCurlProto=https
else
hcCurlProto=http
fi
read -rep $'\nSpecify Target Group \e[04mtraffic\e[0m port: ' trafficPort
if ask "$(echo -e "\nSelect Target Group \e[04mhealth check\e[0m protocol\n"\\\n' 1) HTTP'\\\n' 2) HTTPS'\\\n' 3) TCP'\\\n' 4) TLS'\\\n' 5) UDP'\\\n' 6) TCP_UDP'\\\n\\\n'Enter Response')" Range 1-6; then
case $reply in
1) hcProto=HTTP;;
2) hcProto=HTTPS;;
3) hcProto=TCP;;
4) hcProto=TLS;;
5) hcProto=UDP;;
6) hcProto=TCP_UDP;;
esac
fi
read -rep $'\nSpecify Target Group \e[04mhealth check\e[0m port: ' hcPort
if ask "$(echo -e "\nCreate standard HTTP/HTTPS listeners for web traffic, or adhere to previously specified traffic PROTO/PORT?\n"\\\n' 1) HTTP/HTTPS'\\\n' 2) As Specified'\\\n\\\n'Enter Response')" Range 1-2; then
case $reply in
1) listeners=web;;
2) listeners=custom;;
esac
fi
echo -e "\nNOTE: These values must correspond with the Task Size table found here: (ie: cpu: 256, mem: 512)\n"
echo -e " https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size << DO READ THIS"
read -rep $'\nSpecify ECS Task CPU Allocation: ' ecsTaskCPU
read -rep $'\nSpecify ECS Task Memory Allocation: ' ecsTaskMem
read -rep $'\nSpecify ECS Desired Task Count: ' ecsDesiredTaskCount
if ask "Assign PublicIPs to ECS Fargate Tasks?" Y; then
taskPubIp='ENABLED'
else
taskPubIp='DISABLED'
fi
project="foo"
projectLong="foobar"
# Environment
env="$arg_e_val"
vpc="$arg_v_val"
# Region Map
devRegion='eu-west-1'
qaRegion='eu-west-1'
uatRegion='eu-west-1'
prodRegion='eu-west-1'
drRegion='eu-central-1'
execRegionConstruct="${env}Region"
region="${!execRegionConstruct}"
if [[ -n $arg_r ]] ; then
region="$arg_r_val"
fi
# awsRoute53ZoneId Map
# https://docs.aws.amazon.com/general/latest/gr/rande.html#elb_region
euwest1ZoneId='Z32O12XQLNTSW2'
eucentral1ZoneId='Z215JYRZR1TBD5'
zoneIdConstruct="$(tr -d - <<< ${region})ZoneId"
awsRoute53ZoneId=${!zoneIdConstruct}
# Subnet Map
devWsPubSubnets="subnet-foo subnet-foo"
qaWsPubSubnets="subnet-foo subnet-foo"
uatWsPubSubnets="subnet-foo subnet-foo"
prodWsPubSubnets="subnet-foo subnet-foo"
devWsPrivSubnets="subnet-foo subnet-foo"
qaWsPrivSubnets="subnet-foo subnet-foo"
uatWsPrivSubnets="subnet-foo subnet-foo"
prodWsPrivSubnets="subnet-foo subnet-foo"
devAemPubSubnets="subnet-foo subnet-foo"
qaAemPubSubnets="subnet-foo subnet-foo"
uatAemPubSubnets="subnet-foo subnet-foo"
prodAemPubSubnets="subnet-foo subnet-foo subnet-foo"
devAemPrivSubnets="subnet-foo subnet-foo"
qaAemPrivSubnets="subnet-foo subnet-foo"
uatAemPrivSubnets="subnet-foo subnet-foo"
prodAemPrivSubnets="subnet-foo subnet-foo subnet-foo"
subnetsPubConstruct="${env}${vpc}PubSubnets"
subnetsPublic=(${!subnetsPubConstruct})
subnetsPrivConstruct="${env}${vpc}PrivSubnets"
subnetsPrivate=(${!subnetsPrivConstruct})
if [[ $albScheme == internal ]]; then
albSubnets=(${subnetsPrivate[@]})
else
albSubnets=(${subnetsPublic[@]})
fi
run=1; count=$(wc -w <<< ${in[@]}); for i in ${in[@]}; do if [[ $run -lt $count ]]; then foo+=(\"$i\",); ((run++)); else foo+=(\"$i\"); fi; done
ecsSubnetRun=1
ecsSubnetsFormat=()
subnetsCount=$(wc -w <<< ${subnetsPrivate[@]})
for subnet in ${subnetsPrivate[@]}; do
if [[ $ecsSubnetRun -lt $subnetsCount ]]; then
ecsSubnetsFormat+=(\"$subnet\",)
((ecsSubnetRun++))
else
ecsSubnetsFormat+=(\"$subnet\")
fi
done
# VPC ID Map
devWsVPC="vpc-foo"
qaWsVPC="vpc-foo"
uatWsVPC="vpc-foo"
prodWsVPC="vpc-foo"
devAemVPC="vpc-foo"
qaAemVPC="vpc-foo"
uatAemVPC="vpc-foo"
prodAemVPC="vpc-foo"
execVpcConstruct="${env}${vpc}VPC"
vpcId="${!execVpcConstruct}"
# VPC CIDR Map
devWsVPC="192.168.8.0/21"
qaWsVPC="192.168.24.0/21"
uatWsVPC="192.168.40.0/21"
prodWsVPC="192.168.56.0/21"
devAemVPC="192.168.0.0/21"
qaAemVPC="192.168.16.0/21"
uatAemVPC="192.168.32.0/21"
prodAemVPC="192.168.48.0/21"
execVpcConstruct="${env}${vpc}VPC"
vpcCidr="${!execVpcConstruct}"
# Contexts
localPath="$(pwd)"
datetime=$(date +"%Y%m%d-%H%M%S")
stackName="$(tr '[:upper:]' '[:lower:]' <<< ${arg_n_val})"
stackUniquer="$(openssl rand -base64 128 | tr -dc 'a-zA-Z0-9' | fold -w 4 | head -n 1 | tr '[:upper:]' '[:lower:]')"
#stackResourceName="$(echo "${env}-${project}-${stackName}-${stackUniquer}" | tr '[:upper:]' '[:lower:]')"
stackResourceName="$(echo "${project}-${stackName}-${stackUniquer}" | tr '[:upper:]' '[:lower:]')"
thisFile="${0##*/}"
logFile="$(sed 's/.sh//' <<< "${thisFile}").log"
# AWS
accountId=$(aws --profile ${env} sts get-caller-identity --output text --query 'Account')
ecrEndpoint="${accountId}.dkr.ecr.${region}.amazonaws.com"
ecrRepo="${ecrEndpoint}/${stackResourceName}"
[[ -n ${ecrRepoSupplied+x} ]] && ecrRepo="${ecrRepoSupplied}"
s3Endpoint="https://s3.${region}.amazonaws.com"
s3Bucket="${env}-${projectLong}-devops"
s3Prefix="fargate/${stackResourceName}/"
s3Path="${s3Endpoint}/${s3Bucket}/${s3Prefix}"
# Notification Contact
notificationEmail="foo@bar.com"
resourceIds=()
# --------------------------------------------------------------------------------------------------
# Functions 2/2
# --------------------------------------------------------------------------------------------------
function rc {
if [[ $? -eq $1 ]] ; then
echo -e "$(date --utc +"%FT%T.%3NZ")" - "SUCCESS: $task" | tee -a "$logFile"
else
echo -e "$(date --utc +"%FT%T.%3NZ")" - "ERROR: $task" | tee -a "$logFile"
fi
}
function et {
echo -e "\n${task}..."
}
function id_out {
type=$(cut -d- -f1 <<< $1)
case $type in
albArn)
sed -i "1i${1}" stacks/${date}-${env}-${stackResourceName}.resourceIds.out
;;
*)
echo $1 >> stacks/${date}-${env}-${stackResourceName}.resourceIds.out
;;
esac
resourceIds+=($1)
}
function eventual-consistency {
sleep 1 ; echo -e "\n3..."
sleep 1 ; echo -e "2..."
sleep 1 ; echo -e "1...\n"
sleep 1 ; echo -e "LET'S JAM!!\n"
sleep 1
}
function upload-appspec {
if ! aws --profile "${env}" --region "$region" s3api head-bucket --bucket "$s3Bucket" ; then
task="Create bucket $s3Bucket" ; et
aws --profile "${env}" --region "$region" s3 mb --region "$region" "s3://${s3Bucket}"
rc 0
eventual-consistency
fi
task="Upload appspec.yaml to $s3Path/"
aws --profile "${env}" --region "$region" s3 cp "appspec.yaml" ${s3Path}
rc 0
eventual-consistency
}
function ecrLogin {
task="Login to ECR"; et
$(aws --profile $env --region $region ecr get-login --no-include-email); rc 0
}
# --------------------------------------------------------------------------------------------------
# Main Operations
# --------------------------------------------------------------------------------------------------
date=$(date +"%Y%H%d")
task="Create log group"; et
logGroupName="/ecs/${stackResourceName}"
aws --profile ${env} --region ${region} logs create-log-group --log-group-name ${logGroupName}; rc 0
id_out "logGroup-${logGroupName}"
zoneId=$(aws --profile $env --region ${region} route53 list-hosted-zones-by-name \
--query HostedZones \
| jq -r ".[] | select(.Name == \"${stackTLD}.\").Id" \
| sed 's/\/hostedzone\///')
if [[ $certRequired == true ]]; then
if [[ ! -n ${acmArn+x} ]]; then
task="Request ACM Certificate"; et
acmArn=$(aws --profile $env acm request-certificate \
--region $region \
--domain-name $stackDomain \
--subject-alternative-names *.${stackDomain} \
--validation-method DNS \
--query CertificateArn \
--output text); rc 0
id_out acmArn-${acmArn}
sleep 10
certJson=$(aws --profile $env acm describe-certificate \
--region $region \
--certificate-arn $acmArn \
--query Certificate.DomainValidationOptions)
certName=$(jq -r ".[] | select(.DomainName == \"$stackDomain\").ResourceRecord.Name" <<< $certJson)
certValue=$(jq -r ".[] | select(.DomainName == \"$stackDomain\").ResourceRecord.Value" <<< $certJson)
read -r -d '' r53Json << EOM
{
"Comment": "DNS Validation CNAME record",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "$certName",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [
{
"Value": "$certValue"
}
]
}
}
]
}
EOM
task="Create CNAME for ACM Certificate Validation"; et
changeId=$(aws --profile ${env} --region ${region} route53 change-resource-record-sets \
--hosted-zone-id "$zoneId" \
--change-batch "$r53Json" \
--query ChangeInfo.Id \
--output text); rc 0
task="Waiting for certificate validation (this could take a bit ノಥ益ಥ)ノ ┻━┻#)"; et
aws --profile ${env} acm wait certificate-validated \
--certificate-arn $acmArn \
--region $region; rc 0
id_out r53Record--${certName}--${certValue}--${zoneId}
fi
fi
if [[ ! -n ${ecrRepoSupplied+x} ]]; then
ecrLogin
task="Create ECR repository"; et
unset id; id=$(jq -r .repository.repositoryName <<< $(aws --profile $env --region $region ecr create-repository --repository-name $stackResourceName)); rc 0
ecrImageTag='latest'
id_out ecrRepo-${accountId}-${id}
task="Build $stackName Docker image"; et
docker build -t $stackName .; rc 0
task="Tag image as ${ecrImageTag}"; et
docker tag $stackName:${ecrImageTag} ${ecrRepo}:${ecrImageTag}; rc 0
task="Push image to ECR"; et
docker push ${ecrRepo}:${ecrImageTag}; rc 0
fi
task="Create Security Group: ${stackResourceName}-web"; et
unset id; id=$(jq -r .GroupId <<< $(aws --profile ${env} --region ${region} ec2 create-security-group --group-name "${stackResourceName}-web" --description "World HTTP/S Ingress for ${stackResourceName}" --vpc-id ${vpcId})); rc 0
webSg=$id
id_out $id
task="Tag SG"; et
aws --profile ${env} --region ${region} ec2 create-tags --resources $id --tags Key="Name",Value="World HTTP/S Ingress for ${stackResourceName}-web"; rc 0
task="Add ingress rule for HTTP"; et
aws --profile ${env} --region ${region} ec2 authorize-security-group-ingress --group-id $id --ip-permissions IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges="[{CidrIp=0.0.0.0/0,Description=\"World Ingress via HTTP\"}]"; rc 0
task="Add ingress rule for HTTPS"; et
aws --profile ${env} --region ${region} ec2 authorize-security-group-ingress --group-id $id --ip-permissions IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges="[{CidrIp=0.0.0.0/0,Description=\"World Ingress via HTTPS\"}]"; rc 0
task="Create Security Group: ${stackResourceName}-ecs"; et
unset id; id=$(jq -r .GroupId <<< $(aws --profile ${env} --region ${region} ec2 create-security-group --group-name "${stackResourceName}-ecs" --description "${vpc} VPC Ingress via ${hcProto}${hcPort} for ${stackResourceName}-ecs" --vpc-id ${vpcId})); rc 0
ecsSg=$id
id_out $id
task="Tag SG"; et
aws --profile ${env} --region ${region} ec2 create-tags --resources $id --tags Key="Name",Value="${vpc} VPC Ingress via ${hcProto}\\${hcPort} for ${stackResourceName}-ecs"; rc 0
task="Add ingress rule for ${hcProto}/${hcPort}"; et
aws --profile ${env} --region ${region} ec2 authorize-security-group-ingress --group-id $id --ip-permissions IpProtocol=${taskProto},FromPort=${hcPort},ToPort=${hcPort},IpRanges="[{CidrIp=${vpcCidr},Description=\"${vpc} VPC Ingress via ${hcProto}/${hcPort}\"}]"; rc 0
task="Create IAM Role for ecsTaskExecution"; et
unset id; id=$(jq -r .Role.RoleName <<< $(aws --profile ${env} --region ${region} iam create-role --role-name "ecsTaskExecutionRole_${stackResourceName}" --description "Allows ECS Tasks to call AWS services." --tags Key="Name",Value="ecsTaskExecutionRole_${stackResourceName}" --max-session-duration 3600 --assume-role-policy-document file://fargate/iam/ecsTaskExecutionRole_Trust-Policy.json)); rc 0
id_out iamRole-${id}
ecsTaskExecutionRoleArn=$(aws --profile ${env} --region ${region} iam get-role --role-name ecsTaskExecutionRole_${stackResourceName} --query Role.Arn --output text)
task="Attach AmazonECSTaskExecutionPolicy policy"; et
aws --profile ${env} iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy --role-name ecsTaskExecutionRole_${stackResourceName}; rc 0
task="Create IAM Role for CodeDeploy"; et
unset id; id=$(jq -r .Role.RoleName <<< $(aws --profile ${env} --region ${region} iam create-role --role-name "AWSCodeDeployRoleForECS_${stackResourceName}" --description "Allows CodeDeploy to read S3 objects, invoke Lambda functions, publish to SNS topics, and update ECS services." --tags Key="Name",Value="AWSCodeDeployRoleForECS_${stackResourceName}" --max-session-duration 3600 --assume-role-policy-document file://fargate/iam/AWSCodeDeployRoleForECS_Trust-Policy.json)); rc 0
id_out iamRole-${id}
task="Get ARN for IAM Role for CodeDeploy"; et
ecsCodeDeployRoleArn=$(aws --profile ${env} --region ${region} iam get-role --role-name AWSCodeDeployRoleForECS_${stackResourceName} --query Role.Arn --output text)
Task="Attach AWSCodeDeployRoleForECS policy"
aws --profile ${env} --region ${region} iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS --role-name AWSCodeDeployRoleForECS_${stackResourceName}; rc 0
task="Create Load Balancer"; et
unset id; id=$(jq -r .LoadBalancers[].LoadBalancerArn <<< $(aws --profile ${env} --region ${region} elbv2 create-load-balancer \
--name ${stackResourceName} \
--scheme ${albScheme} \
--subnets ${albSubnets[@]} \
--security-groups $webSg)); rc 0
id_out albArn-${id}
albArn=${id}
sleep 3
albDNSName=$(aws --profile ${env} --region ${region} elbv2 describe-load-balancers --load-balancer-arn ${albArn} --query LoadBalancers[].DNSName --output text)
task="Create TG1"; et
unset id; id=$(jq -r .TargetGroups[].TargetGroupArn <<< $(aws --profile ${env} elbv2 create-target-group \
--name ${stackResourceName}-tg-a \
--protocol ${trafficProto} \
--port ${trafficPort} \
--target-type ip \
--vpc-id ${vpcId} \
--region ${region} \
--health-check-protocol ${hcProto} \
--health-check-port ${hcPort} \
--health-check-enabled \
--health-check-path / \
--health-check-interval-seconds 10 \
--health-check-timeout-seconds 3 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 2)); rc 0
id_out tgArn-${id}
tg1Arn=${id}
task="Create TG2"; et
unset id; id=$(jq -r .TargetGroups[].TargetGroupArn <<< $(aws --profile ${env} elbv2 create-target-group \
--name ${stackResourceName}-tg-b \
--protocol ${trafficProto} \
--port ${trafficPort} \
--target-type ip \
--vpc-id ${vpcId} \
--region ${region} \
--health-check-protocol ${hcProto} \
--health-check-port ${hcPort} \
--health-check-enabled \
--health-check-path / \
--health-check-interval-seconds 10 \
--health-check-timeout-seconds 3 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 2)); rc 0
id_out tgArn-${id}
tg2Arn=${id}
listenerArns=()
if [[ $listeners == web ]]; then
task="Create ALB listener for port 80"; et
listenerArns+=($(aws --profile ${env} --region ${region} elbv2 create-listener \
--load-balancer-arn ${albArn} \
--protocol HTTP \
--port 80 \
--default-actions Type=redirect,Order=1,RedirectConfig="{Protocol=HTTPS,Port=443,Host=\"#{host}\",Path=\"/#{path}\",Query=\"#{query}\",StatusCode=HTTP_301}" \
--query Listeners[].ListenerArn \
--output text)); rc 0
task="Create ALB listener for port 443"; et
deploymentGroupListenerArn=$(aws --profile ${env} --region ${region} elbv2 create-listener \
--load-balancer-arn ${albArn} \
--protocol HTTPS \
--port 443 \
--ssl-policy ELBSecurityPolicy-2016-08 \
--certificates CertificateArn=${acmArn} \
--default-actions Type=fixed-response,Order=1,FixedResponseConfig="{StatusCode=503,ContentType=text/plain}" \
--query Listeners[].ListenerArn \
--output text); rc 0
listenerArns+=(${deploymentGroupListenerArn})
else
if [[ $trafficPort -eq 443 ]]; then
task="Create ALB listener for port 443"; et
deploymentGroupListenerArn=$(aws --profile ${env} --region ${region} elbv2 create-listener \
--load-balancer-arn ${albArn} \
--protocol HTTPS \
--port 443 \
--ssl-policy ELBSecurityPolicy-2016-08 \
--certificates CertificateArn=${acmArn} \
--default-actions Type=fixed-response,Order=1,FixedResponseConfig="{StatusCode=503,ContentType=text/plain}" \
--query Listeners[].ListenerArn \
--output text); rc 0
listenerArns+=(${deploymentGroupListenerArn})
else
task="Create ALB listner for port ${trafficPort}"; et
deploymentGroupListenerArn=$(aws --profile ${env} --region ${region} elbv2 create-listener \
--load-balancer-arn ${albArn} \
--protocol ${trafficProto} \
--port ${trafficPort} \
--default-actions Type=fixed-response,Order=1,FixedResponseConfig="{StatusCode=503,ContentType=text/plain}" \
--query Listeners[].ListenerArn \
--output text); rc 0
listenerArns+=(${deploymentGroupListenerArn})
fi
fi
task="Create listener rule to forward traffic for host-header ${stackDomain}"; et
ruleArn=$(aws --profile ${env} --region ${region} elbv2 create-rule \
--listener-arn ${deploymentGroupListenerArn} \
--conditions Field=host-header,Values="${stackDomain}" \
--priority 10 \
--actions Type=forward,TargetGroupArn=${tg1Arn} \
--query Rules[].RuleArn \
--output text); rc 0
read -r -d '' r53Json << EOM
{
"Comment": "CREATE an alias CNAME record for ${albDNSName}",
"Changes": [{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "${stackDomain}",
"Type": "A",
"AliasTarget":{
"HostedZoneId": "${awsRoute53ZoneId}",
"DNSName": "dualstack.${albDNSName}",
"EvaluateTargetHealth": false
}
}
}]
}
EOM
task="Create A Record \e[04mALIAS\e[0m for ${stackDomain}"; et
changeId=$(aws --profile ${env} --region ${region} route53 change-resource-record-sets \
--hosted-zone-id "$zoneId" \
--change-batch "$r53Json" \
--query ChangeInfo.Id \
--output text); rc 0
task="Waiting for DNS propagation"; et
aws --profile ${env} --region ${region} route53 wait resource-record-sets-changed --id ${changeId}; rc 0
id_out r53Record--${stackDomain}.--dualstack.${albDNSName}--${zoneId}
task="Create ECS cluster ${stackResourceName}"; et
clusterArn=$(aws --profile ${env} --region ${region} ecs create-cluster \
--cluster-name ${stackResourceName} \
--query cluster.clusterArn \
--output text); rc 0
id_out ecsArn-${clusterArn}
read -r -d '' ecsTaskJson << EOM
{
"family": "${stackResourceName}",
"networkMode": "awsvpc",
"containerDefinitions": [
{
"name": "${stackResourceName}",
"image": "${ecrRepo}:${ecrImageTag}",
"portMappings": [
{
"containerPort": ${hcPort},
"protocol": "${taskProto}"
}
],
"essential": true,
"healthCheck": {
"command": [ "CMD-SHELL", "curl -f ${hcCurlProto}://localhost:${hcPort}/ || exit 1" ],
"interval": 5,
"timeout": 3,
"retries": 3,
"startPeriod": 10
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "${logGroupName}",
"awslogs-region": "${region}",
"awslogs-stream-prefix": "${stackResourceName}"
}
}
}
],
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "${ecsTaskCPU}",
"memory": "${ecsTaskMem}",
"executionRoleArn": "$ecsTaskExecutionRoleArn"
}
EOM
task="Register Task Definition"; et
ecsTaskDefName=$(aws --profile ${env} --region ${region} ecs register-task-definition \
--cli-input-json "$ecsTaskJson" \
--query taskDefinition.containerDefinitions[].name \
--output text); rc 0
id_out ecsTaskDef-${ecsTaskDefName}
ecsTaskDefArn=$(aws --profile ${env} --region ${region} ecs describe-task-definition --task-definition ${stackResourceName} --query taskDefinition.taskDefinitionArn --output text)
read -r -d '' ecsServiceJson << EOM
{
"cluster": "${stackResourceName}",
"serviceName": "${stackResourceName}",
"taskDefinition": "${stackResourceName}",
"loadBalancers": [
{
"targetGroupArn": "${tg1Arn}",
"containerName": "${stackResourceName}",
"containerPort": ${hcPort}
}
],
"launchType": "FARGATE",
"schedulingStrategy": "REPLICA",
"deploymentController": {
"type": "CODE_DEPLOY"
},
"platformVersion": "LATEST",
"networkConfiguration": {
"awsvpcConfiguration": {
"assignPublicIp": "${taskPubIp}",
"securityGroups": [ "${ecsSg}" ],
"subnets": [ ${ecsSubnetsFormat[@]} ]
}
},
"desiredCount": $ecsDesiredTaskCount
}
EOM
task="Create ECS Service"; et
sleep 3
aws --profile ${env} --region ${region} ecs create-service \
--cli-input-json "$ecsServiceJson"; rc 0
task="Create CodeDeploy application ${stackResourceName}"; et
aws --profile ${env} --region ${region} deploy create-application \
--application-name ${stackResourceName} \
--compute-platform ECS
read -r -d '' ecsDeploymentGroupJson << EOM
{
"applicationName": "${stackResourceName}",
"autoRollbackConfiguration": {
"enabled": true,
"events": [ "DEPLOYMENT_FAILURE" ]
},
"blueGreenDeploymentConfiguration": {
"deploymentReadyOption": {
"actionOnTimeout": "CONTINUE_DEPLOYMENT",
"waitTimeInMinutes": 0
},
"terminateBlueInstancesOnDeploymentSuccess": {
"action": "TERMINATE",
"terminationWaitTimeInMinutes": 3
}
},
"deploymentGroupName": "${stackResourceName}",
"deploymentStyle": {
"deploymentOption": "WITH_TRAFFIC_CONTROL",
"deploymentType": "BLUE_GREEN"
},
"loadBalancerInfo": {
"targetGroupPairInfoList": [
{
"targetGroups": [
{
"name": "${stackResourceName}-tg-a"
},
{
"name": "${stackResourceName}-tg-b"
}
],
"prodTrafficRoute": {
"listenerArns": [
"${deploymentGroupListenerArn}"
]
}
}
]
},
"serviceRoleArn": "${ecsCodeDeployRoleArn}",
"ecsServices": [
{
"serviceName": "${stackResourceName}",
"clusterName": "${stackResourceName}"
}
]
}
EOM
task="Create Deployment Group"; et
aws --profile ${env} --region ${region} deploy create-deployment-group --cli-input-json "$ecsDeploymentGroupJson"; rc 0
task="Create appspec.yaml"; et
rm -f fargate/appspec.yaml
cat << EOF > fargate/appspec.yaml
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "${ecsTaskDefArn}"
LoadBalancerInfo:
ContainerName: "${stackResourceName}"
ContainerPort: ${hcPort}
PlatformVersion: "LATEST"
EOF
rc 0
task="Copy appspec.yaml to s3://${s3Bucket}/${s3Prefix}"; et
aws --profile ${env} --region ${region} s3 cp fargate/appspec.yaml s3://${s3Bucket}/${s3Prefix}; rc 0
task="Create Deployment"; et
rm -f fargate/deployment.json
cat << EOF > fargate/deployment.json
{
"applicationName": "${stackResourceName}",
"deploymentGroupName": "${stackResourceName}",
"revision": {
"revisionType": "S3",
"s3Location": {
"bucket": "${s3Bucket}",
"key": "${s3Prefix}appspec.yaml",
"bundleType": "YAML"
}
}
}
EOF
rc 0
task="Deploy"; et
aws --profile ${env} --region ${region} deploy create-deployment \
--cli-input-json file://fargate/deployment.json; rc 0
echo -e "\
\n----- STACK OUTPUTS -----\n\
Stack Identifier: $stackUniquer\n\
Stack Resource Name: $stackResourceName\n\
Resource IDs:"
printf '%s\n' "${resourceIds[@]}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment