Last active
November 29, 2023 19:54
-
-
Save AlexAtkinson/5864dd20a58e40235ae66b4ac3d6713a to your computer and use it in GitHub Desktop.
BASH from the PAST: Provision AWS Fargate cluster
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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