├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Jenkinsfile ├── LICENSE ├── Makefile ├── Makefile.env ├── README.md ├── infrastructure ├── 1-InfraStack.yaml ├── 2-AppStack.yaml ├── TaskDefinition.template.json └── TaskSet.template.json └── src ├── Dockerfile ├── index.py └── requirements.txt /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonOutput 2 | import groovy.json.JsonSlurperClassic 3 | 4 | pipeline { 5 | agent any 6 | parameters { 7 | string(name: 'awsProfile', defaultValue: 'cicd', description: 'The AWS profile name to resolve credentials.') 8 | string(name: 'awsAccountNumber', defaultValue: '', description: 'The AWS account number to use.') 9 | } 10 | environment { 11 | AWS_PROFILE = "${params.awsProfile}" 12 | AWS_ACCOUNT_NUMBER = "${params.awsAccountNumber}" 13 | } 14 | stages { 15 | stage('Build') { 16 | steps { 17 | sh 'make build-image' 18 | } 19 | } 20 | stage('EcrPush') { 21 | steps { 22 | script { 23 | readProperties(file: 'Makefile.env').each { key, value -> env[key] = value } 24 | } 25 | sh '$(aws ecr get-login --no-include-email --registry-ids $AWS_ACCOUNT_NUMBER)' 26 | script { 27 | def PUSH_RESULT = sh ( 28 | script: "make push-image", 29 | returnStdout: true 30 | ).trim() 31 | echo "Push result: ${PUSH_RESULT}" 32 | } 33 | } 34 | } 35 | stage('SetEnvironment'){ 36 | steps { 37 | script { 38 | // This step reloads the env with configured values for account number and region in various values. 39 | readProperties(file: 'Makefile.env').each { key, value -> tv = value.replace("AWS_ACCOUNT_NUMBER", env.AWS_ACCOUNT_NUMBER) 40 | env[key] = tv.replace("REGION", env.REGION) 41 | } 42 | } 43 | } 44 | } 45 | stage('GetPrimaryTaskSet'){ 46 | steps{ 47 | script{ 48 | // Read all the TaskSets(deployments) for the cluster. 49 | def describeClusterResult = sh ( 50 | script: "aws ecs describe-services --services $SERVICE_ARN --cluster $CLUSTER_ARN", 51 | returnStdout: true 52 | ).trim() 53 | def clusterDetails = readJSON(text: describeClusterResult) 54 | def primaryTaskSet = null 55 | clusterDetails.services[0].taskSets.each { a -> 56 | if (a.status == "PRIMARY"){ 57 | primaryTaskSet = a 58 | } 59 | } 60 | echo "The primary TaskSet is: ${primaryTaskSet}" 61 | 62 | // Write the Primary TaskSet to file 63 | def primaryTaskSetFile = env.TEMPLATE_BASE_PATH + '/' + env.PREVIOUS_PRIMARY_TASKSET_FILE 64 | writeJSON(file: primaryTaskSetFile, json: primaryTaskSet, pretty: 2) 65 | } 66 | } 67 | } 68 | stage('RegisterTaskDefinition') { 69 | steps { 70 | sh 'printenv' 71 | script { 72 | def newImage = sh ( 73 | script: "make latest_image", 74 | returnStdout: true 75 | ).trim() 76 | 77 | def templateFile = env.TEMPLATE_BASE_PATH +'/' + TASK_DEF_TEMPLATE 78 | def taskFamily = 'family' 79 | if ( env.NEXT_ENV == 'Green'){ 80 | taskFamily = env.GREEN_TASK_FAMILY_PREFIX 81 | } 82 | else { 83 | taskFamily = env.BLUE_TASK_FAMILY_PREFIX 84 | } 85 | 86 | def taskDefinitionTemplate = readJSON(file: templateFile) 87 | taskDefinitionTemplate.family = taskFamily 88 | taskDefinitionTemplate.taskRoleArn = env.TASK_ROLE_ARN 89 | taskDefinitionTemplate.executionRoleArn = env.EXECUTION_ROLE_ARN 90 | taskDefinitionTemplate.containerDefinitions[0].name = env.APP_NAME 91 | taskDefinitionTemplate.containerDefinitions[0].image = newImage 92 | taskDefinitionTemplate.containerDefinitions[0].portMappings[0].containerPort = env.APP_PORT.toInteger() 93 | taskDefinitionTemplate.containerDefinitions[0].logConfiguration.options.'awslogs-group' = env.LOG_GROUP 94 | taskDefinitionTemplate.containerDefinitions[0].logConfiguration.options.'awslogs-region' = env.REGION 95 | taskDefFile = env.TEMPLATE_BASE_PATH + '/' + env.TASK_DEFINITION_FILE 96 | writeJSON(file: taskDefFile, json: taskDefinitionTemplate) 97 | 98 | def registerTaskDefinitionOutput = sh ( 99 | script: "aws ecs register-task-definition --cli-input-json file://${taskDefFile}", 100 | returnStdout: true 101 | ).trim() 102 | echo "Register Task Def result: ${registerTaskDefinitionOutput}" 103 | 104 | def registerTaskDefOutputFile = env.TEMPLATE_BASE_PATH + '/' + env.REGISTER_TASK_DEF_OUTPUT 105 | writeJSON(file: registerTaskDefOutputFile, json: registerTaskDefinitionOutput, pretty: 2) 106 | } 107 | } 108 | } 109 | stage('CreateTaskSetTemplate') { 110 | steps{ 111 | script{ 112 | def taskFamily = 'family' 113 | def taskSetTemplateFile = env.TEMPLATE_BASE_PATH + '/' + env.TASK_SET_TEMPLATE_FILE 114 | def taskSetFile = env.TEMPLATE_BASE_PATH + '/' + env.TASK_SET_FILE 115 | def createTaskSetOutputFile = env.TEMPLATE_BASE_PATH + '/' + env.CREATE_TASK_SET_OUTPUT 116 | def targetGroupArn = 'tg' 117 | def registerTaskDefOutputFile = env.TEMPLATE_BASE_PATH + '/' + env.REGISTER_TASK_DEF_OUTPUT 118 | 119 | if ( env.NEXT_ENV == 'Green' ){ 120 | taskFamily = env.GREEN_TASK_FAMILY_PREFIX 121 | targetGroupArn = env.GREEN_TARGET_GROUP_ARN 122 | } 123 | else{ 124 | taskFamily = env.BLUE_TASK_FAMILY_PREFIX 125 | targetGroupArn = env.BLUE_TARGET_GROUP_ARN 126 | } 127 | 128 | def registerTaskDefinitionOutput = readJSON(file: registerTaskDefOutputFile) 129 | def taskSetTemplateJson = readJSON(file: taskSetTemplateFile) 130 | 131 | def subnet_array = env.TASK_SUBNETS.split(',') 132 | subnet_array.eachWithIndex { subnet, i -> 133 | taskSetTemplateJson.networkConfiguration.awsvpcConfiguration.subnets[i] = subnet 134 | } 135 | def sg_array = env.TASK_SECURITY_GROUPS.split(',') 136 | sg_array.eachWithIndex { sg, i -> 137 | taskSetTemplateJson.networkConfiguration.awsvpcConfiguration.securityGroups[i] = sg 138 | } 139 | 140 | taskSetTemplateJson.taskDefinition = registerTaskDefinitionOutput.taskDefinition.taskDefinitionArn 141 | taskSetTemplateJson.loadBalancers[0].containerPort = env.APP_PORT.toInteger() 142 | taskSetTemplateJson.loadBalancers[0].targetGroupArn = targetGroupArn 143 | writeJSON(file: taskSetFile, json: taskSetTemplateJson, pretty: 2) 144 | 145 | // Register the task 146 | def createTaskSetOutput = sh ( 147 | script: "aws ecs create-task-set --service $SERVICE_ARN --cluster $CLUSTER_ARN --cli-input-json file://${taskSetFile}", 148 | returnStdout: true 149 | ).trim() 150 | echo "Create Task Set Result: ${createTaskSetOutput}" 151 | 152 | writeJSON(file: createTaskSetOutputFile, json: createTaskSetOutput, pretty: 2) 153 | } 154 | } 155 | } 156 | stage('EnableTestListener'){ 157 | steps{ 158 | script{ 159 | def blueTG = null 160 | def greenTG = null 161 | if ( env.NEXT_ENV == 'Green' ){ 162 | blueTG = ["Weight": 0, "TargetGroupArn": env.BLUE_TARGET_GROUP_ARN] 163 | greenTG = ["Weight": 100, "TargetGroupArn": env.GREEN_TARGET_GROUP_ARN] 164 | } 165 | else{ 166 | blueTG = ["Weight": 100, "TargetGroupArn": env.BLUE_TARGET_GROUP_ARN] 167 | greenTG = ["Weight": 0, "TargetGroupArn": env.GREEN_TARGET_GROUP_ARN] 168 | } 169 | def tgs = [blueTG, greenTG] 170 | 171 | 172 | def listenerDefaultActionsTemplate = """ 173 | { 174 | "ListenerArn": "$env.TEST_LISTENER_ARN", 175 | "DefaultActions": [ 176 | { 177 | "Type": "forward", 178 | "ForwardConfig": { 179 | "TargetGroups": ${JsonOutput.prettyPrint(JsonOutput.toJson(tgs))} 180 | } 181 | } 182 | ] 183 | } 184 | """ 185 | def testDefaultActionsFile = env.TEMPLATE_BASE_PATH + '/' + env.TEST_LISTENER_DEFAULT_ACTION_OUTPUT 186 | 187 | def listerDefaultActionJson = new JsonSlurperClassic().parseText(listenerDefaultActionsTemplate) 188 | 189 | writeJSON(file: testDefaultActionsFile, json: listerDefaultActionJson, pretty: 2) 190 | 191 | // Call the api to perform the swap 192 | def modifyTestListenerResult = sh ( 193 | script: "aws elbv2 modify-listener --listener-arn $TEST_LISTENER_ARN --cli-input-json file://${testDefaultActionsFile}", 194 | returnStdout: true 195 | ).trim() 196 | echo "The modify result: ${modifyTestListenerResult}" 197 | } 198 | } 199 | } 200 | stage ('WaitForTestingStage') { 201 | input { 202 | message "Ready to SWAP Live Listener?" 203 | ok "Yes, go ahead." 204 | } 205 | steps{ 206 | echo "Moving on to perform SWAP ..................." 207 | } 208 | } 209 | stage('SwapLive'){ 210 | steps{ 211 | script{ 212 | def liveBlueWeight = null 213 | def liveGreenWeight = null 214 | def testBlueWeight = null 215 | def testGreenWeight = null 216 | if ( env.NEXT_ENV == 'Green' ){ 217 | liveBlueWeight = ["Weight": 0, "TargetGroupArn": env.BLUE_TARGET_GROUP_ARN] 218 | liveGreenWeight = ["Weight": 100, "TargetGroupArn": env.GREEN_TARGET_GROUP_ARN] 219 | testBlueWeight = ["Weight": 100, "TargetGroupArn": env.BLUE_TARGET_GROUP_ARN] 220 | testGreenWeight = ["Weight": 0, "TargetGroupArn": env.GREEN_TARGET_GROUP_ARN] 221 | } 222 | else{ 223 | liveBlueWeight = ["Weight": 100, "TargetGroupArn": env.BLUE_TARGET_GROUP_ARN] 224 | liveGreenWeight = ["Weight": 0, "TargetGroupArn": env.GREEN_TARGET_GROUP_ARN] 225 | testBlueWeight = ["Weight": 0, "TargetGroupArn": env.BLUE_TARGET_GROUP_ARN] 226 | testGreenWeight = ["Weight": 100, "TargetGroupArn": env.GREEN_TARGET_GROUP_ARN] 227 | } 228 | def tgs = [liveBlueWeight, liveGreenWeight] 229 | def test_tgs = [testBlueWeight, testGreenWeight] 230 | 231 | def liveListenerDefaultActionsTemplate = """ 232 | { 233 | "ListenerArn": "$env.LIVE_LISTENER_ARN", 234 | "DefaultActions": [ 235 | { 236 | "Type": "forward", 237 | "ForwardConfig": { 238 | "TargetGroups": ${JsonOutput.prettyPrint(JsonOutput.toJson(tgs))} 239 | } 240 | } 241 | ] 242 | } 243 | """ 244 | def testListenerDefaultActionsTemplate = """ 245 | { 246 | "ListenerArn": "$env.TEST_LISTENER_ARN", 247 | "DefaultActions": [ 248 | { 249 | "Type": "forward", 250 | "ForwardConfig": { 251 | "TargetGroups": ${JsonOutput.prettyPrint(JsonOutput.toJson(test_tgs))} 252 | } 253 | } 254 | ] 255 | } 256 | """ 257 | 258 | // Set live listener to new version 259 | def liveDefaultActionsFile = env.TEMPLATE_BASE_PATH + '/' + env.LIVE_LISTENER_DEFAULT_ACTION_OUTPUT 260 | def liveListerDefaultActionJson = new JsonSlurperClassic().parseText(liveListenerDefaultActionsTemplate) 261 | writeJSON(file: liveDefaultActionsFile, json: liveListerDefaultActionJson, pretty: 2) 262 | 263 | def modifyLiveListenerResult = sh ( 264 | script: "aws elbv2 modify-listener --listener-arn $LIVE_LISTENER_ARN --cli-input-json file://${liveDefaultActionsFile}", 265 | returnStdout: true 266 | ).trim() 267 | echo "The modify result: ${modifyLiveListenerResult}" 268 | 269 | // Set test listener to previous version 270 | def testDefaultActionsFile = env.TEMPLATE_BASE_PATH + '/' + env.TEST_LISTENER_DEFAULT_ACTION_OUTPUT 271 | def testListerDefaultActionJson = new JsonSlurperClassic().parseText(testListenerDefaultActionsTemplate) 272 | writeJSON(file: testDefaultActionsFile, json: testListerDefaultActionJson, pretty: 2) 273 | 274 | def modifyTestListenerResult = sh ( 275 | script: "aws elbv2 modify-listener --listener-arn $TEST_LISTENER_ARN --cli-input-json file://${testDefaultActionsFile}", 276 | returnStdout: true 277 | ).trim() 278 | echo "The modify result: ${modifyTestListenerResult}" 279 | } 280 | } 281 | } 282 | stage('UpdatePrimaryTaskSet'){ 283 | steps{ 284 | script{ 285 | def createTaskSetOutputFile = env.TEMPLATE_BASE_PATH + '/' + env.CREATE_TASK_SET_OUTPUT 286 | def upatePrimaryTaskSetOutputFile = env.TEMPLATE_BASE_PATH + '/' + env.UPDATE_PRIMARY_TASK_SET_OUTPUT 287 | def createTaskSetOutput = readJSON(file: createTaskSetOutputFile) 288 | 289 | def updatePrimaryTaskSetOutput = sh ( 290 | script: "aws ecs update-service-primary-task-set --service $SERVICE_ARN --cluster $CLUSTER_ARN --primary-task-set ${createTaskSetOutput.taskSet.taskSetArn}", 291 | returnStdout: true 292 | ).trim() 293 | echo "Upate Primary TaskSet Result: ${updatePrimaryTaskSetOutput}" 294 | writeJSON(file: upatePrimaryTaskSetOutputFile, json: updatePrimaryTaskSetOutput, pretty: 2) 295 | } 296 | } 297 | } 298 | stage ('WaitForUserToDeletePreviousDeploymentStage') { 299 | input { 300 | message "***CAUTION***: Ready to DELETE previous deployment?" 301 | ok "Yes, go ahead." 302 | } 303 | steps{ 304 | echo "Deleting previous deployment ..................." 305 | } 306 | } 307 | stage('DeleteDeployment'){ 308 | steps{ 309 | script{ 310 | // Read the previous primary TaskSet from file 311 | def primaryTaskSetFile = env.TEMPLATE_BASE_PATH + '/' + env.PREVIOUS_PRIMARY_TASKSET_FILE 312 | def primaryTaskSetJson = readJSON(file: primaryTaskSetFile) 313 | 314 | // Delete the TaskSet(deployment) 315 | def deleteTaskSetOutputFile = env.TEMPLATE_BASE_PATH + '/' + env.DELETE_TASK_SET_OUTPUT 316 | def deleteTaskSetResult = sh ( 317 | script: "aws ecs delete-task-set --cluster $CLUSTER_ARN --service $SERVICE_ARN --task-set ${primaryTaskSetJson.id}", 318 | returnStdout: true 319 | ).trim() 320 | 321 | writeJSON(file: deleteTaskSetOutputFile, json: deleteTaskSetResult, pretty: 2) 322 | echo "Delete TaskSet: ${deleteTaskSetResult}" 323 | 324 | // Deregister old TaskDefinition 325 | def deregisterTaskDefOutputFile = env.TEMPLATE_BASE_PATH + '/' + env.DEREGISTER_TASK_DEF_OUTPUT 326 | def deregisterTaskDefResult = sh ( 327 | script: "aws ecs deregister-task-definition --task-definition ${primaryTaskSetJson.taskDefinition}", 328 | returnStdout: true 329 | ).trim() 330 | 331 | writeJSON(file: deregisterTaskDefOutputFile, json: deregisterTaskDefResult, pretty: 2) 332 | echo "Deregister TaskDefinition: ${deregisterTaskDefResult}" 333 | } 334 | } 335 | } 336 | } 337 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | -include Makefile.env 3 | 4 | SHELL := /bin/bash 5 | .DEFAULT_GOAL := all 6 | 7 | .PHONY: all 8 | ## all: (default) runs build-image 9 | all: build-image 10 | 11 | AWS_PROFILE?= 12 | ROOT_PATH=$(PWD) 13 | APP_DIR=$(ROOT_PATH)/src 14 | TEMP_DIR=$(ROOT_PATH)/temp 15 | 16 | .PHONY: build-image 17 | ## build-image: builds the docker image 18 | build-image: 19 | docker build -t $(APP_NAME):$(GIT_COMMIT) $(APP_DIR) 20 | docker tag $(APP_NAME):$(GIT_COMMIT) $(AWS_ACCOUNT_NUMBER).dkr.ecr.$(REGION).amazonaws.com/$(REPO_NAME):$(GIT_COMMIT) 21 | 22 | .PHONY: push-image 23 | push-image: 24 | @docker push $(AWS_ACCOUNT_NUMBER).dkr.ecr.$(REGION).amazonaws.com/$(REPO_NAME):$(GIT_COMMIT) 25 | 26 | .PHONY: configured-region 27 | configured-region: 28 | @echo $(REGION) 29 | 30 | .PHONY: latest_image 31 | latest_image: 32 | @echo $(AWS_ACCOUNT_NUMBER).dkr.ecr.$(REGION).amazonaws.com/$(REPO_NAME):$(GIT_COMMIT) 33 | -------------------------------------------------------------------------------- /Makefile.env: -------------------------------------------------------------------------------- 1 | NEXT_ENV=Green 2 | APP_NAME=awesome-api 3 | REGION=us-east-1 4 | REPO_NAME=awesome-api-repository 5 | APP_PORT=9000 6 | GREEN_TASK_FAMILY_PREFIX=GreenTaskDefinition 7 | BLUE_TASK_FAMILY_PREFIX=BlueTaskDefinition 8 | LOG_GROUP=BlueGreenAppStack1-ServiceLogGroup-1GMGJHEBYWOX3 9 | TASK_SUBNETS=subnet-032a96cf00eed7dbd,subnet-02649dbcdb178ec56 10 | TASK_SECURITY_GROUPS=sg-0185a55622efb1691 11 | SERVICE_ARN=arn:aws:ecs:REGION:AWS_ACCOUNT_NUMBER:service/AwesomeApiService 12 | CLUSTER_ARN=arn:aws:ecs:REGION:AWS_ACCOUNT_NUMBER:cluster/BlueGreenCluster 13 | TASK_ROLE_ARN=arn:aws:iam::AWS_ACCOUNT_NUMBER:role/BlueGreenAppStack1-TaskRole-1JHTAT6M0TTFU 14 | EXECUTION_ROLE_ARN=arn:aws:iam::AWS_ACCOUNT_NUMBER:role/BlueGreenAppStack1-TaskExecutionRole-WEQXNF9LLPFG 15 | BLUE_TARGET_GROUP_ARN=arn:aws:elasticloadbalancing:REGION:AWS_ACCOUNT_NUMBER:targetgroup/BlueG-BlueS-LVLIGQILG2BQ/d162ce281967f716 16 | GREEN_TARGET_GROUP_ARN=arn:aws:elasticloadbalancing:REGION:AWS_ACCOUNT_NUMBER:targetgroup/BlueG-Green-IFFR6P45HAIM/382da25238e742a6 17 | LIVE_LISTENER_ARN=arn:aws:elasticloadbalancing:REGION:AWS_ACCOUNT_NUMBER:listener/app/BlueG-LoadB-1TEPNOAAGT51R/e06ecd9653b94c25/534bdbba29c5f01e 18 | TEST_LISTENER_ARN=arn:aws:elasticloadbalancing:REGION:AWS_ACCOUNT_NUMBER:listener/app/BlueG-LoadB-1TEPNOAAGT51R/e06ecd9653b94c25/74246654da10bca2 19 | TEMPLATE_BASE_PATH=infrastructure 20 | TASK_DEFINITION_FILE=taskDefinition.json 21 | TASK_DEF_TEMPLATE=TaskDefinition.template.json 22 | REGISTER_TASK_DEF_OUTPUT=registerTaskDefOutput.json 23 | TASK_SET_TEMPLATE_FILE=TaskSet.template.json 24 | TASK_SET_FILE=taskSet.json 25 | CREATE_TASK_SET_OUTPUT=createTaskSetOutput.json 26 | DELETE_TASK_SET_OUTPUT=deleteTaskSetOutput.json 27 | DEREGISTER_TASK_DEF_OUTPUT=deregisterTaskDefOutput.json 28 | UPDATE_PRIMARY_TASK_SET_OUTPUT=updatePrimaryTaskSetOutput.json 29 | LIVE_LISTENER_DEFAULT_ACTION_OUTPUT=liveDefaultActions.json 30 | TEST_LISTENER_DEFAULT_ACTION_OUTPUT=testDefaultActions.json 31 | PREVIOUS_PRIMARY_TASKSET_FILE=previousPrimaryTaskSet.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ecs-bg-external-controller 2 | 3 | Build pipeline and IaC assets for ECS service with blue/green deployment. 4 | 5 | ## Security 6 | 7 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 8 | 9 | ## License 10 | 11 | This library is licensed under the MIT-0 License. See the LICENSE file. -------------------------------------------------------------------------------- /infrastructure/1-InfraStack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Deploy infra required for blue/green deyployment type using EXTERNAL DeploymentController. 3 | 4 | Parameters: 5 | Vpc: 6 | Type: AWS::EC2::VPC::Id 7 | PublicSubnets: 8 | Description: The public subnets. 9 | Type: List 10 | PrivateSubnets: 11 | Description: The private subnets. 12 | Type: List 13 | AppListenPort: 14 | Description: The port the app is listening on. Normally, specified in Dockerfile for a container. 15 | Type: Number 16 | Default: 80 17 | SecondaryAppListenPort: 18 | Description: The port the app is listening on. Normally, specified in Dockerfile for a container. 19 | Type: Number 20 | Default: 9000 21 | LiveLBPort: 22 | Description: The port exposed on LB for Live traffic. This maybe different from the App port. 23 | Type: Number 24 | Default: 8080 25 | TestLBPort: 26 | Description: The port exposed on LB for Test traffic. This maybe different from the App port. 27 | Type: Number 28 | Default: 8081 29 | 30 | Resources: 31 | AwesomeAppRepository: 32 | Type: AWS::ECR::Repository 33 | Properties: 34 | RepositoryName: "awesome-api-repository" 35 | 36 | # ECS resources 37 | EcsCluster: 38 | Type: 'AWS::ECS::Cluster' 39 | Properties: 40 | ClusterName: BlueGreenCluster 41 | ClusterSettings: 42 | - Name: containerInsights 43 | Value: disabled 44 | 45 | # Load balancer resources 46 | LoadBalancer: 47 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 48 | Properties: 49 | Scheme: internet-facing 50 | SecurityGroups: 51 | - !GetAtt LoadBalancerSecurityGroup.GroupId 52 | Subnets: !Ref PublicSubnets 53 | Type: application 54 | 55 | BlueServiceTargetGroup: 56 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 57 | Properties: 58 | HealthCheckIntervalSeconds: 5 59 | HealthCheckPath: / 60 | HealthCheckProtocol: HTTP 61 | HealthyThresholdCount: 2 62 | UnhealthyThresholdCount: 3 63 | HealthCheckTimeoutSeconds: 4 64 | TargetGroupAttributes: 65 | - Key: "deregistration_delay.timeout_seconds" 66 | Value: 5 67 | Port: !Ref AppListenPort 68 | Protocol: HTTP 69 | TargetType: ip 70 | VpcId: !Ref Vpc 71 | 72 | GreenServiceTargetGroup: 73 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 74 | Properties: 75 | HealthCheckIntervalSeconds: 5 76 | HealthCheckPath: / 77 | HealthCheckProtocol: HTTP 78 | HealthyThresholdCount: 2 79 | UnhealthyThresholdCount: 3 80 | HealthCheckTimeoutSeconds: 4 81 | TargetGroupAttributes: 82 | - Key: "deregistration_delay.timeout_seconds" 83 | Value: 5 84 | Port: !Ref AppListenPort 85 | Protocol: HTTP 86 | TargetType: ip 87 | VpcId: !Ref Vpc 88 | 89 | LiveListener: 90 | Type: AWS::ElasticLoadBalancingV2::Listener 91 | Properties: 92 | DefaultActions: 93 | - Type: forward 94 | ForwardConfig: 95 | TargetGroups: 96 | - TargetGroupArn: !Ref BlueServiceTargetGroup 97 | Weight: 100 98 | LoadBalancerArn: !Ref LoadBalancer 99 | Port: !Ref LiveLBPort 100 | Protocol: HTTP 101 | 102 | TestListener: 103 | Type: AWS::ElasticLoadBalancingV2::Listener 104 | Properties: 105 | DefaultActions: 106 | - Type: forward 107 | ForwardConfig: 108 | TargetGroups: 109 | - TargetGroupArn: !Ref GreenServiceTargetGroup 110 | Weight: 100 111 | LoadBalancerArn: !Ref LoadBalancer 112 | Port: !Ref TestLBPort 113 | Protocol: HTTP 114 | 115 | # Security Groups: 116 | # Allow traffic to the load balancer from the internet, 117 | LoadBalancerSecurityGroup: 118 | Type: AWS::EC2::SecurityGroup 119 | Properties: 120 | GroupDescription: "Security group for reInvent Trivia backend load balancer" 121 | SecurityGroupIngress: 122 | - CidrIp: 0.0.0.0/0 123 | Description: Allow from anyone on production traffic port Blue 124 | FromPort: !Ref LiveLBPort 125 | IpProtocol: tcp 126 | ToPort: !Ref LiveLBPort 127 | - CidrIp: 0.0.0.0/0 128 | Description: Allow from anyone on production traffic port Green 129 | FromPort: !Ref TestLBPort 130 | IpProtocol: tcp 131 | ToPort: !Ref TestLBPort 132 | VpcId: !Ref Vpc 133 | 134 | # From the load balancer to the ECS containers. 135 | ServiceSecurityGroup: 136 | Type: AWS::EC2::SecurityGroup 137 | Properties: 138 | GroupDescription: "Security group for Fargate tasks." 139 | VpcId: !Ref Vpc 140 | 141 | LoadBalancerSecurityGroupToServiceSecurityGroupEgress: 142 | Type: AWS::EC2::SecurityGroupEgress 143 | Properties: 144 | Description: Load balancer to target 145 | GroupId: !GetAtt LoadBalancerSecurityGroup.GroupId 146 | DestinationSecurityGroupId: !GetAtt ServiceSecurityGroup.GroupId 147 | IpProtocol: tcp 148 | FromPort: !Ref AppListenPort 149 | ToPort: !Ref AppListenPort 150 | 151 | LoadBalancerSecurityGroupToServiceSecurityGroupEgress2: 152 | Type: AWS::EC2::SecurityGroupEgress 153 | Properties: 154 | Description: Load balancer to target 155 | GroupId: !GetAtt LoadBalancerSecurityGroup.GroupId 156 | DestinationSecurityGroupId: !GetAtt ServiceSecurityGroup.GroupId 157 | IpProtocol: tcp 158 | FromPort: !Ref SecondaryAppListenPort 159 | ToPort: !Ref SecondaryAppListenPort 160 | 161 | LoadBalancerSecurityGroupToServiceSecurityGroupIngress: 162 | Type: AWS::EC2::SecurityGroupIngress 163 | Properties: 164 | Description: Load balancer to target 165 | GroupId: !GetAtt ServiceSecurityGroup.GroupId 166 | SourceSecurityGroupId: !GetAtt LoadBalancerSecurityGroup.GroupId 167 | IpProtocol: tcp 168 | FromPort: !Ref AppListenPort 169 | ToPort: !Ref AppListenPort 170 | 171 | LoadBalancerSecurityGroupToServiceSecurityGroupIngress2: 172 | Type: AWS::EC2::SecurityGroupIngress 173 | Properties: 174 | Description: Load balancer to target 175 | GroupId: !GetAtt ServiceSecurityGroup.GroupId 176 | SourceSecurityGroupId: !GetAtt LoadBalancerSecurityGroup.GroupId 177 | IpProtocol: tcp 178 | FromPort: !Ref SecondaryAppListenPort 179 | ToPort: !Ref SecondaryAppListenPort 180 | 181 | 182 | Outputs: 183 | ServiceURL: 184 | Value: !Join 185 | - "" 186 | - - http:// 187 | - !GetAtt LoadBalancer.DNSName 188 | EcrRepoName: 189 | Description: A name for the ECR repo. 190 | Value: !Ref AwesomeAppRepository 191 | Export: 192 | Name: EcsBGSampleStack:EcrRepoName 193 | EcrRepoArn: 194 | Description: A ARN for the ECR repo. 195 | Value: !GetAtt AwesomeAppRepository.Arn 196 | Export: 197 | Name: EcsBGSampleStack:EcrRepoArn 198 | BlueServiceTargetGroup: 199 | Description: A reference to the blue target group. 200 | Value: !Ref BlueServiceTargetGroup 201 | Export: 202 | Name: EcsBGSampleStack:BlueServiceTargetGroup 203 | GreenServiceTargetGroup: 204 | Description: A reference to the green target group. 205 | Value: !Ref GreenServiceTargetGroup 206 | Export: 207 | Name: EcsBGSampleStack:GreenServiceTargetGroup 208 | EcsCluster: 209 | Description: A reference to the ECS cluster. 210 | Value: !Ref EcsCluster 211 | Export: 212 | Name: EcsBGSampleStack:EcsCluster 213 | ServiceSecurityGroupId: 214 | Description: A reference to the ECS service security group. 215 | Value: !GetAtt ServiceSecurityGroup.GroupId 216 | Export: 217 | Name: EcsBGSampleStack:ServiceSecurityGroupId 218 | StackPublicSubnets: 219 | Description: A reference to the list of subnets for ECS tasks. 220 | Value: !Join 221 | - "," 222 | - !Ref PublicSubnets 223 | Export: 224 | Name: EcsBGSampleStack:StackPublicSubnets 225 | StackPrivateSubnets: 226 | Description: A reference to the list of subnets for ECS tasks. 227 | Value: !Join 228 | - "," 229 | - !Ref PrivateSubnets 230 | Export: 231 | Name: EcsBGSampleStack:StackPrivateSubnets 232 | AppListenPort: 233 | Description: The port the app is listening on. 234 | Value: !Ref AppListenPort 235 | Export: 236 | Name: EcsBGSampleStack:AppListenPort 237 | SecondaryAppListenPort: 238 | Description: The port the app is listening on. 239 | Value: !Ref SecondaryAppListenPort 240 | Export: 241 | Name: EcsBGSampleStack:SecondaryAppListenPort 242 | LiveLBPort: 243 | Description: The port the LB is listening for Live traffic. 244 | Value: !Ref LiveLBPort 245 | TestLBPort: 246 | Description: The port the LB is listening for test traffic. 247 | Value: !Ref TestLBPort 248 | LiveListener: 249 | Description: The Live Listener ARN. 250 | Value: !Ref LiveListener 251 | TestListener: 252 | Description: The Test Listener ARN. 253 | Value: !Ref TestListener 254 | 255 | -------------------------------------------------------------------------------- /infrastructure/2-AppStack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Creates Task Definitions for Blue and Green Tasks. 3 | 4 | Parameters: 5 | BlueImage: 6 | Type: String 7 | Default: nginx 8 | 9 | Resources: 10 | 11 | EcsService: 12 | Type: AWS::ECS::Service 13 | Properties: 14 | Cluster: !ImportValue EcsBGSampleStack:EcsCluster 15 | ServiceName: AwesomeApiService 16 | DesiredCount: 2 17 | DeploymentConfiguration: 18 | MaximumPercent: 200 19 | MinimumHealthyPercent: 100 20 | SchedulingStrategy: REPLICA 21 | DeploymentController: 22 | Type: EXTERNAL 23 | 24 | ServiceLogGroup: 25 | Type: AWS::Logs::LogGroup 26 | 27 | BlueTaskDefinition: 28 | Type: AWS::ECS::TaskDefinition 29 | Properties: 30 | ContainerDefinitions: 31 | - Essential: true 32 | Image: !Ref BlueImage 33 | LogConfiguration: 34 | LogDriver: awslogs 35 | Options: 36 | awslogs-group: !Ref ServiceLogGroup 37 | awslogs-stream-prefix: Service 38 | awslogs-region: us-east-1 39 | Name: awesome-api 40 | PortMappings: 41 | - ContainerPort: !ImportValue EcsBGSampleStack:AppListenPort 42 | Protocol: tcp 43 | Cpu: "256" 44 | ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn 45 | Family: BlueTaskDefinition 46 | Memory: "512" 47 | NetworkMode: awsvpc 48 | RequiresCompatibilities: 49 | - FARGATE 50 | TaskRoleArn: !GetAtt TaskRole.Arn 51 | 52 | # Roles: 53 | TaskRole: 54 | Type: AWS::IAM::Role 55 | Properties: 56 | AssumeRolePolicyDocument: 57 | Statement: 58 | - Action: sts:AssumeRole 59 | Effect: Allow 60 | Principal: 61 | Service: ecs-tasks.amazonaws.com 62 | Version: "2012-10-17" 63 | 64 | TaskExecutionRole: 65 | Type: AWS::IAM::Role 66 | Properties: 67 | AssumeRolePolicyDocument: 68 | Statement: 69 | - Action: sts:AssumeRole 70 | Effect: Allow 71 | Principal: 72 | Service: ecs-tasks.amazonaws.com 73 | Version: "2012-10-17" 74 | 75 | TaskExecutionRolePolicy: 76 | Type: AWS::IAM::Policy 77 | Properties: 78 | PolicyDocument: 79 | Statement: 80 | - Action: 81 | - ecr:GetAuthorizationToken 82 | - ecr:BatchCheckLayerAvailability 83 | - ecr:GetDownloadUrlForLayer 84 | - ecr:GetRepositoryPolicy 85 | - ecr:DescribeRepositories 86 | - ecr:ListImages 87 | - ecr:DescribeImages 88 | - ecr:BatchGetImage 89 | - ecr:GetLifecyclePolicy 90 | - ecr:GetLifecyclePolicyPreview 91 | - ecr:ListTagsForResource 92 | - ecr:DescribeImageScanFindings 93 | Effect: Allow 94 | Resource: "*" 95 | - Action: ecr:GetAuthorizationToken 96 | Effect: Allow 97 | Resource: "*" 98 | - Action: 99 | - logs:CreateLogStream 100 | - logs:PutLogEvents 101 | Effect: Allow 102 | Resource: "*" 103 | Version: "2012-10-17" 104 | PolicyName: !Sub ${AWS::StackName}-ServiceTaskExecutionRolePolicy 105 | Roles: 106 | - !Ref TaskExecutionRole 107 | 108 | BlueTaskSet: 109 | Type: AWS::ECS::TaskSet 110 | Properties: 111 | Cluster: !ImportValue EcsBGSampleStack:EcsCluster 112 | Service: !Ref EcsService 113 | Scale: 114 | Unit: PERCENT 115 | Value: 100 116 | TaskDefinition: !Ref BlueTaskDefinition 117 | LaunchType: FARGATE 118 | LoadBalancers: 119 | - ContainerName: awesome-api 120 | ContainerPort: !ImportValue EcsBGSampleStack:AppListenPort 121 | TargetGroupArn: !ImportValue EcsBGSampleStack:BlueServiceTargetGroup 122 | NetworkConfiguration: 123 | AwsvpcConfiguration: 124 | SecurityGroups: 125 | - !ImportValue EcsBGSampleStack:ServiceSecurityGroupId 126 | Subnets: 127 | Fn::Split: 128 | - "," 129 | - !ImportValue EcsBGSampleStack:StackPrivateSubnets 130 | 131 | PrimaryTaskSet: 132 | Type: AWS::ECS::PrimaryTaskSet 133 | Properties: 134 | Cluster: !ImportValue EcsBGSampleStack:EcsCluster 135 | Service: !Ref EcsService 136 | TaskSetId: !GetAtt BlueTaskSet.Id 137 | 138 | 139 | Outputs: 140 | BlueTaskDefinition: 141 | Description: A reference to the blue task definition. 142 | Value: !Ref BlueTaskDefinition 143 | EcsService: 144 | Description: The ECS service. 145 | Value: !Ref EcsService 146 | BlueTaskSet: 147 | Description: The active stack set. 148 | Value: !Ref BlueTaskSet 149 | PrimaryTaskSet: 150 | Description: The primary stack set id. 151 | Value: !Ref PrimaryTaskSet -------------------------------------------------------------------------------- /infrastructure/TaskDefinition.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "Family Prefix", 3 | "taskRoleArn": "TASK ROLE", 4 | "executionRoleArn": "TASK EXEC ROLE", 5 | "networkMode": "awsvpc", 6 | "containerDefinitions": [ 7 | { 8 | "name": "my app", 9 | "image": "nginx", 10 | "portMappings": [ 11 | { 12 | "containerPort": 80, 13 | "protocol": "tcp" 14 | } 15 | ], 16 | "essential": true, 17 | "logConfiguration": { 18 | "logDriver": "awslogs", 19 | "options": { 20 | "awslogs-group": "Some group", 21 | "awslogs-region": "some region", 22 | "awslogs-stream-prefix": "Service" 23 | }, 24 | "secretOptions": [] 25 | } 26 | } 27 | ], 28 | "requiresCompatibilities": [ 29 | "FARGATE" 30 | ], 31 | "cpu": "256", 32 | "memory": "512", 33 | "inferenceAccelerators": [] 34 | } -------------------------------------------------------------------------------- /infrastructure/TaskSet.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "taskDefinition": "SET TASK DEFINITION", 3 | "launchType": "FARGATE", 4 | "networkConfiguration": { 5 | "awsvpcConfiguration": { 6 | "assignPublicIp": "DISABLED", 7 | "securityGroups": [ 8 | ], 9 | "subnets": [ 10 | ] 11 | } 12 | }, 13 | "loadBalancers": [ 14 | { 15 | "targetGroupArn": "BLUE TG ARN", 16 | "containerName": "awesome-api", 17 | "containerPort": 80 18 | } 19 | ], 20 | "serviceRegistries": [], 21 | "scale": { 22 | "value": 100, 23 | "unit": "PERCENT" 24 | } 25 | } -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | LABEL maintainer="sasquatch" 4 | 5 | COPY . /app 6 | 7 | WORKDIR /app 8 | 9 | RUN pip install -r requirements.txt 10 | 11 | RUN addgroup -S sasquatch 12 | 13 | RUN adduser -S sasquatch -G sasquatch 14 | 15 | ENV APACHE_RUN_USER www-data 16 | 17 | ENV APACHE_RUN_GROUP www-data 18 | 19 | ENV APACHE_LOG_DIR /var/log/apache2 20 | 21 | EXPOSE 9000 22 | 23 | USER sasquatch 24 | 25 | CMD python ./index.py -------------------------------------------------------------------------------- /src/index.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route("/") 6 | def hello(): 7 | return "Greeeeeen it is!!!!" 8 | 9 | @app.route("/healthz") 10 | def healthz(): 11 | return "Aive and Kicking!!!!!" 12 | 13 | if __name__ == "__main__": 14 | app.run(host="0.0.0.0", port=int("9000"), debug=True) 15 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | --------------------------------------------------------------------------------