├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── archive-eks-alb-bg.tar.gz ├── cdk ├── .DS_Store ├── .gitignore ├── README.md ├── bin │ ├── .DS_Store │ └── cdk.ts ├── cdk.json ├── lib │ ├── .DS_Store │ └── cdk-stack.ts ├── package.json ├── test │ ├── .DS_Store │ └── cdk.test.ts └── tsconfig.json ├── cicd ├── Jenkinsfile ├── jenkins-ecs-workshop.json ├── jenkins-ecs-workshop2.json ├── jenkins-ecs-workshop3.json ├── jenkins-jobs-archive.tar.gz └── jenkins-jobs-archive3.tar.gz ├── dockerAssets.d ├── Dockerfile └── entrypoint.sh ├── flask-docker-app ├── .DS_Store ├── Dockerfile ├── app.py ├── k8s │ ├── .DS_Store │ ├── alb-ingress-controller.yaml │ ├── flask-ALB-namespace.yaml │ ├── flask.yaml │ ├── flaskALBBlue.yaml │ ├── flaskALBGreen.yaml │ ├── flaskALBIngress_query.yaml │ ├── flaskALBIngress_query2.yaml │ ├── flaskBlue.yaml │ ├── flaskGreen.yaml │ ├── setup.sh │ └── setup2.sh ├── pyvenv.cfg ├── requirements.txt ├── static │ └── css │ │ ├── bootstrap-responsive.min.css │ │ └── bootstrap.min.css └── templates │ └── hello.html └── images ├── alb-dns.png ├── alb-tg-check1.png ├── alb-tg-check2.png ├── canary-lb.png ├── cfn-kubectl.png ├── eks-bg-1.png ├── eks-bg-2.png ├── eks-canary.png ├── eks-cicd-codebuild.png ├── flask01.png ├── flask02.png ├── stage12-green.png ├── stage34-green.png ├── web-blue-inv.png ├── web-blue.png ├── web-default.png ├── web-green-inv.png └── web-green.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Building CI/CD with Blue/Green and Canary Deployments on EKS using CDK 2 | 3 | In this workshop you'll learn building a CI/CD pipeline (AWS CodePipeline) to develop a web-based application, containerize it, and deploy it on a Amazon EKS cluster. You'll use the blue/green method to deploy application and review the switchover using Application Load Balancer (ALB) Target-groups. You will spawn this infrastructure using AWS Cloud Development Kit (CDK), enabling you to reproduce the environment when needed, in relatively fewer lines of code. 4 | 5 | The hosting infrastructure consists of pods hosted on Blue and Green service on Kubernetes Worker Nodes, being accessed via an Application LoadBalancer. The Blue service represents the production environment accessed using the ALB DNS with http query (group=blue) whereas Green service represents a pre-production / test environment that is accessed using a different http query (group=green). The CodePipeline build stage uses CodeBuild to dockerize the application and post the images to Amazon ECR. In subsequent stages, the image is picked up and deployed on the Green service of the EKS. The Codepipeline workflow is then blocked at the approval stage, allowing the application in Green service to be tested. Once the application is confirmed to be working fine, the user can issue an approval at the Approval Stage and the application is then deployed on to the Blue Service. 6 | 7 | The Blue/Green architecture diagrams are provided below: 8 | 9 | dashboard 10 | dashboard 11 | dashboard 12 | 13 | The CodePipeline would look like the below figure: 14 | 15 | dashboard 16 | dashboard 17 | 18 | The current workshop is based upon this link and the CDK here is extended further to incorporate CodePipeline, Blue/Green Deployment on EKS with ALB. We will also use the weighted target-group to configure B/G Canary Deployment method. Note that currently CodeDeploy does not support deploying on EKS and thus we will instead use CodeBuild to run commands to deploy the Containers on Pods, spawn the EKS Ingress controller and Ingress resource that takes form of ALB. This workshop focuses on providing a simplistic method, though typical deployable model for production environments. Note that blue/green deployments can be achieved using AppMesh, Lambda, DNS based canary deployments too. 19 | 20 | ### Procedure to follow: 21 | 22 | Step1. Cloud9 and commands to run: 23 | 24 | First launch a Cloud9 terminal and prepare it with following commands: 25 | 26 | ```bash 27 | sudo yum install -y jq 28 | export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) 29 | export AWS_REGION=$(curl -s 169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.region') 30 | echo "export ACCOUNT_ID=${ACCOUNT_ID}" | tee -a ~/.bash_profile 31 | echo "export AWS_REGION=${AWS_REGION}" | tee -a ~/.bash_profile 32 | aws configure set default.region ${AWS_REGION} 33 | aws configure get default.region 34 | ``` 35 | Ensure the Cloud9 is assigned a role of an administrator and from Cloud9 -> AWS Settings -> Credentials -> Disable the Temporary Credentials 36 | Now install kubectl package: 37 | 38 | ```bash 39 | curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.17.0/bin/linux/amd64/kubectl 40 | chmod +x ./kubectl 41 | sudo mv ./kubectl /usr/local/bin/kubectl 42 | kubectl help 43 | ``` 44 | Prepare CDK prerequisite: 45 | 46 | ```bash 47 | sudo yum install -y npm 48 | npm install -g aws-cdk@1.124.0 --force 49 | npm install -g typescript@latest 50 | ``` 51 | Run git clone on this repository from Cloud9: 52 | 53 | ```bash 54 | git clone https://github.com/aws-samples/amazon-eks-cdk-blue-green-cicd.git amazon-eks-cicd-codebuild-eks-alb-bg 55 | ``` 56 | 57 | Once cloned, run the below commands: 58 | ```bash 59 | cd amazon-eks-cicd-codebuild-eks-alb-bg 60 | ``` 61 | 62 | Note: For this workshop, we are using CDK version 1.30. If using the latest CDK version using "npm install -g aws-cdk" (without a version specification) then you would need to modify the EKS construct to include version number too. 63 | 64 | 65 | ```bash 66 | git init 67 | git add . 68 | git commit -m "Initial Commit" 69 | git status 70 | git log 71 | ``` 72 | Now run the CDK steps as below: 73 | 74 | ```bash 75 | cd cdk 76 | cdk init 77 | npm install 78 | npm run build 79 | cdk ls 80 | ``` 81 | Ensure the output is CdkStackEksALBBg 82 | 83 | ```bash 84 | cdk synth 85 | cdk bootstrap aws://$ACCOUNT_ID/$AWS_REGION 86 | cdk deploy 87 | ``` 88 | You may be asked to confirm the creation of the roles and authorization before the CloudFormation is executed, for which, you can respond with a “Y”. 89 | 90 | The infrastructure will take some time to be created, please wait until you see the Output of CloudFormation printed on the terminal. Until then, take time to review the CDK code in the below file: cdk/lib/cdk-stack.ts 91 | 92 | You may also check and compare the CloudFormation Template created from this CDK stack: 93 | cdk/cdk.out/CdkStackEksALBBg.template.json 94 | 95 | 96 | Step2: Configure EKS environment: 97 | 98 | Once the cdk is deployed successfully, go to the CloudFormation Service, select the CdkStackALBEksBg stack and go to the outputs section to copy the value from the field "ClusterConfigCommand". 99 | 100 | dashboard 101 | 102 | Then paste this output into Cloud9 terminal to configure the EKS context. 103 | Ensure you can see 2 nodes listed in the output of : 104 | ```bash 105 | kubectl get nodes 106 | ``` 107 | 108 | Now configure the EKS cluster with the deployment, service and ingress resource as ALB using following set of commands: 109 | 110 | ```bash 111 | cd ../flask-docker-app/k8s 112 | ls setup.sh 113 | chmod +x setup.sh 114 | chmod +x setup2.sh 115 | INSTANCE_ROLE=$(aws cloudformation describe-stack-resources --stack-name CdkStackALBEksBg | jq .StackResources[].PhysicalResourceId | grep CdkStackALBEksBg-ClusterDefaultCapacityInstanceRol | tr -d '["\r\n]') 116 | CLUSTER_NAME=$(aws cloudformation describe-stack-resources --stack-name CdkStackALBEksBg | jq '.StackResources[] | select(.ResourceType=="Custom::AWSCDK-EKS-Cluster").PhysicalResourceId' | tr -d '["\r\n]') 117 | echo "INSTANCE_ROLE = " $INSTANCE_ROLE 118 | echo "CLUSTER_NAME = " $CLUSTER_NAME 119 | ``` 120 | 121 | Note: Before proceeding further, confirm to see that both the variables $INSTANCE_ROLE and $CLUSTER_NAME have values populated. IF not, please bring it to the attention of the workshop owner, possibly the IAM role naming convention may have changed with the version. 122 | Also, after EKS version 1.16 onwards, the k8 deploy API's using apps/v1beta1 is deprecated to apps/v1. The update has been made into the yaml files, however, if you are using an older version of EKS, you may need to modify this back. 123 | 124 | ```bash 125 | ./setup2.sh $AWS_REGION $INSTANCE_ROLE $CLUSTER_NAME 126 | ``` 127 | 128 | Step3: Modify the ALB Security Group: 129 | 130 | Modify the Security Group (ControlPlaneSecurityGroup) for the newly spawned Application Load Balancer to add an incoming rule to allow http port 80 for the 0.0.0.0/0. 131 | Services -> EC2 -> Load Balancer -> Select the latest created ALB -> Click Description Tab -> Scroll down to locate the Security Group Edit this security group to add a new rule with following parameters: http, 80, 0.0.0.0/0 132 | 133 | Additionally, from EKS version 1.17 onwards, you would also need to change the security-group for Worker Nodes Data Plane (InstanceSecurityGroup) by adding an incoming rule to allow http port 80 for the ControlPlaneSecurityGroup (ALB). 134 | 135 | Now, check the newly created LoadBalancer and review the listener routing rules: Services -> EC2 -> Load Balancer -> Select the latest created ALB -> Click Listeners Tab -> View/Edit Rules You would see the below settings shown: 136 | 137 | Check the Load Balancer Target-groups and ensure the healthy hosts have registered and health check is consistently passing as shown below: 138 | 139 | dashboard 140 | 141 | Check the healthy hosts count graph to ensure the hosts, containers are stable: 142 | 143 | dashboard 144 | 145 | Step4: Upload the Application to CodeCommit repo: 146 | 147 | Now that the ALB is setup, complete the last part of the configuration by uploading the code to CodeCommit repository: 148 | 149 | ```bash 150 | cd ../.. 151 | pwd => confirm your current directory is amazon-eks-cicd-codebuild-eks-alb-bg 152 | git add flask-docker-app/k8s/alb-ingress-controller.yaml 153 | git add flask-docker-app/k8s/flaskALBIngress_query.yaml 154 | git add flask-docker-app/k8s/flaskALBIngress_query2.yaml 155 | git add flask-docker-app/k8s/iam-policy.json 156 | git commit -m "Updated files" 157 | git remote add codecommit https://git-codecommit.$AWS_REGION.amazonaws.com/v1/repos/CdkStackALBEksBg-repo 158 | git push -u codecommit master 159 | ``` 160 | 161 | This will push the last commit we carried out in our preparation section, which in turn will trigger the CodePipeline. 162 | 163 | ### Review the Infrastructure: 164 | 165 | Collect the DNS Name from the Load Balancer and access the homepage: 166 | 167 | dashboard 168 | 169 | Once the Application is pushed to the Repository, the CodePipeline will be triggered and CodeBuild will run the set of commands to dockerize the application and push it to the Amazon ECR repository. CodeBuild will in turn run the kubectl commands to create the Blue and Green services on the EKS clusters, if it does not exist. For the first time, it will pull the Flask demo Application from the Dockerhub and deploy it on Blue as well as Green service. It should say "Your Flask application is now running on a container in Amazon Web Services" as shown below: 170 | 171 | dashboard 172 | dashboard 173 | 174 | 175 | Go to Services -> CodePipeline -> Pipelines -> CdkStackEksALBBg-[unique-string] 176 | Review the Stages and the CodeBuild Projects to understand the implementation. 177 | Once the Application is deployed on the Green service, access it as mentioned above: http://ALB-DNS-name/?group=green. 178 | 179 | It is important to note that container exposes port 5000, whereas service exposes port 80 (for blue-service) OR 8080 (for green-service) which in turn is mapped to local host port on the EC2 worker node instance. 180 | 181 | After testing is completed, go to the Approval Stage and Click Approve. This will trigger the CodePipeline to execute the next stage to run the "Swap and Deploy" stage where it swaps the mapping of target-group to the blue / green service. 182 | 183 | 184 | dashboard 185 | dashboard 186 | 187 | 188 | ### Configuring for Canary Deployments: 189 | 190 | Configure your ALB for Canary based deployments using below commands from your Cloud9 terminal: 191 | 192 | ```bash 193 | cd /home/ec2-user/environment/amazon-eks-cicd-codebuild-eks-alb-bg/flask-docker-app/k8s 194 | kubectl apply -f flaskALBIngress_query2.yaml 195 | ``` 196 | 197 | dashboard 198 | 199 | To bring the ALB config to the non-canary configuration, run the below commands: 200 | 201 | ```bash 202 | cd /home/ec2-user/environment/amazon-eks-cicd-codebuild-eks-alb-bg/flask-docker-app/k8s 203 | kubectl apply -f flaskALBIngress_query.yaml 204 | ``` 205 | 206 | ### Cleanup 207 | 208 | (a) Remove the EKS Services: 209 | First connect to the kubernetes cluster using command published as "ConfigCommand" under CloudFormation output. Run "kubectl get svc" to check if you can see the Blue and Green services. Then run the below commands to delete the services. 210 | 211 | ```bash 212 | kubectl delete svc/flask-svc-alb-blue svc/flask-svc-alb-green -n flask-alb 213 | kubectl delete deploy/flask-deploy-alb-blue deploy/flask-deploy-alb-green -n flask-alb 214 | kubectl delete ingress alb-ingress -n flask-alb 215 | kubectl delete deploy alb-ingress-controller -n kube-system 216 | ``` 217 | 218 | (b) Destroy the CDK Stack: 219 | Ensure you are in the cdk directory and then run the below command: 220 | 221 | ```bash 222 | cdk destroy 223 | ``` 224 | 225 | (c ) Remove the individual created policies: 226 | Access IAM Service, then access Roles, then select the Worker Node Instance role (search by CdkStackALBEksBg-ClusterDefaultCapacityInstanceRol) and then remove the inline elb-policy 227 | Access IAM Service, then access Policies, then select “alb-ingress-controller” managed policy created for kubectl, then Select Policy Actions and Delete 228 | 229 | ### Conclusion: 230 | 231 | We built the CICD Pipeline using CDK to containerize and deploy a Python Flask based application using the Blue/Green Deployment method on Amazon EKS. We made the code change and saw it propagated through the CICD pipeline and deploy on Blue/Green service of EKS. We also configured and tested B/G Canary Deployment method. 232 | 233 | Hope you enjoyed the workshop! 234 | 235 | 236 | ## License 237 | 238 | This library is licensed under the MIT-0 License. See the LICENSE file. 239 | -------------------------------------------------------------------------------- /archive-eks-alb-bg.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/archive-eks-alb-bg.tar.gz -------------------------------------------------------------------------------- /cdk/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/cdk/.DS_Store -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | # Useful commands 2 | 3 | * `npm run build` compile typescript to js 4 | * `npm run watch` watch for changes and compile 5 | * `npm run test` perform the jest unit tests 6 | * `cdk deploy` deploy this stack to your default AWS account/region 7 | * `cdk diff` compare deployed stack with current state 8 | * `cdk synth` emits the synthesized CloudFormation template 9 | -------------------------------------------------------------------------------- /cdk/bin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/cdk/bin/.DS_Store -------------------------------------------------------------------------------- /cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import cdk = require('@aws-cdk/core'); 4 | import { CdkStackALBEksBg } from '../lib/cdk-stack'; 5 | 6 | const app = new cdk.App(); 7 | 8 | const env = { 9 | region: app.node.tryGetContext('region') || process.env.CDK_INTEG_REGION || process.env.CDK_DEFAULT_REGION, 10 | account: app.node.tryGetContext('account') || process.env.CDK_INTEG_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT 11 | }; 12 | 13 | 14 | new CdkStackALBEksBg(app, 'CdkStackALBEksBg', { env }); 15 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/cdk.ts" 3 | } 4 | -------------------------------------------------------------------------------- /cdk/lib/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/cdk/lib/.DS_Store -------------------------------------------------------------------------------- /cdk/lib/cdk-stack.ts: -------------------------------------------------------------------------------- 1 | 2 | import cdk = require('@aws-cdk/core'); 3 | import ec2 = require('@aws-cdk/aws-ec2'); 4 | import ecr = require('@aws-cdk/aws-ecr'); 5 | import eks = require('@aws-cdk/aws-eks'); 6 | import iam = require('@aws-cdk/aws-iam'); 7 | import codebuild = require('@aws-cdk/aws-codebuild'); 8 | import codecommit = require('@aws-cdk/aws-codecommit'); 9 | import targets = require('@aws-cdk/aws-events-targets'); 10 | import codepipeline = require('@aws-cdk/aws-codepipeline'); 11 | import codepipeline_actions = require('@aws-cdk/aws-codepipeline-actions'); 12 | 13 | 14 | 15 | export class CdkStackALBEksBg extends cdk.Stack { 16 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 17 | super(scope, id, props); 18 | 19 | /** 20 | * Create a new VPC with single NAT Gateway 21 | */ 22 | const vpc = new ec2.Vpc(this, 'NewVPC', { 23 | cidr: '10.0.0.0/16', 24 | natGateways: 1 25 | }); 26 | 27 | const clusterAdmin = new iam.Role(this, 'AdminRole', { 28 | assumedBy: new iam.AccountRootPrincipal() 29 | }); 30 | 31 | const controlPlaneSecurityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { 32 | vpc, 33 | allowAllOutbound: true 34 | }); 35 | 36 | controlPlaneSecurityGroup.addIngressRule( 37 | ec2.Peer.anyIpv4(), 38 | ec2.Port.tcp(80), 39 | "Allow all inbound traffic by default", 40 | ); 41 | 42 | const cluster = new eks.Cluster(this, 'Cluster', { 43 | version: eks.KubernetesVersion.V1_21, 44 | securityGroup: controlPlaneSecurityGroup, 45 | vpc, 46 | defaultCapacity: 2, 47 | mastersRole: clusterAdmin, 48 | outputClusterName: true, 49 | }); 50 | 51 | const ecrRepo = new ecr.Repository(this, 'EcrRepo'); 52 | 53 | const repository = new codecommit.Repository(this, 'CodeCommitRepo', { 54 | repositoryName: `${this.stackName}-repo` 55 | }); 56 | 57 | 58 | 59 | // CODEBUILD - project 60 | const project = new codebuild.Project(this, 'MyProject', { 61 | projectName: `${this.stackName}`, 62 | source: codebuild.Source.codeCommit({ repository }), 63 | environment: { 64 | buildImage: codebuild.LinuxBuildImage.fromAsset(this, 'CustomImage', { 65 | directory: '../dockerAssets.d', 66 | }), 67 | privileged: true 68 | }, 69 | environmentVariables: { 70 | 'CLUSTER_NAME': { 71 | value: `${cluster.clusterName}` 72 | }, 73 | 'ECR_REPO_URI': { 74 | value: `${ecrRepo.repositoryUri}` 75 | } 76 | }, 77 | buildSpec: codebuild.BuildSpec.fromObject({ 78 | version: "0.2", 79 | phases: { 80 | pre_build: { 81 | commands: [ 82 | 'env', 83 | 'export TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION}', 84 | 'export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output=text)', 85 | '/usr/local/bin/entrypoint.sh', 86 | 'echo Logging in to Amazon ECR', 87 | 'aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com' 88 | ] 89 | }, 90 | build: { 91 | commands: [ 92 | 'cd flask-docker-app', 93 | `docker build -t $ECR_REPO_URI:$TAG .`, 94 | 'docker push $ECR_REPO_URI:$TAG' 95 | ] 96 | }, 97 | post_build: { 98 | commands: [ 99 | 'kubectl get nodes -n flask-alb', 100 | 'kubectl get deploy -n flask-alb', 101 | 'kubectl get svc -n flask-alb', 102 | "isDeployed=$(kubectl get deploy -n flask-alb -o json | jq '.items[0]')", 103 | "deploy8080=$(kubectl get svc -n flask-alb -o wide | grep 8080: | tr ' ' '\n' | grep app= | sed 's/app=//g')", 104 | "echo $isDeployed $deploy8080", 105 | "if [[ \"$isDeployed\" == \"null\" ]]; then kubectl apply -f k8s/flaskALBBlue.yaml && kubectl apply -f k8s/flaskALBGreen.yaml; else kubectl set image deployment/$deploy8080 -n flask-alb flask=$ECR_REPO_URI:$TAG; fi", 106 | 'kubectl get deploy -n flask-alb', 107 | 'kubectl get svc -n flask-alb' 108 | ] 109 | } 110 | } 111 | }) 112 | }) 113 | 114 | 115 | 116 | 117 | // CODEBUILD - project2 118 | const project2 = new codebuild.Project(this, 'MyProject2', { 119 | projectName: `${this.stackName}2`, 120 | source: codebuild.Source.codeCommit({ repository }), 121 | environment: { 122 | buildImage: codebuild.LinuxBuildImage.fromAsset(this, 'CustomImage2', { 123 | directory: '../dockerAssets.d', 124 | }), 125 | privileged: true 126 | }, 127 | environmentVariables: { 128 | 'CLUSTER_NAME': { 129 | value: `${cluster.clusterName}` 130 | }, 131 | 'ECR_REPO_URI': { 132 | value: `${ecrRepo.repositoryUri}` 133 | } 134 | }, 135 | buildSpec: codebuild.BuildSpec.fromObject({ 136 | version: "0.2", 137 | phases: { 138 | pre_build: { 139 | commands: [ 140 | 'env', 141 | 'export TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION}', 142 | '/usr/local/bin/entrypoint.sh' 143 | ] 144 | }, 145 | build: { 146 | commands: [ 147 | 'cd flask-docker-app', 148 | 'echo "Dummy Action"' 149 | ] 150 | }, 151 | post_build: { 152 | commands: [ 153 | 'kubectl get nodes -n flask-alb', 154 | 'kubectl get deploy -n flask-alb', 155 | 'kubectl get svc -n flask-alb', 156 | "deploy8080=$(kubectl get svc -n flask-alb -o wide | grep ' 8080:' | tr ' ' '\n' | grep app= | sed 's/app=//g')", 157 | "deploy80=$(kubectl get svc -n flask-alb -o wide | grep ' 80:' | tr ' ' '\n' | grep app= | sed 's/app=//g')", 158 | "echo $deploy80 $deploy8080", 159 | "kubectl patch svc flask-svc-alb-blue -n flask-alb -p '{\"spec\":{\"selector\": {\"app\": \"'$deploy8080'\"}}}'", 160 | "kubectl patch svc flask-svc-alb-green -n flask-alb -p '{\"spec\":{\"selector\": {\"app\": \"'$deploy80'\"}}}'", 161 | 'kubectl get deploy -n flask-alb', 162 | 'kubectl get svc -n flask-alb' 163 | ] 164 | } 165 | } 166 | }) 167 | }) 168 | 169 | 170 | 171 | 172 | 173 | // PIPELINE 174 | 175 | const sourceOutput = new codepipeline.Artifact(); 176 | 177 | const sourceAction = new codepipeline_actions.CodeCommitSourceAction({ 178 | actionName: 'CodeCommit', 179 | repository, 180 | output: sourceOutput, 181 | }); 182 | 183 | const buildAction = new codepipeline_actions.CodeBuildAction({ 184 | actionName: 'CodeBuild', 185 | project: project, 186 | input: sourceOutput, 187 | outputs: [new codepipeline.Artifact()], // optional 188 | }); 189 | 190 | 191 | const buildAction2 = new codepipeline_actions.CodeBuildAction({ 192 | actionName: 'CodeBuild', 193 | project: project2, 194 | input: sourceOutput, 195 | }); 196 | 197 | 198 | const manualApprovalAction = new codepipeline_actions.ManualApprovalAction({ 199 | actionName: 'Approve', 200 | }); 201 | 202 | 203 | 204 | new codepipeline.Pipeline(this, 'MyPipeline', { 205 | stages: [ 206 | { 207 | stageName: 'Source', 208 | actions: [sourceAction], 209 | }, 210 | { 211 | stageName: 'BuildAndDeploy', 212 | actions: [buildAction], 213 | }, 214 | { 215 | stageName: 'ApproveSwapBG', 216 | actions: [manualApprovalAction], 217 | }, 218 | { 219 | stageName: 'SwapBG', 220 | actions: [buildAction2], 221 | }, 222 | ], 223 | }); 224 | 225 | 226 | repository.onCommit('OnCommit', { 227 | target: new targets.CodeBuildProject(project) 228 | }); 229 | 230 | ecrRepo.grantPullPush(project.role!) 231 | cluster.awsAuth.addMastersRole(project.role!) 232 | project.addToRolePolicy(new iam.PolicyStatement({ 233 | actions: ['eks:DescribeCluster'], 234 | resources: [`${cluster.clusterArn}`], 235 | })) 236 | 237 | 238 | ecrRepo.grantPullPush(project2.role!) 239 | cluster.awsAuth.addMastersRole(project2.role!) 240 | project2.addToRolePolicy(new iam.PolicyStatement({ 241 | actions: ['eks:DescribeCluster'], 242 | resources: [`${cluster.clusterArn}`], 243 | })) 244 | 245 | 246 | new cdk.CfnOutput(this, 'CodeCommitRepoName', { value: `${repository.repositoryName}` }) 247 | new cdk.CfnOutput(this, 'CodeCommitRepoArn', { value: `${repository.repositoryArn}` }) 248 | new cdk.CfnOutput(this, 'CodeCommitCloneUrlSsh', { value: `${repository.repositoryCloneUrlSsh}` }) 249 | new cdk.CfnOutput(this, 'CodeCommitCloneUrlHttp', { value: `${repository.repositoryCloneUrlHttp}` }) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@aws-cdk/assert": "1.134.0", 15 | "@types/jest": "27.0.3", 16 | "jest": "27.3.1", 17 | "ts-jest": "27.0.7", 18 | "aws-cdk": "1.134.0", 19 | "ts-node": "10.4.0", 20 | "typescript": "4.5.2" 21 | }, 22 | "dependencies": { 23 | "@aws-cdk/aws-codebuild": "1.134.0", 24 | "@aws-cdk/aws-ec2": "1.134.0", 25 | "@aws-cdk/aws-ecr": "1.134.0", 26 | "@aws-cdk/aws-ecs": "1.134.0", 27 | "@aws-cdk/aws-eks": "1.134.0", 28 | "@aws-cdk/aws-events-targets": "1.134.0", 29 | "@aws-cdk/core": "1.134.0", 30 | "@aws-cdk/aws-codepipeline": "1.134.0", 31 | "@aws-cdk/aws-codepipeline-actions": "1.134.0", 32 | "@types/node": "16.11.10", 33 | "source-map-support": "0.5.21" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cdk/test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/cdk/test/.DS_Store -------------------------------------------------------------------------------- /cdk/test/cdk.test.ts: -------------------------------------------------------------------------------- 1 | import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; 2 | import cdk = require('@aws-cdk/core'); 3 | import Cdk = require('../lib/cdk-stack'); 4 | 5 | test('Empty Stack', () => { 6 | const app = new cdk.App(); 7 | // WHEN 8 | const stack = new Cdk.CdkStackALBEksBg(app, 'MyTestStack'); 9 | // THEN 10 | expectCDK(stack).to(matchTemplate({ 11 | "Resources": {} 12 | }, MatchStyle.EXACT)) 13 | }); 14 | -------------------------------------------------------------------------------- /cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2016", "es2017.object", "es2017.string"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization":false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /cicd/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | label 'master' 4 | } 5 | stages { 6 | stage('Pre-Build Stage') { 7 | steps { 8 | withCredentials([[ $class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'e4e458ba-8178-4cc7-af47-cbd9046e76ce', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { 9 | sh ''' 10 | echo "Setting Environment Variables Stage" 11 | export AWS_REGION=${JENKINS_AWS_REGION} 12 | aws configure set region ${AWS_REGION} 13 | export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) 14 | ecrRepo=$(aws cloudformation --region ${AWS_REGION} describe-stack-resources --stack-name CdkStackALBEksBg | jq \'.StackResources[]|select(.ResourceType == "AWS::ECR::Repository").PhysicalResourceId\' | tr -d \'"\') 15 | preEcrRepo="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/" 16 | export ECR_REPO_URI=$preEcrRepo$ecrRepo 17 | echo $ECR_REPO_URI 18 | export kubectlcmd=$(aws cloudformation --region ${AWS_REGION} describe-stacks --stack-name CdkStackALBEksBg | jq \'.Stacks[].Outputs[1].OutputValue\' | tr -d \'"\') 19 | kubeClusterName=$(aws cloudformation --region ${AWS_REGION} describe-stacks --stack-name CdkStackALBEksBg | jq \'.Stacks[].Outputs[0].OutputValue\' | tr -d \'"\') 20 | export CLUSTER_NAME=$kubeClusterName 21 | export ECR_REPO_URI=$ECR_REPO_URI 22 | CODEBUILD_RESOLVED_SOURCE_VERSION=01234abcdefgh 23 | export TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION} 24 | 25 | set > ~/.vars 26 | cat ~/.vars 27 | ''' 28 | } 29 | } 30 | } 31 | 32 | stage ('Build & Containerize Stage') { 33 | steps { 34 | withCredentials([[ $class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'e4e458ba-8178-4cc7-af47-cbd9046e76ce', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { 35 | sh label: '', script: '''echo "===================" 36 | echo "Build Stage" 37 | echo "===================" 38 | 39 | echo "++++++++++++++++" 40 | source ~/.vars 41 | eval $kubectlcmd 42 | aws configure set region ${AWS_REGION} 43 | echo "++++++++++++++++" 44 | 45 | 46 | pwd 47 | ls -al 48 | cd amazon-eks-cicd-codebuild-eks-alb-bg/flask-docker-app 49 | $(aws ecr get-login --no-include-email) 50 | docker build -t $ECR_REPO_URI:$TAG . 51 | docker push $ECR_REPO_URI:$TAG 52 | ''' 53 | } 54 | } 55 | } 56 | 57 | stage ('Deploy to Green Env Stage') { 58 | steps { 59 | withCredentials([[ $class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'e4e458ba-8178-4cc7-af47-cbd9046e76ce', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { 60 | sh label: '', script: '''echo "===================" 61 | echo "Post-Build Stage" 62 | echo "===================" 63 | 64 | echo "++++++++++++++++" 65 | source ~/.vars 66 | eval $kubectlcmd 67 | aws configure set region ${AWS_REGION} 68 | echo "++++++++++++++++" 69 | 70 | kubectl get nodes -n flask-alb 71 | kubectl get deploy -n flask-alb 72 | kubectl get svc -n flask-alb 73 | isDeployed=$(kubectl get deploy -n flask-alb -o json | jq \'.items[0]\') 74 | deploy8080=$(kubectl get svc -n flask-alb -o wide | grep 8080: | tr \' \' \'\\n\' | grep app= | sed \'s/app=//g\') 75 | 76 | if [[ \\"$isDeployed\\" == \\"null\\" ]]; then kubectl apply -f k8s/flaskALBBlue.yaml && kubectl apply -f k8s/flaskALBGreen.yaml; else kubectl set image deployment/$deploy8080 -n flask-alb flask=$ECR_REPO_URI:$TAG; fi 77 | 78 | kubectl get deploy -n flask-alb 79 | kubectl get svc -n flask-alb 80 | ''' 81 | } 82 | } 83 | } 84 | 85 | stage ('Approval & Swap Blue/Green Env Stage') { 86 | input { 87 | message "Do you want to proceed for production deployment?" 88 | } 89 | steps { 90 | withCredentials([[ $class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'e4e458ba-8178-4cc7-af47-cbd9046e76ce', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { 91 | sh label: '', script: '''echo "===================" 92 | echo "Swap Stage" 93 | echo "===================" 94 | 95 | echo "++++++++++++++++" 96 | source ~/.vars 97 | eval $kubectlcmd 98 | aws configure set region ${AWS_REGION} 99 | echo "++++++++++++++++" 100 | 101 | kubectl get nodes -n flask-alb 102 | kubectl get deploy -n flask-alb 103 | kubectl get svc -n flask-alb 104 | deploy8080=$(kubectl get svc -n flask-alb -o wide | grep ' 8080:' | tr ' ' '\n' | grep app= | sed 's/app=//g') 105 | deploy80=$(kubectl get svc -n flask-alb -o wide | grep ' 80:' | tr ' ' '\n' | grep app= | sed 's/app=//g') 106 | echo $deploy80 $deploy8080 107 | kubectl patch svc flask-svc-alb-blue -n flask-alb -p '{\"spec\":{\"selector\": {\"app\": \"'$deploy8080'\"}}}' 108 | kubectl patch svc flask-svc-alb-green -n flask-alb -p '{\"spec\":{\"selector\": {\"app\": \"'$deploy80'\"}}}' 109 | kubectl get deploy -n flask-alb 110 | kubectl get svc -n flask-alb 111 | ''' 112 | } 113 | } 114 | } 115 | 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /cicd/jenkins-ecs-workshop.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Cloudformation stack for Jenkins ECS", 4 | "Parameters": { 5 | "KeyName": { 6 | "Description": "Optional: the EC2 Key Pair to allow SSH access to the instances", 7 | "Type": "String", 8 | "Default": "" 9 | }, 10 | "AllowedIPRange": { 11 | "Description": "The public IP address range that can be used to connect to the instances", 12 | "Type": "String", 13 | "MinLength": "9", 14 | "MaxLength": "18", 15 | "Default": "0.0.0.0/0", 16 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 17 | "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." 18 | }, 19 | "VPCIPRange": { 20 | "Description": "The private IP address range for allocating IPs within the VPC.", 21 | "Type": "String", 22 | "MinLength": "9", 23 | "MaxLength": "18", 24 | "Default": "10.0.0.0/16", 25 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 26 | "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." 27 | }, 28 | "InstanceType": { 29 | "Description": "EC2 instance type", 30 | "Type": "String", 31 | "Default": "t2.medium", 32 | "AllowedValues": [ 33 | "t2.micro", 34 | "t2.small", 35 | "t2.medium" 36 | ], 37 | "ConstraintDescription": "Must be a valid EC2 instance type." 38 | }, 39 | "DockerImage": { 40 | "Description": "The docker image to use for Jenkins", 41 | "Type": "String", 42 | "Default": "nikunjv/jenkins:v4" 43 | } 44 | }, 45 | "Mappings": { 46 | "RegionAmazonECSOptimizedAMIMapping": { 47 | "us-east-2": { 48 | "AMI": "ami-0c0415cdff14e2a4a" 49 | }, 50 | "us-east-1": { 51 | "AMI": "ami-098616968d61e549e" 52 | }, 53 | "us-west-2": { 54 | "AMI": "ami-014a2e30da708ee8b" 55 | }, 56 | "us-west-1": { 57 | "AMI": "ami-514e6431" 58 | }, 59 | "eu-west-2": { 60 | "AMI": "ami-0a85946e" 61 | }, 62 | "eu-west-1": { 63 | "AMI": "ami-bd7e8dc4" 64 | }, 65 | "eu-central-1": { 66 | "AMI": "ami-f15ff69e" 67 | }, 68 | "ap-northeast-1": { 69 | "AMI": "ami-ab5ea9cd" 70 | }, 71 | "ap-southeast-2": { 72 | "AMI": "ami-c3233ba0" 73 | }, 74 | "ap-southeast-1": { 75 | "AMI": "ami-ae0b91cd" 76 | }, 77 | "ca-central-1": { 78 | "AMI": "ami-32bb0556" 79 | } 80 | } 81 | }, 82 | "Conditions": { 83 | "HasKeyName": {"Fn::Not": [{"Fn::Equals": ["", {"Ref": "KeyName"}]}]} 84 | }, 85 | "Resources": { 86 | "VPC": { 87 | "Type": "AWS::EC2::VPC", 88 | "Properties": { 89 | "EnableDnsSupport": "true", 90 | "EnableDnsHostnames": "true", 91 | "CidrBlock": {"Ref": "VPCIPRange"}, 92 | "Tags": [ 93 | { 94 | "Key": "Application", 95 | "Value": { 96 | "Ref": "AWS::StackId" 97 | } 98 | } 99 | ] 100 | } 101 | }, 102 | "InternetGateway": { 103 | "Type": "AWS::EC2::InternetGateway", 104 | "Properties": { 105 | "Tags": [ 106 | { 107 | "Key": "Application", 108 | "Value": { 109 | "Ref": "AWS::StackName" 110 | } 111 | }, 112 | { 113 | "Key": "Network", 114 | "Value": "Public" 115 | } 116 | ] 117 | } 118 | }, 119 | "GatewayToInternet": { 120 | "Type": "AWS::EC2::VPCGatewayAttachment", 121 | "Properties": { 122 | "VpcId": { 123 | "Ref": "VPC" 124 | }, 125 | "InternetGatewayId": { 126 | "Ref": "InternetGateway" 127 | } 128 | } 129 | }, 130 | "RouteTable": { 131 | "Type": "AWS::EC2::RouteTable", 132 | "Properties": { 133 | "VpcId": { 134 | "Ref": "VPC" 135 | } 136 | } 137 | }, 138 | "SubnetRouteTableAssoc": { 139 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 140 | "Properties": { 141 | "RouteTableId": { 142 | "Ref": "RouteTable" 143 | }, 144 | "SubnetId": { 145 | "Ref": "Subnet" 146 | } 147 | } 148 | }, 149 | "InternetGatewayRoute": { 150 | "Type": "AWS::EC2::Route", 151 | "Properties": { 152 | "DestinationCidrBlock": "0.0.0.0/0", 153 | "RouteTableId": { 154 | "Ref": "RouteTable" 155 | }, 156 | "GatewayId": { 157 | "Ref": "InternetGateway" 158 | } 159 | } 160 | }, 161 | "Subnet": { 162 | "Type": "AWS::EC2::Subnet", 163 | "Properties": { 164 | "VpcId": { 165 | "Ref": "VPC" 166 | }, 167 | "CidrBlock": {"Ref": "VPCIPRange"}, 168 | "Tags": [ 169 | { 170 | "Key": "Application", 171 | "Value": { 172 | "Ref": "AWS::StackId" 173 | } 174 | } 175 | ] 176 | } 177 | }, 178 | "ECSServiceRole":{ 179 | "Type":"AWS::IAM::Role", 180 | "Properties":{ 181 | "AssumeRolePolicyDocument":{ 182 | "Statement":[ 183 | { 184 | "Effect":"Allow", 185 | "Principal":{ 186 | "Service":[ 187 | "ecs.amazonaws.com" 188 | ] 189 | }, 190 | "Action":[ 191 | "sts:AssumeRole" 192 | ] 193 | } 194 | ] 195 | }, 196 | "Path":"/", 197 | "Policies":[ 198 | { 199 | "PolicyName":"ecs-service", 200 | "PolicyDocument":{ 201 | "Statement":[ 202 | { 203 | "Effect":"Allow", 204 | "Action":[ 205 | "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", 206 | "elasticloadbalancing:DeregisterTargets", 207 | "elasticloadbalancing:Describe*", 208 | "elasticloadbalancing:RegisterInstancesWithLoadBalancer", 209 | "elasticloadbalancing:RegisterTargets", 210 | "ec2:Describe*", 211 | "ec2:AuthorizeSecurityGroupIngress" 212 | ], 213 | "Resource":"*" 214 | } 215 | ] 216 | } 217 | } 218 | ] 219 | } 220 | }, 221 | "EC2Role":{ 222 | "Type":"AWS::IAM::Role", 223 | "Properties":{ 224 | "AssumeRolePolicyDocument":{ 225 | "Statement":[ 226 | { 227 | "Effect":"Allow", 228 | "Principal":{ 229 | "Service":[ 230 | "ec2.amazonaws.com" 231 | ] 232 | }, 233 | "Action":[ 234 | "sts:AssumeRole" 235 | ] 236 | } 237 | ] 238 | }, 239 | "Path":"/", 240 | "Policies":[ 241 | { 242 | "PolicyName":"ecs-service", 243 | "PolicyDocument":{ 244 | "Statement":[ 245 | { 246 | "Effect":"Allow", 247 | "Action":[ 248 | "ecs:*", 249 | "elasticloadbalancing:Describe*", 250 | "logs:CreateLogStream", 251 | "logs:PutLogEvents" 252 | ], 253 | "Resource":"*" 254 | } 255 | ] 256 | } 257 | } 258 | ] 259 | } 260 | }, 261 | "JenkinsECSInstanceProfile": { 262 | "Type": "AWS::IAM::InstanceProfile", 263 | "Properties": { 264 | "Path": "/", 265 | "Roles": [ 266 | { 267 | "Ref": "EC2Role" 268 | } 269 | ] 270 | } 271 | }, 272 | "JenkinsSecurityGroup": { 273 | "Type": "AWS::EC2::SecurityGroup", 274 | "Properties": { 275 | "GroupDescription": "SecurityGroup for Jenkins instances: master and slaves", 276 | "VpcId": { 277 | "Ref": "VPC" 278 | }, 279 | "SecurityGroupIngress": [ 280 | { 281 | "IpProtocol": "tcp", 282 | "FromPort": "22", 283 | "ToPort": "22", 284 | "CidrIp": { 285 | "Ref": "AllowedIPRange" 286 | } 287 | }, 288 | { 289 | "IpProtocol": "tcp", 290 | "FromPort": "8080", 291 | "ToPort": "8080", 292 | "CidrIp": { 293 | "Ref": "VPCIPRange" 294 | } 295 | }, 296 | { 297 | "IpProtocol": "tcp", 298 | "FromPort": "50000", 299 | "ToPort": "50000", 300 | "CidrIp": { 301 | "Ref": "VPCIPRange" 302 | } 303 | } 304 | ] 305 | } 306 | }, 307 | "JenkinsELBSecurityGroup": { 308 | "Type": "AWS::EC2::SecurityGroup", 309 | "Properties": { 310 | "GroupDescription": "SecurityGroup for Jenkins ELB", 311 | "VpcId": { 312 | "Ref": "VPC" 313 | }, 314 | "SecurityGroupIngress": [ 315 | { 316 | "IpProtocol": "tcp", 317 | "FromPort": "80", 318 | "ToPort": "80", 319 | "CidrIp": { 320 | "Ref": "AllowedIPRange" 321 | } 322 | } 323 | ] 324 | } 325 | }, 326 | "EFSSecurityGroup": { 327 | "Type": "AWS::EC2::SecurityGroup", 328 | "Properties": { 329 | "GroupDescription": "Security group for EFS mount target", 330 | "VpcId": { 331 | "Ref": "VPC" 332 | }, 333 | "SecurityGroupIngress": [ 334 | { 335 | "IpProtocol": "tcp", 336 | "FromPort": "2049", 337 | "ToPort": "2049", 338 | "CidrIp": { 339 | "Ref": "VPCIPRange" 340 | } 341 | } 342 | ] 343 | } 344 | }, 345 | "JenkinsEFS": { 346 | "Type": "AWS::EFS::FileSystem", 347 | "Properties": { 348 | "FileSystemTags": [ 349 | { 350 | "Key": "Name", 351 | "Value": "JenkinsEFS" 352 | } 353 | ] 354 | } 355 | }, 356 | "MountTarget": { 357 | "Type": "AWS::EFS::MountTarget", 358 | "Properties": { 359 | "FileSystemId": { 360 | "Ref": "JenkinsEFS" 361 | }, 362 | "SubnetId": { 363 | "Ref": "Subnet" 364 | }, 365 | "SecurityGroups": [ 366 | { 367 | "Ref": "EFSSecurityGroup" 368 | } 369 | ] 370 | } 371 | }, 372 | "JenkinsELB": { 373 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 374 | "Properties": { 375 | "LoadBalancerName": "jenkins-elb", 376 | "Scheme": "internet-facing", 377 | "Subnets": [ 378 | { 379 | "Ref": "Subnet" 380 | } 381 | ], 382 | "SecurityGroups": [ 383 | { 384 | "Ref": "JenkinsELBSecurityGroup" 385 | } 386 | ], 387 | "Listeners": [ 388 | { 389 | "InstancePort": "8080", 390 | "InstanceProtocol": "HTTP", 391 | "LoadBalancerPort": "80", 392 | "Protocol": "HTTP", 393 | "PolicyNames": [ 394 | "JenkinsELBStickiness" 395 | ] 396 | } 397 | ], 398 | "LBCookieStickinessPolicy": [ 399 | { 400 | "CookieExpirationPeriod": "3600", 401 | "PolicyName": "JenkinsELBStickiness" 402 | } 403 | ], 404 | "HealthCheck": { 405 | "HealthyThreshold": "3", 406 | "Interval": "20", 407 | "Target": "HTTP:8080/login", 408 | "Timeout": "2", 409 | "UnhealthyThreshold": "10" 410 | } 411 | } 412 | }, 413 | "JenkinsCluster": { 414 | "Type": "AWS::ECS::Cluster", 415 | "Properties": { 416 | "ClusterName": "jenkins-cluster" 417 | } 418 | }, 419 | "JenkinsMasterTaskDefinition": { 420 | "Type": "AWS::ECS::TaskDefinition", 421 | "Properties": { 422 | "Family": "jenkins-master", 423 | "NetworkMode": "bridge", 424 | "ContainerDefinitions": [ 425 | { 426 | "Name": "jenkins-master", 427 | "Image": { 428 | "Ref": "DockerImage" 429 | }, 430 | "MountPoints": [ 431 | { 432 | "SourceVolume": "data-volume", 433 | "ContainerPath": "/var/jenkins_home" 434 | }, 435 | { 436 | "SourceVolume": "docker-volume", 437 | "ContainerPath": "/var/run/docker.sock" 438 | } 439 | ], 440 | "Essential": true, 441 | "Cpu": 3, 442 | "MemoryReservation": 2048, 443 | "PortMappings": [ 444 | { 445 | "HostPort": 8080, 446 | "ContainerPort": 8080, 447 | "Protocol": "tcp" 448 | }, 449 | { 450 | "HostPort": 50000, 451 | "ContainerPort": 50000, 452 | "Protocol": "tcp" 453 | } 454 | ] 455 | } 456 | ], 457 | "Volumes": [ 458 | { 459 | "Host": { 460 | "SourcePath": "/data/" 461 | }, 462 | "Name": "data-volume" 463 | }, 464 | { 465 | "Host": { 466 | "SourcePath": "/var/run/docker.sock" 467 | }, 468 | "Name": "docker-volume" 469 | } 470 | ] 471 | } 472 | }, 473 | "JenkinsECSService": { 474 | "DependsOn": ["JenkinsELB"], 475 | "Type": "AWS::ECS::Service", 476 | "Properties": { 477 | "Cluster": "jenkins-cluster", 478 | "DesiredCount": 1, 479 | "ServiceName": "jenkins-master", 480 | "TaskDefinition": { 481 | "Ref": "JenkinsMasterTaskDefinition" 482 | }, 483 | "Role" : { "Ref" : "ECSServiceRole" }, 484 | "LoadBalancers": [ 485 | { 486 | "LoadBalancerName": "jenkins-elb", 487 | "ContainerPort": "8080", 488 | "ContainerName": "jenkins-master" 489 | } 490 | ] 491 | } 492 | }, 493 | "JenkinsECSLaunchConfiguration": { 494 | "Type": "AWS::AutoScaling::LaunchConfiguration", 495 | "Properties": { 496 | "AssociatePublicIpAddress": true, 497 | "ImageId": { 498 | "Fn::FindInMap": [ 499 | "RegionAmazonECSOptimizedAMIMapping", 500 | { 501 | "Ref": "AWS::Region" 502 | }, 503 | "AMI" 504 | ] 505 | }, 506 | "IamInstanceProfile": { 507 | "Ref": "JenkinsECSInstanceProfile" 508 | }, 509 | "InstanceType": { 510 | "Ref": "InstanceType" 511 | }, 512 | "KeyName": {"Fn::If": ["HasKeyName", {"Ref": "KeyName"}, {"Ref": "AWS::NoValue"}]}, 513 | "SecurityGroups": [ 514 | { 515 | "Ref": "JenkinsSecurityGroup" 516 | } 517 | ], 518 | "BlockDeviceMappings": [ 519 | { 520 | "DeviceName": "/dev/xvdcz", 521 | "Ebs": { 522 | "VolumeSize": "24", 523 | "DeleteOnTermination": true 524 | } 525 | } 526 | ], 527 | "UserData": { 528 | "Fn::Base64": { 529 | "Fn::Join": [ 530 | "", 531 | [ 532 | "#!/bin/bash\n", 533 | "echo 'ECS_CLUSTER=jenkins-cluster' >> /etc/ecs/ecs.config\n", 534 | "#Mount EFS volume\n", 535 | "yum install -y nfs-utils\n", 536 | "EC2_AVAIL_ZONE=`curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone`\n", 537 | "EC2_REGION=", 538 | { 539 | "Ref": "AWS::Region" 540 | }, 541 | "\n", 542 | "EFS_FILE_SYSTEM_ID=", 543 | { 544 | "Ref": "JenkinsEFS" 545 | }, 546 | "\n", 547 | "EFS_PATH=$EC2_AVAIL_ZONE.$EFS_FILE_SYSTEM_ID.efs.$EC2_REGION.amazonaws.com\n", 548 | "mkdir /data\n", 549 | "mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 $EFS_PATH:/ /data\n", 550 | "#Give ownership to jenkins user\n", 551 | "chown 1000 /data\n", 552 | "chmod 777 /var/run/docker.sock\n", 553 | "if [[ ! -d /data/jobs/ ]]; then cd /data; curl -LJO https://github.com/aws-samples/amazon-eks-cdk-blue-green-cicd/raw/master/cicd/jenkins-jobs-archive.tar.gz; tar -xzvf jenkins-jobs-archive.tar.gz; cd; ls /data/jobs/; else echo 'SKIPPING DOWNLOAD...'; fi\n" 554 | ] 555 | ] 556 | } 557 | } 558 | } 559 | }, 560 | "JenkinsECSAutoScaling": { 561 | "Type": "AWS::AutoScaling::AutoScalingGroup", 562 | "Properties": { 563 | "VPCZoneIdentifier": [ 564 | { 565 | "Ref": "Subnet" 566 | } 567 | ], 568 | "LaunchConfigurationName": { 569 | "Ref": "JenkinsECSLaunchConfiguration" 570 | }, 571 | "MinSize": "2", 572 | "MaxSize": "5", 573 | "DesiredCapacity": "2", 574 | "HealthCheckType": "EC2", 575 | "HealthCheckGracePeriod": "400", 576 | "Tags": [ 577 | { 578 | "Key": "Name", 579 | "Value": "jenkins-ecs-instance", 580 | "PropagateAtLaunch": "true" 581 | } 582 | ] 583 | } 584 | }, 585 | 586 | "JenkinsClusterScaleUpPolicy": { 587 | "Type" : "AWS::AutoScaling::ScalingPolicy", 588 | "Properties" : { 589 | "AdjustmentType" : "ChangeInCapacity", 590 | "AutoScalingGroupName" : { "Ref": "JenkinsECSAutoScaling" }, 591 | "EstimatedInstanceWarmup" : 60, 592 | "MetricAggregationType" : "Average", 593 | "PolicyType" : "StepScaling", 594 | "StepAdjustments" : [ { 595 | "MetricIntervalLowerBound" : 0, 596 | "ScalingAdjustment" : 2 597 | }] 598 | } 599 | }, 600 | 601 | "JenkinsClusterScaleUpAlarm" : { 602 | "Type" : "AWS::CloudWatch::Alarm", 603 | "Properties" : { 604 | "AlarmDescription" : "CPU utilization peaked at 70% during the last minute", 605 | "AlarmName" : "JenkinsClusterScaleUpAlarm", 606 | "AlarmActions": [ { "Ref": "JenkinsClusterScaleUpPolicy" } ], 607 | "Dimensions" : [{ 608 | "Name": "ClusterName", 609 | "Value": "jenkins-cluster" 610 | }], 611 | "MetricName" : "CPUReservation", 612 | "Namespace" : "AWS/ECS", 613 | "ComparisonOperator" : "GreaterThanOrEqualToThreshold", 614 | "Statistic" : "Maximum", 615 | "Threshold" : 70, 616 | "Period" : 60, 617 | "EvaluationPeriods": 1, 618 | "TreatMissingData" : "notBreaching" 619 | } 620 | }, 621 | 622 | "JenkinsClusterScaleDownPolicy": { 623 | "Type" : "AWS::AutoScaling::ScalingPolicy", 624 | "Properties" : { 625 | "AdjustmentType" : "PercentChangeInCapacity", 626 | "AutoScalingGroupName" : { "Ref": "JenkinsECSAutoScaling" }, 627 | "Cooldown" : "120", 628 | "ScalingAdjustment" : "-50" 629 | } 630 | }, 631 | 632 | "JenkinsClusterScaleDownAlarm" : { 633 | "Type" : "AWS::CloudWatch::Alarm", 634 | "Properties" : { 635 | "AlarmDescription" : "CPU utilization is under 50% for the last 10 min (change 10 min to 45 min for prod use as you pay by the hour )", 636 | "AlarmName" : "JenkinsClusterScaleDownAlarm", 637 | "AlarmActions": [ { "Ref": "JenkinsClusterScaleDownPolicy" } ], 638 | "Dimensions" : [{ 639 | "Name": "ClusterName", 640 | "Value": "jenkins-cluster" 641 | }], 642 | "MetricName" : "CPUReservation", 643 | "Namespace" : "AWS/ECS", 644 | "ComparisonOperator" : "LessThanThreshold", 645 | "Statistic" : "Maximum", 646 | "Threshold" : 50, 647 | "Period" : 600, 648 | "EvaluationPeriods": 1, 649 | "TreatMissingData" : "notBreaching" 650 | } 651 | } 652 | 653 | }, 654 | "Outputs" : { 655 | "JenkinsELB" : { 656 | "Description": "Jenkins URL", 657 | "Value" : {"Fn::Join": ["", ["http://", { "Fn::GetAtt" : [ "JenkinsELB", "DNSName" ]}] ]} 658 | } 659 | } 660 | } 661 | -------------------------------------------------------------------------------- /cicd/jenkins-ecs-workshop2.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Cloudformation stack for Jenkins ECS", 4 | "Parameters": { 5 | "KeyName": { 6 | "Description": "Optional: the EC2 Key Pair to allow SSH access to the instances", 7 | "Type": "String", 8 | "Default": "" 9 | }, 10 | "AllowedIPRange": { 11 | "Description": "The public IP address range that can be used to connect to the instances", 12 | "Type": "String", 13 | "MinLength": "9", 14 | "MaxLength": "18", 15 | "Default": "0.0.0.0/0", 16 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 17 | "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." 18 | }, 19 | "VPCIPRange": { 20 | "Description": "The private IP address range for allocating IPs within the VPC.", 21 | "Type": "String", 22 | "MinLength": "9", 23 | "MaxLength": "18", 24 | "Default": "10.0.0.0/16", 25 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 26 | "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." 27 | }, 28 | "InstanceType": { 29 | "Description": "EC2 instance type", 30 | "Type": "String", 31 | "Default": "t2.medium", 32 | "AllowedValues": [ 33 | "t2.micro", 34 | "t2.small", 35 | "t2.medium" 36 | ], 37 | "ConstraintDescription": "Must be a valid EC2 instance type." 38 | }, 39 | "DockerImage": { 40 | "Description": "The docker image to use for Jenkins", 41 | "Type": "String", 42 | "Default": "nikunjv/jenkins:v4" 43 | } 44 | }, 45 | "Mappings": { 46 | "RegionAmazonECSOptimizedAMIMapping": { 47 | "us-east-2": { 48 | "AMI": "ami-0c0415cdff14e2a4a" 49 | }, 50 | "us-east-1": { 51 | "AMI": "ami-098616968d61e549e" 52 | }, 53 | "us-west-2": { 54 | "AMI": "ami-014a2e30da708ee8b" 55 | }, 56 | "us-west-1": { 57 | "AMI": "ami-00271233a1ebb9161" 58 | }, 59 | "eu-west-2": { 60 | "AMI": "ami-02254c861b8371869" 61 | }, 62 | "eu-west-1": { 63 | "AMI": "ami-0bb01c7d2705a4800" 64 | }, 65 | "eu-central-1": { 66 | "AMI": "ami-01b521fb7540d9996" 67 | }, 68 | "ap-northeast-1": { 69 | "AMI": "ami-0763fff45988661c8" 70 | }, 71 | "ap-southeast-2": { 72 | "AMI": "ami-0a7c4f7f17d3eecbc" 73 | }, 74 | "ap-southeast-1": { 75 | "AMI": "ami-0bd1daf5da8a9a903" 76 | }, 77 | "ca-central-1": { 78 | "AMI": "ami-092262e997a1ab27b" 79 | } 80 | } 81 | }, 82 | "Conditions": { 83 | "HasKeyName": {"Fn::Not": [{"Fn::Equals": ["", {"Ref": "KeyName"}]}]} 84 | }, 85 | "Resources": { 86 | "VPC": { 87 | "Type": "AWS::EC2::VPC", 88 | "Properties": { 89 | "EnableDnsSupport": "true", 90 | "EnableDnsHostnames": "true", 91 | "CidrBlock": {"Ref": "VPCIPRange"}, 92 | "Tags": [ 93 | { 94 | "Key": "Application", 95 | "Value": { 96 | "Ref": "AWS::StackId" 97 | } 98 | } 99 | ] 100 | } 101 | }, 102 | "InternetGateway": { 103 | "Type": "AWS::EC2::InternetGateway", 104 | "Properties": { 105 | "Tags": [ 106 | { 107 | "Key": "Application", 108 | "Value": { 109 | "Ref": "AWS::StackName" 110 | } 111 | }, 112 | { 113 | "Key": "Network", 114 | "Value": "Public" 115 | } 116 | ] 117 | } 118 | }, 119 | "GatewayToInternet": { 120 | "Type": "AWS::EC2::VPCGatewayAttachment", 121 | "Properties": { 122 | "VpcId": { 123 | "Ref": "VPC" 124 | }, 125 | "InternetGatewayId": { 126 | "Ref": "InternetGateway" 127 | } 128 | } 129 | }, 130 | "RouteTable": { 131 | "Type": "AWS::EC2::RouteTable", 132 | "Properties": { 133 | "VpcId": { 134 | "Ref": "VPC" 135 | } 136 | } 137 | }, 138 | "SubnetRouteTableAssoc": { 139 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 140 | "Properties": { 141 | "RouteTableId": { 142 | "Ref": "RouteTable" 143 | }, 144 | "SubnetId": { 145 | "Ref": "Subnet" 146 | } 147 | } 148 | }, 149 | "InternetGatewayRoute": { 150 | "Type": "AWS::EC2::Route", 151 | "Properties": { 152 | "DestinationCidrBlock": "0.0.0.0/0", 153 | "RouteTableId": { 154 | "Ref": "RouteTable" 155 | }, 156 | "GatewayId": { 157 | "Ref": "InternetGateway" 158 | } 159 | } 160 | }, 161 | "Subnet": { 162 | "Type": "AWS::EC2::Subnet", 163 | "Properties": { 164 | "VpcId": { 165 | "Ref": "VPC" 166 | }, 167 | "CidrBlock": {"Ref": "VPCIPRange"}, 168 | "Tags": [ 169 | { 170 | "Key": "Application", 171 | "Value": { 172 | "Ref": "AWS::StackId" 173 | } 174 | } 175 | ] 176 | } 177 | }, 178 | "ECSServiceRole":{ 179 | "Type":"AWS::IAM::Role", 180 | "Properties":{ 181 | "AssumeRolePolicyDocument":{ 182 | "Statement":[ 183 | { 184 | "Effect":"Allow", 185 | "Principal":{ 186 | "Service":[ 187 | "ecs.amazonaws.com" 188 | ] 189 | }, 190 | "Action":[ 191 | "sts:AssumeRole" 192 | ] 193 | } 194 | ] 195 | }, 196 | "Path":"/", 197 | "Policies":[ 198 | { 199 | "PolicyName":"ecs-service", 200 | "PolicyDocument":{ 201 | "Statement":[ 202 | { 203 | "Effect":"Allow", 204 | "Action":[ 205 | "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", 206 | "elasticloadbalancing:DeregisterTargets", 207 | "elasticloadbalancing:Describe*", 208 | "elasticloadbalancing:RegisterInstancesWithLoadBalancer", 209 | "elasticloadbalancing:RegisterTargets", 210 | "ec2:Describe*", 211 | "ec2:AuthorizeSecurityGroupIngress" 212 | ], 213 | "Resource":"*" 214 | } 215 | ] 216 | } 217 | } 218 | ] 219 | } 220 | }, 221 | "EC2Role":{ 222 | "Type":"AWS::IAM::Role", 223 | "Properties":{ 224 | "AssumeRolePolicyDocument":{ 225 | "Statement":[ 226 | { 227 | "Effect":"Allow", 228 | "Principal":{ 229 | "Service":[ 230 | "ec2.amazonaws.com" 231 | ] 232 | }, 233 | "Action":[ 234 | "sts:AssumeRole" 235 | ] 236 | } 237 | ] 238 | }, 239 | "Path":"/", 240 | "Policies":[ 241 | { 242 | "PolicyName":"ecs-service", 243 | "PolicyDocument":{ 244 | "Statement":[ 245 | { 246 | "Effect":"Allow", 247 | "Action":[ 248 | "ecs:*", 249 | "elasticloadbalancing:Describe*", 250 | "logs:CreateLogStream", 251 | "logs:PutLogEvents" 252 | ], 253 | "Resource":"*" 254 | } 255 | ] 256 | } 257 | } 258 | ] 259 | } 260 | }, 261 | "JenkinsECSInstanceProfile": { 262 | "Type": "AWS::IAM::InstanceProfile", 263 | "Properties": { 264 | "Path": "/", 265 | "Roles": [ 266 | { 267 | "Ref": "EC2Role" 268 | } 269 | ] 270 | } 271 | }, 272 | "JenkinsSecurityGroup": { 273 | "Type": "AWS::EC2::SecurityGroup", 274 | "Properties": { 275 | "GroupDescription": "SecurityGroup for Jenkins instances: master and slaves", 276 | "VpcId": { 277 | "Ref": "VPC" 278 | }, 279 | "SecurityGroupIngress": [ 280 | { 281 | "IpProtocol": "tcp", 282 | "FromPort": "22", 283 | "ToPort": "22", 284 | "CidrIp": { 285 | "Ref": "AllowedIPRange" 286 | } 287 | }, 288 | { 289 | "IpProtocol": "tcp", 290 | "FromPort": "8080", 291 | "ToPort": "8080", 292 | "CidrIp": { 293 | "Ref": "VPCIPRange" 294 | } 295 | }, 296 | { 297 | "IpProtocol": "tcp", 298 | "FromPort": "50000", 299 | "ToPort": "50000", 300 | "CidrIp": { 301 | "Ref": "VPCIPRange" 302 | } 303 | } 304 | ] 305 | } 306 | }, 307 | "JenkinsELBSecurityGroup": { 308 | "Type": "AWS::EC2::SecurityGroup", 309 | "Properties": { 310 | "GroupDescription": "SecurityGroup for Jenkins ELB", 311 | "VpcId": { 312 | "Ref": "VPC" 313 | }, 314 | "SecurityGroupIngress": [ 315 | { 316 | "IpProtocol": "tcp", 317 | "FromPort": "80", 318 | "ToPort": "80", 319 | "CidrIp": { 320 | "Ref": "AllowedIPRange" 321 | } 322 | } 323 | ] 324 | } 325 | }, 326 | "EFSSecurityGroup": { 327 | "Type": "AWS::EC2::SecurityGroup", 328 | "Properties": { 329 | "GroupDescription": "Security group for EFS mount target", 330 | "VpcId": { 331 | "Ref": "VPC" 332 | }, 333 | "SecurityGroupIngress": [ 334 | { 335 | "IpProtocol": "tcp", 336 | "FromPort": "2049", 337 | "ToPort": "2049", 338 | "CidrIp": { 339 | "Ref": "VPCIPRange" 340 | } 341 | } 342 | ] 343 | } 344 | }, 345 | "JenkinsEFS": { 346 | "Type": "AWS::EFS::FileSystem", 347 | "Properties": { 348 | "FileSystemTags": [ 349 | { 350 | "Key": "Name", 351 | "Value": "JenkinsEFS" 352 | } 353 | ] 354 | } 355 | }, 356 | "MountTarget": { 357 | "Type": "AWS::EFS::MountTarget", 358 | "Properties": { 359 | "FileSystemId": { 360 | "Ref": "JenkinsEFS" 361 | }, 362 | "SubnetId": { 363 | "Ref": "Subnet" 364 | }, 365 | "SecurityGroups": [ 366 | { 367 | "Ref": "EFSSecurityGroup" 368 | } 369 | ] 370 | } 371 | }, 372 | "JenkinsELB": { 373 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 374 | "Properties": { 375 | "LoadBalancerName": "jenkins-elb", 376 | "Scheme": "internet-facing", 377 | "Subnets": [ 378 | { 379 | "Ref": "Subnet" 380 | } 381 | ], 382 | "SecurityGroups": [ 383 | { 384 | "Ref": "JenkinsELBSecurityGroup" 385 | } 386 | ], 387 | "Listeners": [ 388 | { 389 | "InstancePort": "8080", 390 | "InstanceProtocol": "HTTP", 391 | "LoadBalancerPort": "80", 392 | "Protocol": "HTTP", 393 | "PolicyNames": [ 394 | "JenkinsELBStickiness" 395 | ] 396 | } 397 | ], 398 | "LBCookieStickinessPolicy": [ 399 | { 400 | "CookieExpirationPeriod": "3600", 401 | "PolicyName": "JenkinsELBStickiness" 402 | } 403 | ], 404 | "HealthCheck": { 405 | "HealthyThreshold": "3", 406 | "Interval": "20", 407 | "Target": "HTTP:8080/login", 408 | "Timeout": "2", 409 | "UnhealthyThreshold": "10" 410 | } 411 | } 412 | }, 413 | "JenkinsCluster": { 414 | "Type": "AWS::ECS::Cluster", 415 | "Properties": { 416 | "ClusterName": "jenkins-cluster" 417 | } 418 | }, 419 | "JenkinsMasterTaskDefinition": { 420 | "Type": "AWS::ECS::TaskDefinition", 421 | "Properties": { 422 | "Family": "jenkins-master", 423 | "NetworkMode": "bridge", 424 | "ContainerDefinitions": [ 425 | { 426 | "Name": "jenkins-master", 427 | "Image": { 428 | "Ref": "DockerImage" 429 | }, 430 | "MountPoints": [ 431 | { 432 | "SourceVolume": "data-volume", 433 | "ContainerPath": "/var/jenkins_home" 434 | }, 435 | { 436 | "SourceVolume": "docker-volume", 437 | "ContainerPath": "/var/run/docker.sock" 438 | } 439 | ], 440 | "Essential": true, 441 | "Cpu": 3, 442 | "MemoryReservation": 2048, 443 | "PortMappings": [ 444 | { 445 | "HostPort": 8080, 446 | "ContainerPort": 8080, 447 | "Protocol": "tcp" 448 | }, 449 | { 450 | "HostPort": 50000, 451 | "ContainerPort": 50000, 452 | "Protocol": "tcp" 453 | } 454 | ] 455 | } 456 | ], 457 | "Volumes": [ 458 | { 459 | "Host": { 460 | "SourcePath": "/data/" 461 | }, 462 | "Name": "data-volume" 463 | }, 464 | { 465 | "Host": { 466 | "SourcePath": "/var/run/docker.sock" 467 | }, 468 | "Name": "docker-volume" 469 | } 470 | ] 471 | } 472 | }, 473 | "JenkinsECSService": { 474 | "DependsOn": ["JenkinsELB"], 475 | "Type": "AWS::ECS::Service", 476 | "Properties": { 477 | "Cluster": "jenkins-cluster", 478 | "DesiredCount": 1, 479 | "ServiceName": "jenkins-master", 480 | "TaskDefinition": { 481 | "Ref": "JenkinsMasterTaskDefinition" 482 | }, 483 | "Role" : { "Ref" : "ECSServiceRole" }, 484 | "LoadBalancers": [ 485 | { 486 | "LoadBalancerName": "jenkins-elb", 487 | "ContainerPort": "8080", 488 | "ContainerName": "jenkins-master" 489 | } 490 | ] 491 | } 492 | }, 493 | "JenkinsECSLaunchConfiguration": { 494 | "Type": "AWS::AutoScaling::LaunchConfiguration", 495 | "Properties": { 496 | "AssociatePublicIpAddress": true, 497 | "ImageId": { 498 | "Fn::FindInMap": [ 499 | "RegionAmazonECSOptimizedAMIMapping", 500 | { 501 | "Ref": "AWS::Region" 502 | }, 503 | "AMI" 504 | ] 505 | }, 506 | "IamInstanceProfile": { 507 | "Ref": "JenkinsECSInstanceProfile" 508 | }, 509 | "InstanceType": { 510 | "Ref": "InstanceType" 511 | }, 512 | "KeyName": {"Fn::If": ["HasKeyName", {"Ref": "KeyName"}, {"Ref": "AWS::NoValue"}]}, 513 | "SecurityGroups": [ 514 | { 515 | "Ref": "JenkinsSecurityGroup" 516 | } 517 | ], 518 | "BlockDeviceMappings": [ 519 | { 520 | "DeviceName": "/dev/xvdcz", 521 | "Ebs": { 522 | "VolumeSize": "24", 523 | "DeleteOnTermination": true 524 | } 525 | } 526 | ], 527 | "UserData": { 528 | "Fn::Base64": { 529 | "Fn::Join": [ 530 | "", 531 | [ 532 | "#!/bin/bash\n", 533 | "echo 'ECS_CLUSTER=jenkins-cluster' >> /etc/ecs/ecs.config\n", 534 | "#Mount EFS volume\n", 535 | "yum install -y nfs-utils\n", 536 | "EC2_AVAIL_ZONE=`curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone`\n", 537 | "EC2_REGION=", 538 | { 539 | "Ref": "AWS::Region" 540 | }, 541 | "\n", 542 | "EFS_FILE_SYSTEM_ID=", 543 | { 544 | "Ref": "JenkinsEFS" 545 | }, 546 | "\n", 547 | "EFS_PATH=$EC2_AVAIL_ZONE.$EFS_FILE_SYSTEM_ID.efs.$EC2_REGION.amazonaws.com\n", 548 | "mkdir /data\n", 549 | "mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 $EFS_PATH:/ /data\n", 550 | "#Give ownership to jenkins user\n", 551 | "chown 1000 /data\n", 552 | "chmod 777 /var/run/docker.sock\n", 553 | "if [[ ! -d /data/jobs/ ]]; then cd /data; curl -LJO https://github.com/aws-samples/amazon-eks-cdk-blue-green-cicd/raw/master/cicd/jenkins-jobs-archive.tar.gz; tar -xzvf jenkins-jobs-archive.tar.gz; cd; ls /data/jobs/; else echo 'SKIPPING DOWNLOAD...'; fi\n" 554 | ] 555 | ] 556 | } 557 | } 558 | } 559 | }, 560 | "JenkinsECSAutoScaling": { 561 | "Type": "AWS::AutoScaling::AutoScalingGroup", 562 | "Properties": { 563 | "VPCZoneIdentifier": [ 564 | { 565 | "Ref": "Subnet" 566 | } 567 | ], 568 | "LaunchConfigurationName": { 569 | "Ref": "JenkinsECSLaunchConfiguration" 570 | }, 571 | "MinSize": "2", 572 | "MaxSize": "5", 573 | "DesiredCapacity": "2", 574 | "HealthCheckType": "EC2", 575 | "HealthCheckGracePeriod": "400", 576 | "Tags": [ 577 | { 578 | "Key": "Name", 579 | "Value": "jenkins-ecs-instance", 580 | "PropagateAtLaunch": "true" 581 | } 582 | ] 583 | } 584 | }, 585 | 586 | "JenkinsClusterScaleUpPolicy": { 587 | "Type" : "AWS::AutoScaling::ScalingPolicy", 588 | "Properties" : { 589 | "AdjustmentType" : "ChangeInCapacity", 590 | "AutoScalingGroupName" : { "Ref": "JenkinsECSAutoScaling" }, 591 | "EstimatedInstanceWarmup" : 60, 592 | "MetricAggregationType" : "Average", 593 | "PolicyType" : "StepScaling", 594 | "StepAdjustments" : [ { 595 | "MetricIntervalLowerBound" : 0, 596 | "ScalingAdjustment" : 2 597 | }] 598 | } 599 | }, 600 | 601 | "JenkinsClusterScaleUpAlarm" : { 602 | "Type" : "AWS::CloudWatch::Alarm", 603 | "Properties" : { 604 | "AlarmDescription" : "CPU utilization peaked at 70% during the last minute", 605 | "AlarmName" : "JenkinsClusterScaleUpAlarm", 606 | "AlarmActions": [ { "Ref": "JenkinsClusterScaleUpPolicy" } ], 607 | "Dimensions" : [{ 608 | "Name": "ClusterName", 609 | "Value": "jenkins-cluster" 610 | }], 611 | "MetricName" : "CPUReservation", 612 | "Namespace" : "AWS/ECS", 613 | "ComparisonOperator" : "GreaterThanOrEqualToThreshold", 614 | "Statistic" : "Maximum", 615 | "Threshold" : 70, 616 | "Period" : 60, 617 | "EvaluationPeriods": 1, 618 | "TreatMissingData" : "notBreaching" 619 | } 620 | }, 621 | 622 | "JenkinsClusterScaleDownPolicy": { 623 | "Type" : "AWS::AutoScaling::ScalingPolicy", 624 | "Properties" : { 625 | "AdjustmentType" : "PercentChangeInCapacity", 626 | "AutoScalingGroupName" : { "Ref": "JenkinsECSAutoScaling" }, 627 | "Cooldown" : "120", 628 | "ScalingAdjustment" : "-50" 629 | } 630 | }, 631 | 632 | "JenkinsClusterScaleDownAlarm" : { 633 | "Type" : "AWS::CloudWatch::Alarm", 634 | "Properties" : { 635 | "AlarmDescription" : "CPU utilization is under 50% for the last 10 min (change 10 min to 45 min for prod use as you pay by the hour )", 636 | "AlarmName" : "JenkinsClusterScaleDownAlarm", 637 | "AlarmActions": [ { "Ref": "JenkinsClusterScaleDownPolicy" } ], 638 | "Dimensions" : [{ 639 | "Name": "ClusterName", 640 | "Value": "jenkins-cluster" 641 | }], 642 | "MetricName" : "CPUReservation", 643 | "Namespace" : "AWS/ECS", 644 | "ComparisonOperator" : "LessThanThreshold", 645 | "Statistic" : "Maximum", 646 | "Threshold" : 50, 647 | "Period" : 600, 648 | "EvaluationPeriods": 1, 649 | "TreatMissingData" : "notBreaching" 650 | } 651 | } 652 | 653 | }, 654 | "Outputs" : { 655 | "JenkinsELB" : { 656 | "Description": "Jenkins URL", 657 | "Value" : {"Fn::Join": ["", ["http://", { "Fn::GetAtt" : [ "JenkinsELB", "DNSName" ]}] ]} 658 | } 659 | } 660 | } 661 | -------------------------------------------------------------------------------- /cicd/jenkins-ecs-workshop3.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Cloudformation stack for Jenkins ECS", 4 | "Parameters": { 5 | "KeyName": { 6 | "Description": "Optional: the EC2 Key Pair to allow SSH access to the instances", 7 | "Type": "String", 8 | "Default": "" 9 | }, 10 | "AllowedIPRange": { 11 | "Description": "The public IP address range that can be used to connect to the instances", 12 | "Type": "String", 13 | "MinLength": "9", 14 | "MaxLength": "18", 15 | "Default": "0.0.0.0/0", 16 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 17 | "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." 18 | }, 19 | "VPCIPRange": { 20 | "Description": "The private IP address range for allocating IPs within the VPC.", 21 | "Type": "String", 22 | "MinLength": "9", 23 | "MaxLength": "18", 24 | "Default": "10.0.0.0/16", 25 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 26 | "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." 27 | }, 28 | "InstanceType": { 29 | "Description": "EC2 instance type", 30 | "Type": "String", 31 | "Default": "t2.medium", 32 | "AllowedValues": [ 33 | "t2.micro", 34 | "t2.small", 35 | "t2.medium" 36 | ], 37 | "ConstraintDescription": "Must be a valid EC2 instance type." 38 | }, 39 | "DockerImage": { 40 | "Description": "The docker image to use for Jenkins", 41 | "Type": "String", 42 | "Default": "nikunjv/jenkins:v4" 43 | } 44 | }, 45 | "Mappings": { 46 | "RegionAmazonECSOptimizedAMIMapping": { 47 | "us-east-2": { 48 | "AMI": "ami-0c0415cdff14e2a4a" 49 | }, 50 | "us-east-1": { 51 | "AMI": "ami-098616968d61e549e" 52 | }, 53 | "us-west-2": { 54 | "AMI": "ami-014a2e30da708ee8b" 55 | }, 56 | "us-west-1": { 57 | "AMI": "ami-00271233a1ebb9161" 58 | }, 59 | "eu-west-2": { 60 | "AMI": "ami-02254c861b8371869" 61 | }, 62 | "eu-west-1": { 63 | "AMI": "ami-0bb01c7d2705a4800" 64 | }, 65 | "eu-central-1": { 66 | "AMI": "ami-01b521fb7540d9996" 67 | }, 68 | "ap-northeast-1": { 69 | "AMI": "ami-0763fff45988661c8" 70 | }, 71 | "ap-southeast-2": { 72 | "AMI": "ami-0a7c4f7f17d3eecbc" 73 | }, 74 | "ap-southeast-1": { 75 | "AMI": "ami-0bd1daf5da8a9a903" 76 | }, 77 | "ca-central-1": { 78 | "AMI": "ami-092262e997a1ab27b" 79 | } 80 | } 81 | }, 82 | "Conditions": { 83 | "HasKeyName": {"Fn::Not": [{"Fn::Equals": ["", {"Ref": "KeyName"}]}]} 84 | }, 85 | "Resources": { 86 | "VPC": { 87 | "Type": "AWS::EC2::VPC", 88 | "Properties": { 89 | "EnableDnsSupport": "true", 90 | "EnableDnsHostnames": "true", 91 | "CidrBlock": {"Ref": "VPCIPRange"}, 92 | "Tags": [ 93 | { 94 | "Key": "Application", 95 | "Value": { 96 | "Ref": "AWS::StackId" 97 | } 98 | } 99 | ] 100 | } 101 | }, 102 | "InternetGateway": { 103 | "Type": "AWS::EC2::InternetGateway", 104 | "Properties": { 105 | "Tags": [ 106 | { 107 | "Key": "Application", 108 | "Value": { 109 | "Ref": "AWS::StackName" 110 | } 111 | }, 112 | { 113 | "Key": "Network", 114 | "Value": "Public" 115 | } 116 | ] 117 | } 118 | }, 119 | "GatewayToInternet": { 120 | "Type": "AWS::EC2::VPCGatewayAttachment", 121 | "Properties": { 122 | "VpcId": { 123 | "Ref": "VPC" 124 | }, 125 | "InternetGatewayId": { 126 | "Ref": "InternetGateway" 127 | } 128 | } 129 | }, 130 | "RouteTable": { 131 | "Type": "AWS::EC2::RouteTable", 132 | "Properties": { 133 | "VpcId": { 134 | "Ref": "VPC" 135 | } 136 | } 137 | }, 138 | "SubnetRouteTableAssoc": { 139 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 140 | "Properties": { 141 | "RouteTableId": { 142 | "Ref": "RouteTable" 143 | }, 144 | "SubnetId": { 145 | "Ref": "Subnet" 146 | } 147 | } 148 | }, 149 | "InternetGatewayRoute": { 150 | "Type": "AWS::EC2::Route", 151 | "Properties": { 152 | "DestinationCidrBlock": "0.0.0.0/0", 153 | "RouteTableId": { 154 | "Ref": "RouteTable" 155 | }, 156 | "GatewayId": { 157 | "Ref": "InternetGateway" 158 | } 159 | } 160 | }, 161 | "Subnet": { 162 | "Type": "AWS::EC2::Subnet", 163 | "Properties": { 164 | "VpcId": { 165 | "Ref": "VPC" 166 | }, 167 | "CidrBlock": {"Ref": "VPCIPRange"}, 168 | "Tags": [ 169 | { 170 | "Key": "Application", 171 | "Value": { 172 | "Ref": "AWS::StackId" 173 | } 174 | } 175 | ] 176 | } 177 | }, 178 | "ECSServiceRole":{ 179 | "Type":"AWS::IAM::Role", 180 | "Properties":{ 181 | "AssumeRolePolicyDocument":{ 182 | "Statement":[ 183 | { 184 | "Effect":"Allow", 185 | "Principal":{ 186 | "Service":[ 187 | "ecs.amazonaws.com" 188 | ] 189 | }, 190 | "Action":[ 191 | "sts:AssumeRole" 192 | ] 193 | } 194 | ] 195 | }, 196 | "Path":"/", 197 | "Policies":[ 198 | { 199 | "PolicyName":"ecs-service", 200 | "PolicyDocument":{ 201 | "Statement":[ 202 | { 203 | "Effect":"Allow", 204 | "Action":[ 205 | "elasticloadbalancing:DeregisterInstancesFromLoadBalancer", 206 | "elasticloadbalancing:DeregisterTargets", 207 | "elasticloadbalancing:Describe*", 208 | "elasticloadbalancing:RegisterInstancesWithLoadBalancer", 209 | "elasticloadbalancing:RegisterTargets", 210 | "ec2:Describe*", 211 | "ec2:AuthorizeSecurityGroupIngress" 212 | ], 213 | "Resource":"*" 214 | } 215 | ] 216 | } 217 | } 218 | ] 219 | } 220 | }, 221 | "EC2Role":{ 222 | "Type":"AWS::IAM::Role", 223 | "Properties":{ 224 | "AssumeRolePolicyDocument":{ 225 | "Statement":[ 226 | { 227 | "Effect":"Allow", 228 | "Principal":{ 229 | "Service":[ 230 | "ec2.amazonaws.com" 231 | ] 232 | }, 233 | "Action":[ 234 | "sts:AssumeRole" 235 | ] 236 | } 237 | ] 238 | }, 239 | "Path":"/", 240 | "Policies":[ 241 | { 242 | "PolicyName":"ecs-service", 243 | "PolicyDocument":{ 244 | "Statement":[ 245 | { 246 | "Effect":"Allow", 247 | "Action":[ 248 | "ecs:*", 249 | "elasticloadbalancing:Describe*", 250 | "logs:CreateLogStream", 251 | "logs:PutLogEvents" 252 | ], 253 | "Resource":"*" 254 | } 255 | ] 256 | } 257 | } 258 | ] 259 | } 260 | }, 261 | "JenkinsECSInstanceProfile": { 262 | "Type": "AWS::IAM::InstanceProfile", 263 | "Properties": { 264 | "Path": "/", 265 | "Roles": [ 266 | { 267 | "Ref": "EC2Role" 268 | } 269 | ] 270 | } 271 | }, 272 | "JenkinsSecurityGroup": { 273 | "Type": "AWS::EC2::SecurityGroup", 274 | "Properties": { 275 | "GroupDescription": "SecurityGroup for Jenkins instances: manager and workers", 276 | "VpcId": { 277 | "Ref": "VPC" 278 | }, 279 | "SecurityGroupIngress": [ 280 | { 281 | "IpProtocol": "tcp", 282 | "FromPort": "22", 283 | "ToPort": "22", 284 | "CidrIp": { 285 | "Ref": "AllowedIPRange" 286 | } 287 | }, 288 | { 289 | "IpProtocol": "tcp", 290 | "FromPort": "8080", 291 | "ToPort": "8080", 292 | "CidrIp": { 293 | "Ref": "VPCIPRange" 294 | } 295 | }, 296 | { 297 | "IpProtocol": "tcp", 298 | "FromPort": "50000", 299 | "ToPort": "50000", 300 | "CidrIp": { 301 | "Ref": "VPCIPRange" 302 | } 303 | } 304 | ] 305 | } 306 | }, 307 | "JenkinsELBSecurityGroup": { 308 | "Type": "AWS::EC2::SecurityGroup", 309 | "Properties": { 310 | "GroupDescription": "SecurityGroup for Jenkins ELB", 311 | "VpcId": { 312 | "Ref": "VPC" 313 | }, 314 | "SecurityGroupIngress": [ 315 | { 316 | "IpProtocol": "tcp", 317 | "FromPort": "80", 318 | "ToPort": "80", 319 | "CidrIp": { 320 | "Ref": "AllowedIPRange" 321 | } 322 | } 323 | ] 324 | } 325 | }, 326 | "EFSSecurityGroup": { 327 | "Type": "AWS::EC2::SecurityGroup", 328 | "Properties": { 329 | "GroupDescription": "Security group for EFS mount target", 330 | "VpcId": { 331 | "Ref": "VPC" 332 | }, 333 | "SecurityGroupIngress": [ 334 | { 335 | "IpProtocol": "tcp", 336 | "FromPort": "2049", 337 | "ToPort": "2049", 338 | "CidrIp": { 339 | "Ref": "VPCIPRange" 340 | } 341 | } 342 | ] 343 | } 344 | }, 345 | "JenkinsEFS": { 346 | "Type": "AWS::EFS::FileSystem", 347 | "Properties": { 348 | "FileSystemTags": [ 349 | { 350 | "Key": "Name", 351 | "Value": "JenkinsEFS" 352 | } 353 | ] 354 | } 355 | }, 356 | "MountTarget": { 357 | "Type": "AWS::EFS::MountTarget", 358 | "Properties": { 359 | "FileSystemId": { 360 | "Ref": "JenkinsEFS" 361 | }, 362 | "SubnetId": { 363 | "Ref": "Subnet" 364 | }, 365 | "SecurityGroups": [ 366 | { 367 | "Ref": "EFSSecurityGroup" 368 | } 369 | ] 370 | } 371 | }, 372 | "JenkinsELB": { 373 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 374 | "Properties": { 375 | "LoadBalancerName": "jenkins-elb", 376 | "Scheme": "internet-facing", 377 | "Subnets": [ 378 | { 379 | "Ref": "Subnet" 380 | } 381 | ], 382 | "SecurityGroups": [ 383 | { 384 | "Ref": "JenkinsELBSecurityGroup" 385 | } 386 | ], 387 | "Listeners": [ 388 | { 389 | "InstancePort": "8080", 390 | "InstanceProtocol": "HTTP", 391 | "LoadBalancerPort": "80", 392 | "Protocol": "HTTP", 393 | "PolicyNames": [ 394 | "JenkinsELBStickiness" 395 | ] 396 | } 397 | ], 398 | "LBCookieStickinessPolicy": [ 399 | { 400 | "CookieExpirationPeriod": "3600", 401 | "PolicyName": "JenkinsELBStickiness" 402 | } 403 | ], 404 | "HealthCheck": { 405 | "HealthyThreshold": "3", 406 | "Interval": "20", 407 | "Target": "HTTP:8080/login", 408 | "Timeout": "2", 409 | "UnhealthyThreshold": "10" 410 | } 411 | } 412 | }, 413 | "JenkinsCluster": { 414 | "Type": "AWS::ECS::Cluster", 415 | "Properties": { 416 | "ClusterName": "jenkins-cluster" 417 | } 418 | }, 419 | "JenkinsManagerTaskDefinition": { 420 | "Type": "AWS::ECS::TaskDefinition", 421 | "Properties": { 422 | "Family": "jenkins-manager", 423 | "NetworkMode": "bridge", 424 | "ContainerDefinitions": [ 425 | { 426 | "Name": "jenkins-manager", 427 | "Image": { 428 | "Ref": "DockerImage" 429 | }, 430 | "MountPoints": [ 431 | { 432 | "SourceVolume": "data-volume", 433 | "ContainerPath": "/var/jenkins_home" 434 | }, 435 | { 436 | "SourceVolume": "docker-volume", 437 | "ContainerPath": "/var/run/docker.sock" 438 | } 439 | ], 440 | "Essential": true, 441 | "Cpu": 3, 442 | "MemoryReservation": 2048, 443 | "PortMappings": [ 444 | { 445 | "HostPort": 8080, 446 | "ContainerPort": 8080, 447 | "Protocol": "tcp" 448 | }, 449 | { 450 | "HostPort": 50000, 451 | "ContainerPort": 50000, 452 | "Protocol": "tcp" 453 | } 454 | ] 455 | } 456 | ], 457 | "Volumes": [ 458 | { 459 | "Host": { 460 | "SourcePath": "/data/" 461 | }, 462 | "Name": "data-volume" 463 | }, 464 | { 465 | "Host": { 466 | "SourcePath": "/var/run/docker.sock" 467 | }, 468 | "Name": "docker-volume" 469 | } 470 | ] 471 | } 472 | }, 473 | "JenkinsECSService": { 474 | "DependsOn": ["JenkinsELB"], 475 | "Type": "AWS::ECS::Service", 476 | "Properties": { 477 | "Cluster": "jenkins-cluster", 478 | "DesiredCount": 1, 479 | "ServiceName": "jenkins-manager", 480 | "TaskDefinition": { 481 | "Ref": "JenkinsManagerTaskDefinition" 482 | }, 483 | "Role" : { "Ref" : "ECSServiceRole" }, 484 | "LoadBalancers": [ 485 | { 486 | "LoadBalancerName": "jenkins-elb", 487 | "ContainerPort": "8080", 488 | "ContainerName": "jenkins-manager" 489 | } 490 | ] 491 | } 492 | }, 493 | "JenkinsECSLaunchConfiguration": { 494 | "Type": "AWS::AutoScaling::LaunchConfiguration", 495 | "Properties": { 496 | "AssociatePublicIpAddress": true, 497 | "ImageId": { 498 | "Fn::FindInMap": [ 499 | "RegionAmazonECSOptimizedAMIMapping", 500 | { 501 | "Ref": "AWS::Region" 502 | }, 503 | "AMI" 504 | ] 505 | }, 506 | "IamInstanceProfile": { 507 | "Ref": "JenkinsECSInstanceProfile" 508 | }, 509 | "InstanceType": { 510 | "Ref": "InstanceType" 511 | }, 512 | "KeyName": {"Fn::If": ["HasKeyName", {"Ref": "KeyName"}, {"Ref": "AWS::NoValue"}]}, 513 | "SecurityGroups": [ 514 | { 515 | "Ref": "JenkinsSecurityGroup" 516 | } 517 | ], 518 | "BlockDeviceMappings": [ 519 | { 520 | "DeviceName": "/dev/xvdcz", 521 | "Ebs": { 522 | "VolumeSize": "24", 523 | "DeleteOnTermination": true 524 | } 525 | } 526 | ], 527 | "UserData": { 528 | "Fn::Base64": { 529 | "Fn::Join": [ 530 | "", 531 | [ 532 | "#!/bin/bash\n", 533 | "echo 'ECS_CLUSTER=jenkins-cluster' >> /etc/ecs/ecs.config\n", 534 | "#Mount EFS volume\n", 535 | "yum install -y nfs-utils\n", 536 | "EC2_AVAIL_ZONE=`curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone`\n", 537 | "EC2_REGION=", 538 | { 539 | "Ref": "AWS::Region" 540 | }, 541 | "\n", 542 | "EFS_FILE_SYSTEM_ID=", 543 | { 544 | "Ref": "JenkinsEFS" 545 | }, 546 | "\n", 547 | "EFS_PATH=$EC2_AVAIL_ZONE.$EFS_FILE_SYSTEM_ID.efs.$EC2_REGION.amazonaws.com\n", 548 | "mkdir /data\n", 549 | "mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 $EFS_PATH:/ /data\n", 550 | "#Give ownership to jenkins user\n", 551 | "chown 1000 /data\n", 552 | "chmod 777 /var/run/docker.sock\n", 553 | "if [[ ! -d /data/jobs/ ]]; then cd /data; curl -LJO https://github.com/aws-samples/amazon-eks-cdk-blue-green-cicd/raw/master/cicd/jenkins-jobs-archive3.tar.gz; tar -xzvf jenkins-jobs-archive3.tar.gz; cd; ls /data/jobs/; else echo 'SKIPPING DOWNLOAD...'; fi\n", 554 | "#Give ownership to jenkins user\n", 555 | "chown -R 1000:1000 /data\n", 556 | "ls -altr /data" 557 | ] 558 | ] 559 | } 560 | } 561 | } 562 | }, 563 | "JenkinsECSAutoScaling": { 564 | "Type": "AWS::AutoScaling::AutoScalingGroup", 565 | "Properties": { 566 | "VPCZoneIdentifier": [ 567 | { 568 | "Ref": "Subnet" 569 | } 570 | ], 571 | "LaunchConfigurationName": { 572 | "Ref": "JenkinsECSLaunchConfiguration" 573 | }, 574 | "MinSize": "2", 575 | "MaxSize": "5", 576 | "DesiredCapacity": "2", 577 | "HealthCheckType": "EC2", 578 | "HealthCheckGracePeriod": "400", 579 | "Tags": [ 580 | { 581 | "Key": "Name", 582 | "Value": "jenkins-ecs-instance", 583 | "PropagateAtLaunch": "true" 584 | } 585 | ] 586 | } 587 | }, 588 | 589 | "JenkinsClusterScaleUpPolicy": { 590 | "Type" : "AWS::AutoScaling::ScalingPolicy", 591 | "Properties" : { 592 | "AdjustmentType" : "ChangeInCapacity", 593 | "AutoScalingGroupName" : { "Ref": "JenkinsECSAutoScaling" }, 594 | "EstimatedInstanceWarmup" : 60, 595 | "MetricAggregationType" : "Average", 596 | "PolicyType" : "StepScaling", 597 | "StepAdjustments" : [ { 598 | "MetricIntervalLowerBound" : 0, 599 | "ScalingAdjustment" : 2 600 | }] 601 | } 602 | }, 603 | 604 | "JenkinsClusterScaleUpAlarm" : { 605 | "Type" : "AWS::CloudWatch::Alarm", 606 | "Properties" : { 607 | "AlarmDescription" : "CPU utilization peaked at 70% during the last minute", 608 | "AlarmName" : "JenkinsClusterScaleUpAlarm", 609 | "AlarmActions": [ { "Ref": "JenkinsClusterScaleUpPolicy" } ], 610 | "Dimensions" : [{ 611 | "Name": "ClusterName", 612 | "Value": "jenkins-cluster" 613 | }], 614 | "MetricName" : "CPUReservation", 615 | "Namespace" : "AWS/ECS", 616 | "ComparisonOperator" : "GreaterThanOrEqualToThreshold", 617 | "Statistic" : "Maximum", 618 | "Threshold" : 70, 619 | "Period" : 60, 620 | "EvaluationPeriods": 1, 621 | "TreatMissingData" : "notBreaching" 622 | } 623 | }, 624 | 625 | "JenkinsClusterScaleDownPolicy": { 626 | "Type" : "AWS::AutoScaling::ScalingPolicy", 627 | "Properties" : { 628 | "AdjustmentType" : "PercentChangeInCapacity", 629 | "AutoScalingGroupName" : { "Ref": "JenkinsECSAutoScaling" }, 630 | "Cooldown" : "120", 631 | "ScalingAdjustment" : "-50" 632 | } 633 | }, 634 | 635 | "JenkinsClusterScaleDownAlarm" : { 636 | "Type" : "AWS::CloudWatch::Alarm", 637 | "Properties" : { 638 | "AlarmDescription" : "CPU utilization is under 50% for the last 10 min (change 10 min to 45 min for prod use as you pay by the hour )", 639 | "AlarmName" : "JenkinsClusterScaleDownAlarm", 640 | "AlarmActions": [ { "Ref": "JenkinsClusterScaleDownPolicy" } ], 641 | "Dimensions" : [{ 642 | "Name": "ClusterName", 643 | "Value": "jenkins-cluster" 644 | }], 645 | "MetricName" : "CPUReservation", 646 | "Namespace" : "AWS/ECS", 647 | "ComparisonOperator" : "LessThanThreshold", 648 | "Statistic" : "Maximum", 649 | "Threshold" : 50, 650 | "Period" : 600, 651 | "EvaluationPeriods": 1, 652 | "TreatMissingData" : "notBreaching" 653 | } 654 | } 655 | 656 | }, 657 | "Outputs" : { 658 | "JenkinsELB" : { 659 | "Description": "Jenkins URL", 660 | "Value" : {"Fn::Join": ["", ["http://", { "Fn::GetAtt" : [ "JenkinsELB", "DNSName" ]}] ]} 661 | } 662 | } 663 | } 664 | -------------------------------------------------------------------------------- /cicd/jenkins-jobs-archive.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/cicd/jenkins-jobs-archive.tar.gz -------------------------------------------------------------------------------- /cicd/jenkins-jobs-archive3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/cicd/jenkins-jobs-archive3.tar.gz -------------------------------------------------------------------------------- /dockerAssets.d/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/p8v8e7e5/myartifacts:alpine-jan2021 2 | 3 | 4 | ENV KUBECONFIG /home/kubectl/.kube/kubeconfig 5 | ENV HOME /home/kubectl 6 | # ENV KUBECONFIG /root/.kube/kubeconfig 7 | 8 | 9 | RUN \ 10 | mkdir /root/bin /aws; \ 11 | apk -Uuv add groff less bash python3 py-pip jq curl docker && \ 12 | pip3 install --upgrade pip; \ 13 | pip3 install awscli && \ 14 | rm /var/cache/apk/* && \ 15 | # Create non-root user (with a randomly chosen UID/GUI). 16 | adduser kubectl -Du 5566 17 | 18 | ADD https://s3.us-west-2.amazonaws.com/amazon-eks/1.22.6/2022-03-09/bin/linux/amd64/kubectl /usr/local/bin/kubectl 19 | #COPY kubectl /usr/local/bin/kubectl 20 | 21 | WORKDIR $HOME 22 | 23 | COPY entrypoint.sh /usr/local/bin/entrypoint.sh 24 | 25 | RUN chmod a+x /usr/local/bin/kubectl /usr/local/bin/entrypoint.sh 26 | 27 | VOLUME /var/lib/docker 28 | 29 | # USER kubectl 30 | ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] 31 | -------------------------------------------------------------------------------- /dockerAssets.d/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # export PATH=$PATH:/root/bin 5 | HOME=/home/kubectl 6 | 7 | export KUBECONFIG=$HOME/.kube/kubeconfig 8 | 9 | start_dockerd() { 10 | /usr/bin/dockerd \ 11 | --host=unix:///var/run/docker.sock \ 12 | --host=tcp://127.0.0.1:2375 \ 13 | --storage-driver=overlay &>/var/log/docker.log & 14 | tries=0 15 | d_timeout=60 16 | until docker info >/dev/null 2>&1 17 | do 18 | if [ "$tries" -gt "$d_timeout" ]; then 19 | cat /var/log/docker.log 20 | echo 'Timed out trying to connect to internal docker host.' >&2 21 | exit 1 22 | fi 23 | tries=$(( $tries + 1 )) 24 | sleep 1 25 | done 26 | } 27 | 28 | 29 | if [[ ! -z ${CODEBUILD_BUILD_ID} ]]; then 30 | # in AWS CodeBuild 31 | echo "found myself in AWS CodeBuild, starting dockerd..." 32 | start_dockerd 33 | fi 34 | 35 | 36 | if [[ ! -z ${AWS_REGION} ]]; then 37 | region=$AWS_REGION 38 | echo "[INFO] region=$AWS_REGION" 39 | else 40 | echo "REGION not defined, trying to lookup from EC2 metadata..." 41 | region=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq .region -r) 42 | fi 43 | 44 | # export AWS_DEFAULT_REGION=${REGION-${CODEBUILD_AGENT_ENV_CODEBUILD_REGION-$region}} 45 | export AWS_DEFAULT_REGION=$region 46 | 47 | CLUSTER_NAME=${CLUSTER_NAME-default} 48 | 49 | update_kubeconfig(){ 50 | if [[ -n ${EKS_ROLE_ARN} ]]; then 51 | echo "[INFO] got EKS_ROLE_ARN=${EKS_ROLE_ARN}, updating kubeconfig with this role" 52 | aws eks update-kubeconfig --name $CLUSTER_NAME --kubeconfig $KUBECONFIG --role-arn "${EKS_ROLE_ARN}" 53 | else 54 | aws eks update-kubeconfig --name $CLUSTER_NAME --kubeconfig $KUBECONFIG 55 | fi 56 | } 57 | 58 | update_kubeconfig 59 | exec "$@" 60 | -------------------------------------------------------------------------------- /flask-docker-app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/flask-docker-app/.DS_Store -------------------------------------------------------------------------------- /flask-docker-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/p8v8e7e5/myartifacts:alpine-3.8 2 | RUN apk add python3 py-pip && \ 3 | python3 -m ensurepip && \ 4 | pip install --upgrade pip && \ 5 | pip install flask 6 | 7 | ENV FLASK_APP app.py 8 | ENV PLATFORM 'Amazon EKS' 9 | 10 | WORKDIR /app 11 | COPY . /app/ 12 | 13 | CMD ["python", "app.py"] 14 | -------------------------------------------------------------------------------- /flask-docker-app/app.py: -------------------------------------------------------------------------------- 1 | # from flask import Flask, escape, request, render_template 2 | import flask 3 | import datetime 4 | import platform 5 | import os 6 | 7 | app = flask.Flask(__name__) 8 | 9 | 10 | @app.route('/') 11 | def hello(): 12 | name = flask.request.args.get("name", "Flask-demo") 13 | time = datetime.datetime.now() 14 | python_version = platform.python_version() 15 | aws_platform = os.environ.get('PLATFORM', 'Amazon Web Services') 16 | return flask.render_template('hello.html', 17 | platform=aws_platform, 18 | flask_version=flask.__version__, 19 | python_version=python_version, 20 | flask_url='https://palletsprojects.com/p/flask/', 21 | time=time, 22 | name=name) 23 | 24 | 25 | if __name__ == '__main__': 26 | app.run( 27 | debug=True, 28 | host='0.0.0.0', 29 | port=5000 30 | ) 31 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/flask-docker-app/k8s/.DS_Store -------------------------------------------------------------------------------- /flask-docker-app/k8s/alb-ingress-controller.yaml: -------------------------------------------------------------------------------- 1 | # Application Load Balancer (ALB) Ingress Controller Deployment Manifest. 2 | # This manifest details sensible defaults for deploying an ALB Ingress Controller. 3 | # GitHub: https://github.com/kubernetes-sigs/aws-alb-ingress-controller 4 | apiVersion: apps/v1 5 | kind: Deployment 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: alb-ingress-controller 9 | name: alb-ingress-controller 10 | # Namespace the ALB Ingress Controller should run in. Does not impact which 11 | # namespaces it's able to resolve ingress resource for. For limiting ingress 12 | # namespace scope, see --watch-namespace. 13 | namespace: kube-system 14 | spec: 15 | selector: 16 | matchLabels: 17 | app.kubernetes.io/name: alb-ingress-controller 18 | template: 19 | metadata: 20 | labels: 21 | app.kubernetes.io/name: alb-ingress-controller 22 | spec: 23 | containers: 24 | - name: alb-ingress-controller 25 | args: 26 | # Limit the namespace where this ALB Ingress Controller deployment will 27 | # resolve ingress resources. If left commented, all namespaces are used. 28 | # - --watch-namespace=your-k8s-namespace 29 | 30 | # Setting the ingress-class flag below ensures that only ingress resources with the 31 | # annotation kubernetes.io/ingress.class: "alb" are respected by the controller. You may 32 | # choose any class you'd like for this controller to respect. 33 | - --ingress-class=alb 34 | 35 | # REQUIRED 36 | # Name of your cluster. Used when naming resources created 37 | # by the ALB Ingress Controller, providing distinction between 38 | # clusters. 39 | - --cluster-name=devCluster 40 | 41 | # AWS VPC ID this ingress controller will use to create AWS resources. 42 | # If unspecified, it will be discovered from ec2metadata. 43 | # - --aws-vpc-id=vpc-xxxxxx 44 | 45 | # AWS region this ingress controller will operate in. 46 | # If unspecified, it will be discovered from ec2metadata. 47 | # List of regions: http://docs.aws.amazon.com/general/latest/gr/rande.html#vpc_region 48 | # - --aws-region=us-west-1 49 | 50 | # Enables logging on all outbound requests sent to the AWS API. 51 | # If logging is desired, set to true. 52 | # - --aws-api-debug 53 | # Maximum number of times to retry the aws calls. 54 | # defaults to 10. 55 | # - --aws-max-retries=10 56 | # env: 57 | # AWS key id for authenticating with the AWS API. 58 | # This is only here for examples. It's recommended you instead use 59 | # a project like kube2iam for granting access. 60 | #- name: AWS_ACCESS_KEY_ID 61 | # value: KEYVALUE 62 | 63 | # AWS key secret for authenticating with the AWS API. 64 | # This is only here for examples. It's recommended you instead use 65 | # a project like kube2iam for granting access. 66 | #- name: AWS_SECRET_ACCESS_KEY 67 | # value: SECRETVALUE 68 | # Repository location of the ALB Ingress Controller. 69 | image: docker.io/amazon/aws-alb-ingress-controller:v1.1.5 70 | serviceAccountName: alb-ingress-controller 71 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/flask-ALB-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: "flask-alb" 5 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/flask.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: flask-svc 5 | spec: 6 | selector: 7 | app: flask 8 | ports: 9 | - name: web 10 | port: 80 11 | targetPort: 5000 12 | type: LoadBalancer 13 | --- 14 | apiVersion: apps/v1beta1 15 | kind: Deployment 16 | metadata: 17 | labels: 18 | run: flask 19 | name: flask 20 | spec: 21 | replicas: 1 22 | template: 23 | metadata: 24 | labels: 25 | app: flask 26 | spec: 27 | containers: 28 | - name: flask 29 | image: nikunjv/flask-image:blue 30 | ports: 31 | - containerPort: 5000 32 | # command: 33 | # - "sh" 34 | # - "-c" 35 | # - "yum install -y python3-pip && tail -f /var/log/yum.log " 36 | resources: 37 | limits: 38 | memory: "500Mi" 39 | cpu: "0.25" 40 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/flaskALBBlue.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: "flask-svc-alb-blue" 5 | namespace: "flask-alb" 6 | spec: 7 | selector: 8 | app: "flask-deploy-alb-blue" 9 | type: NodePort 10 | ports: 11 | - name: web 12 | port: 80 13 | targetPort: 5000 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | labels: 19 | run: "flask" 20 | name: "flask-deploy-alb-blue" 21 | namespace: "flask-alb" 22 | spec: 23 | selector: 24 | matchLabels: 25 | app: "flask-deploy-alb-blue" 26 | replicas: 1 27 | template: 28 | metadata: 29 | labels: 30 | app: "flask-deploy-alb-blue" 31 | spec: 32 | containers: 33 | - name: "flask" 34 | image: public.ecr.aws/p8v8e7e5/myartifacts:flask-image-grey 35 | ports: 36 | - containerPort: 5000 37 | # command: 38 | # - "sh" 39 | # - "-c" 40 | # - "yum install -y python3-pip && tail -f /var/log/yum.log " 41 | resources: 42 | limits: 43 | memory: "500Mi" 44 | cpu: "0.25" 45 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/flaskALBGreen.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: "flask-svc-alb-green" 5 | namespace: "flask-alb" 6 | spec: 7 | selector: 8 | app: "flask-deploy-alb-green" 9 | type: NodePort 10 | ports: 11 | - name: web 12 | port: 8080 13 | targetPort: 5000 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | labels: 19 | run: "flask" 20 | name: "flask-deploy-alb-green" 21 | namespace: "flask-alb" 22 | spec: 23 | selector: 24 | matchLabels: 25 | app: "flask-deploy-alb-green" 26 | replicas: 1 27 | template: 28 | metadata: 29 | labels: 30 | app: "flask-deploy-alb-green" 31 | spec: 32 | containers: 33 | - name: "flask" 34 | image: public.ecr.aws/p8v8e7e5/myartifacts:flask-image-grey 35 | ports: 36 | - containerPort: 5000 37 | # command: 38 | # - "sh" 39 | # - "-c" 40 | # - "yum install -y python3-pip && tail -f /var/log/yum.log " 41 | resources: 42 | limits: 43 | memory: "500Mi" 44 | cpu: "0.25" 45 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/flaskALBIngress_query.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: alb-ingress 5 | namespace: "flask-alb" 6 | annotations: 7 | kubernetes.io/ingress.class: alb 8 | alb.ingress.kubernetes.io/scheme: internet-facing 9 | alb.ingress.kubernetes.io/subnets: public-subnets 10 | alb.ingress.kubernetes.io/security-groups: sec-grp 11 | alb.ingress.kubernetes.io/conditions.flask-svc-alb-green: '[{"Field":"query-string","QueryStringConfig":{"Values":[{"Key":"group","Value":"green"}]}}]' 12 | alb.ingress.kubernetes.io/conditions.forward-multiple-tg: '[{"Field":"query-string","QueryStringConfig":{"Values":[{"Key":"group","Value":"blue"}]}}]' 13 | alb.ingress.kubernetes.io/actions.forward-multiple-tg: '{"Type":"forward","ForwardConfig":{"TargetGroups":[{"ServiceName":"flask-svc-alb-blue","ServicePort":"80","Weight":100},{"ServiceName":"flask-svc-alb-green","ServicePort":"8080","Weight":0}]}}' 14 | 15 | labels: 16 | app: flask-ingress 17 | spec: 18 | rules: 19 | - http: 20 | paths: 21 | - backend: 22 | serviceName: flask-svc-alb-green 23 | servicePort: 8080 24 | - backend: 25 | serviceName: forward-multiple-tg 26 | servicePort: use-annotation 27 | - path: /* 28 | backend: 29 | serviceName: flask-svc-alb-blue 30 | servicePort: 80 31 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/flaskALBIngress_query2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: alb-ingress 5 | namespace: "flask-alb" 6 | annotations: 7 | kubernetes.io/ingress.class: alb 8 | alb.ingress.kubernetes.io/scheme: internet-facing 9 | alb.ingress.kubernetes.io/subnets: public-subnets 10 | alb.ingress.kubernetes.io/security-groups: sec-grp 11 | alb.ingress.kubernetes.io/conditions.flask-svc-alb-green: '[{"Field":"query-string","QueryStringConfig":{"Values":[{"Key":"group","Value":"green"}]}}]' 12 | alb.ingress.kubernetes.io/conditions.forward-multiple-tg: '[{"Field":"query-string","QueryStringConfig":{"Values":[{"Key":"group","Value":"blue"}]}}]' 13 | alb.ingress.kubernetes.io/actions.forward-multiple-tg: '{"Type":"forward","ForwardConfig":{"TargetGroups":[{"ServiceName":"flask-svc-alb-blue","ServicePort":"80","Weight":90},{"ServiceName":"flask-svc-alb-green","ServicePort":"8080","Weight":10}]}}' 14 | 15 | labels: 16 | app: flask-ingress 17 | spec: 18 | rules: 19 | - http: 20 | paths: 21 | - backend: 22 | serviceName: flask-svc-alb-green 23 | servicePort: 8080 24 | - backend: 25 | serviceName: forward-multiple-tg 26 | servicePort: use-annotation 27 | - path: /* 28 | backend: 29 | serviceName: flask-svc-alb-blue 30 | servicePort: 80 31 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/flaskBlue.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: flask-svc-blue 5 | spec: 6 | selector: 7 | app: flask-deploy-blue 8 | ports: 9 | - name: web 10 | port: 80 11 | targetPort: 5000 12 | type: LoadBalancer 13 | --- 14 | apiVersion: apps/v1beta1 15 | kind: Deployment 16 | metadata: 17 | labels: 18 | run: flask 19 | name: flask-deploy-blue 20 | spec: 21 | replicas: 1 22 | template: 23 | metadata: 24 | labels: 25 | app: flask-deploy-blue 26 | spec: 27 | containers: 28 | - name: flask 29 | image: nikunjv/flask-image:blue 30 | ports: 31 | - containerPort: 5000 32 | # command: 33 | # - "sh" 34 | # - "-c" 35 | # - "yum install -y python3-pip && tail -f /var/log/yum.log " 36 | resources: 37 | limits: 38 | memory: "500Mi" 39 | cpu: "0.25" 40 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/flaskGreen.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: flask-svc-green 5 | spec: 6 | selector: 7 | app: flask-deploy-green 8 | ports: 9 | - name: web 10 | port: 8080 11 | targetPort: 5000 12 | type: LoadBalancer 13 | --- 14 | apiVersion: apps/v1beta1 15 | kind: Deployment 16 | metadata: 17 | labels: 18 | run: flask 19 | name: flask-deploy-green 20 | spec: 21 | replicas: 1 22 | template: 23 | metadata: 24 | labels: 25 | app: flask-deploy-green 26 | spec: 27 | containers: 28 | - name: flask 29 | image: nikunjv/flask-image:blue 30 | ports: 31 | - containerPort: 5000 32 | # command: 33 | # - "sh" 34 | # - "-c" 35 | # - "yum install -y python3-pip && tail -f /var/log/yum.log " 36 | resources: 37 | limits: 38 | memory: "500Mi" 39 | cpu: "0.25" 40 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | #Setup Env Vars 6 | export REGION=$1 7 | export NODE_ROLE_NAME=$2 8 | export CLUSTER_NAME=$3 9 | 10 | export ALB_POLICY_NAME=alb-ingress-controller 11 | policyExists=$(aws iam list-policies | jq '.Policies[].PolicyName' | grep alb-ingress-controller | tr -d '["\r\n]') 12 | if [[ "$policyExists" != "alb-ingress-controller" ]]; then 13 | echo "Policy does not exist, creating..." 14 | export ALB_POLICY_ARN=$(aws iam create-policy --region=$REGION --policy-name $ALB_POLICY_NAME --policy-document "https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/master/docs/examples/iam-policy.json" --query "Policy.Arn" | sed 's/"//g') 15 | aws iam attach-role-policy --region=$REGION --role-name=$NODE_ROLE_NAME --policy-arn=$ALB_POLICY_ARN 16 | fi 17 | 18 | #Create Ingress Controller 19 | if [ ! -f alb-ingress-controller.yaml ]; then 20 | wget https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.5/docs/examples/alb-ingress-controller.yaml 21 | fi 22 | sed -i "s/devCluster/$CLUSTER_NAME/g" alb-ingress-controller.yaml 23 | sed -i "s/# - --cluster-name/- --cluster-name/g" alb-ingress-controller.yaml 24 | kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.5/docs/examples/rbac-role.yaml 25 | kubectl apply -f alb-ingress-controller.yaml 26 | 27 | #Check 28 | kubectl get pods -n kube-system 29 | #kubectl logs -n kube-system $(kubectl get po -n kube-system | egrep -o "alb-ingress[a-zA-Z0-9-]+") 30 | 31 | #Attach IAM policy to Worker Node Role 32 | if [ ! -f iam-policy.json ]; then 33 | curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/master/docs/examples/iam-policy.json 34 | fi 35 | aws iam put-role-policy --role-name $NODE_ROLE_NAME --policy-name elb-policy --policy-document file://iam-policy.json 36 | 37 | #Instantiate Blue and Green PODS 38 | kubectl apply -f flask-ALB-namespace.yaml 39 | kubectl apply -f flaskALBBlue.yaml 40 | kubectl apply -f flaskALBGreen.yaml 41 | 42 | #Check 43 | kubectl get deploy -n flask-alb 44 | kubectl get svc -n flask-alb 45 | kubectl get pods -n flask-alb 46 | 47 | #Update Ingress Resource file and spawn ALB 48 | sg=$(aws ec2 describe-security-groups --filters Name=tag:aws:cloudformation:stack-name,Values=CdkStackALBEksBg | jq '.SecurityGroups[0].GroupId' | tr -d '["]') 49 | vpcid=$(aws ec2 describe-security-groups --filters Name=tag:aws:cloudformation:stack-name,Values=CdkStackALBEksBg | jq '.SecurityGroups[0].VpcId' | tr -d '["]') 50 | subnets=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$vpcid" "Name=tag:aws-cdk:subnet-name,Values=Public" | jq '.Subnets[0].SubnetId' | tr -d '["]') 51 | subnets="$subnets, $(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$vpcid" "Name=tag:aws-cdk:subnet-name,Values=Public" | jq '.Subnets[1].SubnetId' | tr -d '["]')" 52 | subnets="$subnets, $(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$vpcid" "Name=tag:aws-cdk:subnet-name,Values=Public" | jq '.Subnets[2].SubnetId' | tr -d '["]')" 53 | 54 | sed -i "s/public-subnets/$subnets/g" flaskALBIngress_query.yaml 55 | sed -i "s/public-subnets/$subnets/g" flaskALBIngress_query2.yaml 56 | sed -i "s/sec-grp/$sg/g" flaskALBIngress_query.yaml 57 | sed -i "s/sec-grp/$sg/g" flaskALBIngress_query2.yaml 58 | kubectl apply -f flaskALBIngress_query.yaml 59 | -------------------------------------------------------------------------------- /flask-docker-app/k8s/setup2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | #Setup Env Vars 6 | export REGION=$1 7 | export NODE_ROLE_NAME=$2 8 | export CLUSTER_NAME=$3 9 | 10 | set +x 11 | echo "========================" 12 | echo "------CLEANUP BEGIN-----" 13 | echo "========================" 14 | set -x 15 | rm flaskALBIngress_query.yaml 16 | rm flaskALBIngress_query2.yaml 17 | wget https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/master/flask-docker-app/k8s/flaskALBIngress_query.yaml 18 | wget https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/master/flask-docker-app/k8s/flaskALBIngress_query2.yaml 19 | rm alb-ingress-controller.yaml 20 | kubectl delete svc/flask-svc-alb-blue svc/flask-svc-alb-green -n flask-alb 21 | kubectl delete deploy/flask-deploy-alb-blue deploy/flask-deploy-alb-green -n flask-alb 22 | kubectl delete ingress alb-ingress -n flask-alb 23 | kubectl delete deploy alb-ingress-controller -n kube-system 24 | set +x 25 | echo "======================" 26 | echo "------CLEANUP END-----" 27 | echo "======================" 28 | set -x 29 | kubectl get deploy -n flask-alb 30 | kubectl get svc -n flask-alb 31 | kubectl get pods -n flask-alb 32 | kubectl get ingress -n flask-alb 33 | kubectl get pods -n kube-system 34 | ls -al 35 | set +x 36 | echo "============================================" 37 | echo "------CAPTURE COMPLETE, BEGIN EXECUTION-----" 38 | echo "============================================" 39 | 40 | echo "Sleep for 5 seconds to allow termination of resources" 41 | set -x 42 | sleep 5 43 | 44 | export ALB_POLICY_NAME=alb-ingress-controller 45 | policyExists=$(aws iam list-policies | jq '.Policies[].PolicyName' | grep alb-ingress-controller | tr -d '["\r\n]') 46 | if [[ "$policyExists" != "alb-ingress-controller" ]]; then 47 | echo "Policy does not exist, creating..." 48 | export ALB_POLICY_ARN=$(aws iam create-policy --region=$REGION --policy-name $ALB_POLICY_NAME --policy-document "https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/master/docs/examples/iam-policy.json" --query "Policy.Arn" | sed 's/"//g') 49 | aws iam attach-role-policy --region=$REGION --role-name=$NODE_ROLE_NAME --policy-arn=$ALB_POLICY_ARN 50 | fi 51 | 52 | #Create Ingress Controller 53 | if [ ! -f alb-ingress-controller.yaml ]; then 54 | wget https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.5/docs/examples/alb-ingress-controller.yaml 55 | fi 56 | sed -i "s/devCluster/$CLUSTER_NAME/g" alb-ingress-controller.yaml 57 | sed -i "s/# - --cluster-name/- --cluster-name/g" alb-ingress-controller.yaml 58 | kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/v1.1.5/docs/examples/rbac-role.yaml 59 | kubectl apply -f alb-ingress-controller.yaml 60 | 61 | #Check 62 | kubectl get pods -n kube-system 63 | #kubectl logs -n kube-system $(kubectl get po -n kube-system | egrep -o "alb-ingress[a-zA-Z0-9-]+") 64 | 65 | #Attach IAM policy to Worker Node Role 66 | if [ ! -f iam-policy.json ]; then 67 | curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-alb-ingress-controller/master/docs/examples/iam-policy.json 68 | fi 69 | aws iam put-role-policy --role-name $NODE_ROLE_NAME --policy-name elb-policy --policy-document file://iam-policy.json 70 | 71 | #Instantiate Blue and Green PODS 72 | kubectl apply -f flask-ALB-namespace.yaml 73 | kubectl apply -f flaskALBBlue.yaml 74 | kubectl apply -f flaskALBGreen.yaml 75 | 76 | #Check 77 | kubectl get deploy -n flask-alb 78 | kubectl get svc -n flask-alb 79 | kubectl get pods -n flask-alb 80 | 81 | #Update Ingress Resource file and spawn ALB 82 | sg=$(aws ec2 describe-security-groups --filters Name=tag:aws:cloudformation:stack-name,Values=CdkStackALBEksBg | jq '.SecurityGroups[0].GroupId' | tr -d '["]') 83 | vpcid=$(aws ec2 describe-security-groups --filters Name=tag:aws:cloudformation:stack-name,Values=CdkStackALBEksBg | jq '.SecurityGroups[0].VpcId' | tr -d '["]') 84 | subnets=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$vpcid" "Name=tag:aws-cdk:subnet-name,Values=Public" | jq '.Subnets[0].SubnetId' | tr -d '["]') 85 | subnets="$subnets, $(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$vpcid" "Name=tag:aws-cdk:subnet-name,Values=Public" | jq '.Subnets[1].SubnetId' | tr -d '["]')" 86 | subnets="$subnets, $(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$vpcid" "Name=tag:aws-cdk:subnet-name,Values=Public" | jq '.Subnets[2].SubnetId' | tr -d '["]')" 87 | 88 | sed -i "s/public-subnets/$subnets/g" flaskALBIngress_query.yaml 89 | sed -i "s/public-subnets/$subnets/g" flaskALBIngress_query2.yaml 90 | sed -i "s/sec-grp/$sg/g" flaskALBIngress_query.yaml 91 | sed -i "s/sec-grp/$sg/g" flaskALBIngress_query2.yaml 92 | kubectl apply -f flaskALBIngress_query.yaml 93 | set +x 94 | echo "================" 95 | echo "--CHECK OUTPUT--" 96 | echo "================" 97 | set -x 98 | kubectl get deploy -n flask-alb 99 | kubectl get svc -n flask-alb 100 | kubectl get ingress -n flask-alb 101 | kubectl get pods -n kube-system 102 | kubectl get pods -n flask-alb 103 | set +x 104 | echo "========================" 105 | echo "------END EXECUTION-----" 106 | echo "========================" 107 | 108 | #Add cluster sg ingress rule from alb source 109 | CLUSTER_SG=$(aws eks describe-cluster --name $CLUSTER_NAME --query cluster.resourcesVpcConfig.clusterSecurityGroupId | tr -d '["]') 110 | 111 | aws ec2 authorize-security-group-ingress \ 112 | --group-id $CLUSTER_SG \ 113 | --protocol -1 \ 114 | --port -1 \ 115 | --source-group $sg -------------------------------------------------------------------------------- /flask-docker-app/pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /Users/pahud/homebrew/bin 2 | include-system-site-packages = false 3 | version = 3.7.4 4 | -------------------------------------------------------------------------------- /flask-docker-app/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | Flask==1.1.1 3 | itsdangerous==1.1.0 4 | Jinja2==2.11.3 5 | MarkupSafe==1.1.1 6 | Werkzeug==0.15.6 7 | -------------------------------------------------------------------------------- /flask-docker-app/static/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.2.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */@-ms-viewport{width:device-width}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /flask-docker-app/templates/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Simple Flask App 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

{{ name }}

19 |

Congratulations

20 |

Your Flask application is now running on a container in {{ platform }}

21 |

The container is running Flask version {{ flask_version }} and Python {{ python_version }}

22 | 23 | -------------------------------------------------------------------------------- /images/alb-dns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/alb-dns.png -------------------------------------------------------------------------------- /images/alb-tg-check1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/alb-tg-check1.png -------------------------------------------------------------------------------- /images/alb-tg-check2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/alb-tg-check2.png -------------------------------------------------------------------------------- /images/canary-lb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/canary-lb.png -------------------------------------------------------------------------------- /images/cfn-kubectl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/cfn-kubectl.png -------------------------------------------------------------------------------- /images/eks-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/eks-bg-1.png -------------------------------------------------------------------------------- /images/eks-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/eks-bg-2.png -------------------------------------------------------------------------------- /images/eks-canary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/eks-canary.png -------------------------------------------------------------------------------- /images/eks-cicd-codebuild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/eks-cicd-codebuild.png -------------------------------------------------------------------------------- /images/flask01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/flask01.png -------------------------------------------------------------------------------- /images/flask02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/flask02.png -------------------------------------------------------------------------------- /images/stage12-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/stage12-green.png -------------------------------------------------------------------------------- /images/stage34-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/stage34-green.png -------------------------------------------------------------------------------- /images/web-blue-inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/web-blue-inv.png -------------------------------------------------------------------------------- /images/web-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/web-blue.png -------------------------------------------------------------------------------- /images/web-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/web-default.png -------------------------------------------------------------------------------- /images/web-green-inv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/web-green-inv.png -------------------------------------------------------------------------------- /images/web-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-eks-cdk-blue-green-cicd/872efe33a3511f9c1d28676663e30303c8bbd59a/images/web-green.png --------------------------------------------------------------------------------