├── Terraform ├── output.tf ├── jenkins.key.example ├── jenkins.pem.example ├── simple_web_app.key.example ├── simple_web_app.pem.example ├── random.tf ├── providers.tf ├── jenkins-server │ ├── output.tf │ ├── main.tf │ ├── variables.tf │ └── user_data.sh ├── application-server │ ├── output.tf │ ├── main.tf │ ├── variables.tf │ └── user_data.sh ├── secrets.tf ├── key-pairs.tf ├── terraform.tfvars.example ├── jenkins-config │ ├── get_credentials_id.sh │ ├── confirm_url.sh │ ├── download_install_plugins.sh │ ├── create_credentials.sh │ ├── create_admin_user.sh │ └── create_multibranch_pipeline.sh ├── application.tf ├── variables.tf ├── s3.tf ├── ecr.tf ├── jenkins.tf ├── iam.tf └── networking.tf ├── server ├── src │ ├── public │ │ ├── js │ │ │ └── scripts.js │ │ └── css │ │ │ └── styles.css │ ├── routes │ │ ├── db.json │ │ └── index.js │ ├── index.js │ └── views │ │ ├── index.html │ │ └── users.html ├── .mocharc.yml ├── babel.config.js ├── test │ ├── integration │ │ └── index.js │ └── unit │ │ └── index.js ├── webpack.config.js └── package.json ├── .dockerignore ├── Dockerfile.test ├── Dockerfile ├── LICENSE.md ├── README.md ├── .gitignore └── Jenkinsfile /Terraform/output.tf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Terraform/jenkins.key.example: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Terraform/jenkins.pem.example: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/src/public/js/scripts.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Terraform/simple_web_app.key.example: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Terraform/simple_web_app.pem.example: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Terraform/random.tf: -------------------------------------------------------------------------------- 1 | resource "random_id" "job-id" { 2 | byte_length = 16 3 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | Dockerfile.test 3 | node_modules 4 | .gitignore 5 | .git 6 | Terraform -------------------------------------------------------------------------------- /server/.mocharc.yml: -------------------------------------------------------------------------------- 1 | exit: true 2 | require: 3 | - "@babel/register" 4 | - "regenerator-runtime" 5 | recursive: true 6 | timeout: "10000" 7 | -------------------------------------------------------------------------------- /Terraform/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | access_key = var.aws-access-key 3 | secret_key = var.aws-secret-key 4 | region = var.aws-region 5 | } -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine@sha256:b2da3316acdc2bec442190a1fe10dc094e7ba4121d029cb32075ff59bb27390a 2 | 3 | COPY . /opt/app 4 | 5 | WORKDIR /opt/app/server 6 | 7 | RUN npm i -------------------------------------------------------------------------------- /server/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /Terraform/jenkins-server/output.tf: -------------------------------------------------------------------------------- 1 | output "instance-id" { 2 | value = aws_instance.default.id 3 | } 4 | 5 | output "name" { 6 | value = var.name 7 | } 8 | 9 | output "private-ip" { 10 | value = aws_instance.default.private_ip 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Terraform/application-server/output.tf: -------------------------------------------------------------------------------- 1 | output "instance-id" { 2 | value = aws_instance.default.id 3 | } 4 | 5 | output "name" { 6 | value = var.name 7 | } 8 | 9 | output "private-ip" { 10 | value = aws_instance.default.private_ip 11 | } 12 | 13 | -------------------------------------------------------------------------------- /server/src/public/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #333; 3 | color: white; 4 | } 5 | 6 | .container { 7 | position: absolute; 8 | top: 50%; 9 | left: 50%; 10 | transform: translate(-50%, -50%); 11 | text-align: center; 12 | } 13 | -------------------------------------------------------------------------------- /Terraform/secrets.tf: -------------------------------------------------------------------------------- 1 | resource "aws_secretsmanager_secret" "simple-web-app" { 2 | name = "simple-web-app" 3 | } 4 | 5 | resource "aws_secretsmanager_secret_version" "simple-web-app" { 6 | secret_id = aws_secretsmanager_secret.simple-web-app.id 7 | secret_string = jsonencode(var.secrets) 8 | } 9 | -------------------------------------------------------------------------------- /Terraform/key-pairs.tf: -------------------------------------------------------------------------------- 1 | # SSH key - Web App 2 | 3 | resource "aws_key_pair" "simple-web-app-key" { 4 | key_name = "simple-web-app" 5 | public_key = file("./simple_web_app.pem") 6 | } 7 | 8 | # SSH key - Jenkins 9 | 10 | resource "aws_key_pair" "jenkins-key" { 11 | key_name = "jenkins" 12 | public_key = file("./jenkins.pem") 13 | } -------------------------------------------------------------------------------- /Terraform/terraform.tfvars.example: -------------------------------------------------------------------------------- 1 | aws-access-key = "" 2 | aws-secret-key = "" 3 | aws-region = "us-east-1" 4 | admin-username = "" 5 | admin-password = "" 6 | admin-fullname = " doe" 7 | admin-email = "" 8 | remote-repo = "" 9 | job-name= "CI-CD Pipeline" 10 | secrets = { 11 | public = "" 12 | private = "" 13 | slackToken = "" 14 | } -------------------------------------------------------------------------------- /Terraform/jenkins-config/get_credentials_id.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | cookie_jar="$(mktemp)" 4 | full_crumb=$(curl -u "$user:$password" --cookie-jar "$cookie_jar" $url/crumbIssuer/api/xml?xpath=concat\(//crumbRequestField,%22:%22,//crumb\)) 5 | 6 | curl -u "$user:$password" -X GET "$url/credentials/store/system/domain/_/api/json?tree=credentials[id]" \ 7 | -H "$full_crumb" \ 8 | --cookie $cookie_jar 9 | -------------------------------------------------------------------------------- /server/src/routes/db.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Fibre Bundle", 4 | "email": "fibre@bundle.com" 5 | }, 6 | { 7 | "name": "Quantum Electrodynamics", 8 | "email": "Quantum@electrodynamics.com" 9 | }, 10 | { 11 | "name": "Feynman Diagram", 12 | "email": "feynman@diagram.com" 13 | }, 14 | { 15 | "name": "Differentiable Manifold", 16 | "email": "differentiable@manifold.com" 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /Terraform/application-server/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_instance" "default" { 2 | ami = var.ami-id 3 | iam_instance_profile = var.iam-instance-profile 4 | instance_type = var.instance-type 5 | key_name = var.key-pair 6 | network_interface { 7 | device_index = var.device-index 8 | network_interface_id = var.network-interface-id 9 | } 10 | 11 | user_data = templatefile("${path.module}/user_data.sh", {repository_url = var.repository-url}) 12 | 13 | tags = { 14 | Name = var.name 15 | } 16 | } -------------------------------------------------------------------------------- /Terraform/application.tf: -------------------------------------------------------------------------------- 1 | module "application-server" { 2 | source = "./application-server" 3 | 4 | ami-id = "ami-0742b4e673072066f" # AMI for an Amazon Linux instance for region: us-east-1 5 | 6 | iam-instance-profile = aws_iam_instance_profile.simple-web-app.id 7 | key-pair = aws_key_pair.simple-web-app-key.key_name 8 | name = "Simple Web App" 9 | device-index = 0 10 | network-interface-id = aws_network_interface.simple-web-app.id 11 | repository-url = aws_ecr_repository.simple-web-app.repository_url 12 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine@sha256:b2da3316acdc2bec442190a1fe10dc094e7ba4121d029cb32075ff59bb27390a 2 | 3 | COPY --chown=node:node . /opt/app 4 | 5 | WORKDIR /opt/app/server 6 | 7 | RUN npm i && \ 8 | chmod 775 -R ./node_modules/ && \ 9 | npm run build && \ 10 | npm prune --production && \ 11 | mv -f dist node_modules package.json package-lock.json /tmp && \ 12 | rm -f -R * && \ 13 | mv -f /tmp/* . && \ 14 | rm -f -R /tmp 15 | 16 | ENV NODE_ENV production 17 | 18 | EXPOSE 8000 19 | 20 | USER node 21 | 22 | CMD ["node", "./dist/bundle.js"] -------------------------------------------------------------------------------- /Terraform/application-server/variables.tf: -------------------------------------------------------------------------------- 1 | variable "ami-id" { 2 | type = string 3 | } 4 | 5 | variable "iam-instance-profile" { 6 | default = "" 7 | type = string 8 | } 9 | 10 | variable "instance-type" { 11 | type = string 12 | default = "t2.micro" 13 | } 14 | 15 | variable "name" { 16 | type = string 17 | } 18 | 19 | variable "key-pair" { 20 | type = string 21 | } 22 | 23 | variable "network-interface-id" { 24 | type = string 25 | } 26 | 27 | variable "device-index" { 28 | type = number 29 | } 30 | 31 | variable "repository-url" { 32 | type = string 33 | } 34 | -------------------------------------------------------------------------------- /Terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "aws-access-key" { 2 | type = string 3 | } 4 | 5 | variable "aws-secret-key" { 6 | type = string 7 | } 8 | 9 | variable "aws-region" { 10 | type = string 11 | } 12 | 13 | variable "admin-username" { 14 | type = string 15 | } 16 | 17 | variable "admin-password" { 18 | type = string 19 | } 20 | 21 | variable "admin-fullname" { 22 | type = string 23 | } 24 | 25 | variable "admin-email" { 26 | type = string 27 | } 28 | 29 | variable "remote-repo" { 30 | type = string 31 | } 32 | 33 | variable "job-name" { 34 | type = string 35 | } 36 | 37 | variable "secrets" { 38 | type = map(string) 39 | } -------------------------------------------------------------------------------- /server/test/integration/index.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from "chai"; 2 | import chaiHttp from "chai-http"; 3 | import app from "../../src/index.js"; 4 | 5 | chai.use(chaiHttp); 6 | chai.should(); 7 | 8 | describe("GET /users", () => { 9 | it("Should display a list of users, fetched from db", (done) => { 10 | chai 11 | .request(app) 12 | .get("/users") 13 | .then((res) => { 14 | expect(res).to.be.html; 15 | res.text.should.match(/Users<\/h1>/g); 16 | res.text.should.match(/.*<\/h3>/g); 17 | done(); 18 | }) 19 | .catch((err) => { 20 | done(err); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /Terraform/s3.tf: -------------------------------------------------------------------------------- 1 | # S3 Bucket storing logs 2 | 3 | resource "aws_s3_bucket" "simple-web-app-logs" { 4 | bucket = "kevindenotariis-simple-web-app-logs" 5 | acl = "private" 6 | } 7 | 8 | # S3 Bucket storing jenkins user data 9 | 10 | resource "aws_s3_bucket" "jenkins-config" { 11 | bucket = "kevindenotariis-jenkins-config" 12 | acl = "private" 13 | } 14 | 15 | # To upload all the config files in the folder jenkins-config 16 | 17 | resource "aws_s3_bucket_object" "jenkins-config" { 18 | bucket = aws_s3_bucket.jenkins-config.id 19 | for_each = fileset("jenkins-config/", "*") 20 | key = each.value 21 | source = "jenkins-config/${each.value}" 22 | etag = filemd5("jenkins-config/${each.value}") 23 | } -------------------------------------------------------------------------------- /server/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import got from "got"; 4 | 5 | const routes = (app) => { 6 | app.get("/", (req, res) => { 7 | return res.render("index.html", { 8 | title: "My Web App With a CI / CD Pipeline", 9 | }); 10 | }); 11 | 12 | app.get("/api/users", (req, res) => { 13 | const users = JSON.parse( 14 | fs.readFileSync(path.join(__dirname, "./db.json")) 15 | ); 16 | 17 | return res.json(users); 18 | }); 19 | 20 | app.get("/users", async (req, res) => { 21 | const users = await got.get(`http://localhost:8000/api/users`).json(); 22 | 23 | return res.render("users.html", { 24 | users: users, 25 | }); 26 | }); 27 | }; 28 | 29 | export default routes; 30 | -------------------------------------------------------------------------------- /server/test/unit/index.js: -------------------------------------------------------------------------------- 1 | import chai, { expect } from "chai"; 2 | import chaiHttp from "chai-http"; 3 | import app from "../../src/index.js"; 4 | 5 | chai.use(chaiHttp); 6 | chai.should(); 7 | 8 | describe("GET /", () => { 9 | it("Should return an HTML page", (done) => { 10 | chai 11 | .request(app) 12 | .get("/") 13 | .end(async (err, res) => { 14 | if (err) done(err); 15 | expect(res).to.be.html; 16 | done(); 17 | }); 18 | }); 19 | it("Should contain a title as an

tag", (done) => { 20 | chai 21 | .request(app) 22 | .get("/") 23 | .end(async (err, res) => { 24 | if (err) done(err); 25 | res.text.should.match(/

.*<\/h1>/g); 26 | done(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from "path"; 3 | import ejs from "ejs"; 4 | import cors from "cors"; 5 | import helmet from "helmet"; 6 | 7 | import routes from "./routes"; 8 | 9 | const app = express(); 10 | 11 | const PORT = 8000; 12 | 13 | app.set("view engine", "ejs"); 14 | app.set("views", path.join(__dirname, "views")); 15 | app.engine("html", ejs.renderFile); 16 | 17 | app.use(express.static(path.join(__dirname, "public"))); 18 | 19 | app.use(express.json()); 20 | app.use(express.urlencoded({ extended: true })); 21 | 22 | app.use(helmet()); 23 | 24 | app.use( 25 | cors({ 26 | origin: (origin, cb) => cb(null, true), 27 | credentials: true, 28 | }) 29 | ); 30 | 31 | routes(app); 32 | 33 | app.listen(PORT, () => { 34 | console.log(`Server listening on ${PORT}`); 35 | }); 36 | 37 | export default app; 38 | -------------------------------------------------------------------------------- /server/src/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | Simple Web App 11 | 25 | 26 | 27 |
28 |

<%= title %>

29 |

Try to navigate to /users

30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /server/src/views/users.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | Simple Web App 11 | 25 | 26 | 27 |
28 |

Users

29 | <% users.map(user => { %> 30 |

<%=user.name %>

31 | <%})%> 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /Terraform/application-server/user_data.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | sudo yum update -y 4 | 5 | # Install Docker 6 | sudo amazon-linux-extras install docker 7 | 8 | # Start Docker 9 | sudo systemctl start docker 10 | sudo systemctl enable docker 11 | 12 | # Create a shell script to run the server by taking the image tagged as simple-web-app:release from the ECR 13 | cat << EOT > start-website 14 | /bin/sh -e -c 'echo \$(aws ecr get-login-password --region us-east-1) | docker login -u AWS --password-stdin ${repository_url}' 15 | sudo docker pull ${repository_url}:release 16 | sudo docker run -p 80:8000 ${repository_url}:release 17 | EOT 18 | 19 | # Move the script into the specific amazon ec2 linux start up folder, in order for the script to run after boot 20 | sudo mv start-website /var/lib/cloud/scripts/per-boot/start-website 21 | 22 | # Mark the script as executable 23 | sudo chmod +x /var/lib/cloud/scripts/per-boot/start-website 24 | 25 | # Run the script 26 | /var/lib/cloud/scripts/per-boot/start-website -------------------------------------------------------------------------------- /Terraform/jenkins-config/confirm_url.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | url_urlEncoded=$(python -c "import urllib;print urllib.quote(raw_input(), safe='')" <<< "$url") 3 | 4 | cookie_jar="$(mktemp)" 5 | full_crumb=$(curl -u "$user:$password" --cookie-jar "$cookie_jar" $url/crumbIssuer/api/xml?xpath=concat\(//crumbRequestField,%22:%22,//crumb\)) 6 | arr_crumb=(${full_crumb//:/ }) 7 | only_crumb=$(echo ${arr_crumb[1]}) 8 | 9 | curl -X POST -u "$user:$password" $url/setupWizard/configureInstance \ 10 | -H 'Accept: application/json, text/javascript, */*; q=0.01' \ 11 | -H 'X-Requested-With: XMLHttpRequest' \ 12 | -H "$full_crumb" \ 13 | -H 'Content-Type: application/x-www-form-urlencoded' \ 14 | -H 'Accept-Language: en,en-US;q=0.9,it;q=0.8' \ 15 | --cookie $cookie_jar \ 16 | --data-raw "rootUrl=$url_urlEncoded%2F&Jenkins-Crumb=$only_crumb&json=%7B%22rootUrl%22%3A%20%22$url_urlEncoded%2F%22%2C%20%22Jenkins-Crumb%22%3A%20%22$only_crumb%22%7D&core%3Aapply=&Submit=Save&json=%7B%22rootUrl%22%3A%20%22$url_urlEncoded%2F%22%2C%20%22Jenkins-Crumb%22%3A%20%22$only_crumb%22%7D" -------------------------------------------------------------------------------- /Terraform/ecr.tf: -------------------------------------------------------------------------------- 1 | # Production Repository 2 | 3 | resource "aws_ecr_repository" "simple-web-app" { 4 | name = "simple-web-app" 5 | image_tag_mutability = "MUTABLE" 6 | 7 | image_scanning_configuration { 8 | scan_on_push = true 9 | } 10 | 11 | tags = { 12 | Name = "Elastic Container Registry to store Docker Artifacts" 13 | } 14 | } 15 | 16 | # Staging Repository 17 | 18 | resource "aws_ecr_repository" "simple-web-app-staging" { 19 | name = "simple-web-app-staging" 20 | image_tag_mutability = "MUTABLE" 21 | 22 | image_scanning_configuration { 23 | scan_on_push = true 24 | } 25 | 26 | tags = { 27 | Name = "Elastic Container Registry to store Docker Artifacts" 28 | } 29 | } 30 | 31 | # Test Repository 32 | 33 | resource "aws_ecr_repository" "simple-web-app-test" { 34 | name = "simple-web-app-test" 35 | image_tag_mutability = "MUTABLE" 36 | 37 | image_scanning_configuration { 38 | scan_on_push = true 39 | } 40 | 41 | tags = { 42 | Name = "Elastic Container Registry to store Docker Artifacts" 43 | } 44 | } -------------------------------------------------------------------------------- /Terraform/jenkins-config/download_install_plugins.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | cookie_jar="$(mktemp)" 3 | full_crumb=$(curl -u "$user:$password" --cookie-jar "$cookie_jar" $url/crumbIssuer/api/xml?xpath=concat\(//crumbRequestField,%22:%22,//crumb\)) 4 | arr_crumb=(${full_crumb//:/ }) 5 | only_crumb=$(echo ${arr_crumb[1]}) 6 | 7 | # MAKE THE REQUEST TO DOWNLOAD AND INSTALL REQUIRED MODULES 8 | curl -X POST -u "$user:$password" $url/pluginManager/installPlugins \ 9 | -H 'Accept: application/json, text/javascript, */*; q=0.01' \ 10 | -H 'X-Requested-With: XMLHttpRequest' \ 11 | -H "$full_crumb" \ 12 | -H 'Content-Type: application/json' \ 13 | -H 'Accept-Language: en,en-US;q=0.9,it;q=0.8' \ 14 | --cookie $cookie_jar \ 15 | --data-raw "{'dynamicLoad':true,'plugins':['cloudbees-folder','antisamy-markup-formatter','build-timeout','credentials-binding','timestamper','ws-cleanup','ant','gradle','workflow-aggregator','github-branch-source','pipeline-github-lib','pipeline-stage-view','git','ssh-slaves','matrix-auth','pam-auth','ldap','email-ext','mailer','bitbucket','docker-workflow','blueocean'],'Jenkins-Crumb':'$only_crumb'}" -------------------------------------------------------------------------------- /Terraform/jenkins.tf: -------------------------------------------------------------------------------- 1 | module "jenkins" { 2 | source ="./jenkins-server" 3 | 4 | ami-id = "ami-0742b4e673072066f" # AMI for an Amazon Linux instance for region: us-east-1 5 | iam-instance-profile = aws_iam_instance_profile.jenkins.name 6 | key-pair = aws_key_pair.jenkins-key.key_name 7 | name = "jenkins" 8 | device-index = 0 9 | network-interface-id = aws_network_interface.jenkins.id 10 | repository-url = aws_ecr_repository.simple-web-app.repository_url 11 | repository-test-url = aws_ecr_repository.simple-web-app-test.repository_url 12 | repository-staging-url = aws_ecr_repository.simple-web-app-staging.repository_url 13 | instance-id = module.application-server.instance-id 14 | public-dns = aws_eip.jenkins.public_dns 15 | admin-username = var.admin-username 16 | admin-password = var.admin-password 17 | admin-fullname = var.admin-fullname 18 | admin-email = var.admin-email 19 | bucket-logs-name = aws_s3_bucket.simple-web-app-logs.id 20 | bucket-config-name = aws_s3_bucket.jenkins-config.id 21 | remote-repo = var.remote-repo 22 | job-name = var.job-name 23 | job-id = random_id.job-id.id 24 | } -------------------------------------------------------------------------------- /Terraform/jenkins-server/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_instance" "default" { 2 | ami = var.ami-id 3 | iam_instance_profile = var.iam-instance-profile 4 | instance_type = var.instance-type 5 | key_name = var.key-pair 6 | network_interface { 7 | device_index = var.device-index 8 | network_interface_id = var.network-interface-id 9 | } 10 | 11 | user_data = templatefile( 12 | "${path.module}/user_data.sh", 13 | { 14 | repository_url = var.repository-url, 15 | repository_test_url = var.repository-test-url, 16 | repository_staging_url = var.repository-staging-url, 17 | instance_id = var.instance-id, 18 | bucket_logs_name = var.bucket-logs-name, 19 | public_dns = var.public-dns, 20 | admin_username = var.admin-username, 21 | admin_password = var.admin-password, 22 | admin_fullname = var.admin-fullname, 23 | admin_email = var.admin-email, 24 | remote_repo = var.remote-repo, 25 | job_name = var.job-name, 26 | job_id = var.job-id, 27 | bucket_config_name = var.bucket-config-name 28 | } 29 | ) 30 | 31 | tags = { 32 | Name = var.name 33 | } 34 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kevin De Notariis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Build a complete CI/CD Pipeline and its infrastructure with AWS — Jenkins — Bitbucket — Docker — Terraform 2 | 3 | This Repository is the completed project from the tutorial I wrote on Medium: 4 | 5 | - STEP 1 --> https://faun.pub/build-a-complete-ci-cd-pipeline-and-its-infrastructure-with-aws-jenkins-bitbucket-docker-bd29968a99b6 6 | 7 | - STEP 2 --> https://kevin-denotariis.medium.com/build-a-complete-ci-cd-pipeline-and-its-infrastructure-with-aws-jenkins-bitbucket-docker-75bf8fb7d1c6 8 | 9 | - STEP 3 --> https://kevin-denotariis.medium.com/build-a-complete-ci-cd-pipeline-and-its-infrastructure-with-aws-jenkins-bitbucket-docker-22c49ad4674a 10 | 11 | - STEP 4 --> https://kevin-denotariis.medium.com/build-a-complete-ci-cd-pipeline-and-its-infrastructure-with-aws-jenkins-bitbucket-docker-2724b43897b8 12 | 13 | - STEP 5 --> https://kevin-denotariis.medium.com/build-a-complete-ci-cd-pipeline-and-its-infrastructure-with-aws-jenkins-bitbucket-docker-aa36f0f3bbb8 14 | 15 | - STEP 6 --> https://kevin-denotariis.medium.com/build-a-complete-ci-cd-pipeline-and-its-infrastructure-with-aws-jenkins-bitbucket-docker-9b55ea63d2ed 16 | 17 | Follow it to be able to implement it correctly! 18 | -------------------------------------------------------------------------------- /server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpackNodeExternals = require("webpack-node-externals"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: "./src/index.js", 7 | externals: [webpackNodeExternals()], 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.js$/, 12 | exclude: /node_modules/, 13 | use: "babel-loader", 14 | }, 15 | { 16 | test: /\.(html)$/, 17 | loader: "html-loader", 18 | options: { 19 | esModule: false, 20 | }, 21 | }, 22 | { 23 | test: /\.css$/i, 24 | use: ["style-loader", "css-loader"], 25 | }, 26 | ], 27 | }, 28 | plugins: [ 29 | new CopyWebpackPlugin({ 30 | patterns: [ 31 | { 32 | from: "./src/views/", 33 | to: "./views", 34 | }, 35 | { 36 | from: "./src/public/", 37 | to: "./public", 38 | }, 39 | { 40 | from: "./src/routes/db.json", 41 | to: "./", 42 | }, 43 | ], 44 | }), 45 | ], 46 | output: { 47 | filename: "bundle.js", 48 | path: path.resolve(__dirname, "dist"), 49 | clean: true, 50 | }, 51 | target: "node", 52 | }; 53 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --progress --mode production", 8 | "watch": "babel-watch -L ./src/index.js", 9 | "test:unit": "mocha --reporter mochawesome ./test/unit", 10 | "test:integration": "mocha --reporter mochawesome ./test/integration", 11 | "test:load": "loadtest -n 10000 http://localhost:8000" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@babel/core": "^7.13.15", 18 | "@babel/preset-env": "^7.13.15", 19 | "@babel/register": "^7.13.14", 20 | "babel-loader": "^8.2.2", 21 | "babel-watch": "^7.4.1", 22 | "chai": "^4.3.4", 23 | "chai-http": "^4.3.0", 24 | "copy-webpack-plugin": "^8.1.1", 25 | "core-js": "^3.10.1", 26 | "css-loader": "^5.2.1", 27 | "html-loader": "^2.1.2", 28 | "loadtest": "^5.1.2", 29 | "mocha": "^8.3.2", 30 | "mochawesome": "^6.2.2", 31 | "regenerator-runtime": "^0.13.7", 32 | "webpack": "^5.31.2", 33 | "webpack-cli": "^4.6.0", 34 | "webpack-node-externals": "^2.5.2" 35 | }, 36 | "dependencies": { 37 | "cors": "^2.8.5", 38 | "ejs": "^3.1.6", 39 | "express": "^4.17.1", 40 | "got": "^11.8.2", 41 | "helmet": "^4.4.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Terraform/jenkins-config/create_credentials.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Retrieve Secrets and Extract the Private key using a python command 4 | python -c "import sys;import json;print(json.loads(json.loads(raw_input())['SecretString'])['private'])" <<< $(aws secretsmanager get-secret-value --secret-id simple-web-app --region us-east-1) > ssh_tmp 5 | 6 | ssh_private_key=$(awk -v ORS='\\n' '1' ssh_tmp) 7 | 8 | rm ssh_tmp 9 | 10 | cookie_jar="$(mktemp)" 11 | full_crumb=$(curl -u "$user:$password" --cookie-jar "$cookie_jar" $url/crumbIssuer/api/xml?xpath=concat\(//crumbRequestField,%22:%22,//crumb\)) 12 | arr_crumb=(${full_crumb//:/ }) 13 | only_crumb=$(echo ${arr_crumb[1]}) 14 | 15 | curl -u "$user:$password" -X POST "$url/credentials/store/system/domain/_/createCredentials" \ 16 | -H "$full_crumb" \ 17 | --cookie $cookie_jar \ 18 | --data-urlencode "json={ 19 | '': '2', 20 | 'credentials': { 21 | 'scope': 'GLOBAL', 22 | 'id': '', 23 | 'username': 'Git', 24 | 'password': '', 25 | 'description': '', 26 | 'privateKeySource': { 27 | 'value': '0', 28 | 'stapler-class': 'com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey\$DirectEntryPrivateKeySource', 29 | 'privateKey': \"$ssh_private_key\" 30 | }, 31 | 'stapler-class': 'com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey', 32 | }, 33 | 'Jenkins-Crumb': '$only_crumb' 34 | }" -------------------------------------------------------------------------------- /Terraform/jenkins-server/variables.tf: -------------------------------------------------------------------------------- 1 | variable "ami-id" { 2 | type = string 3 | } 4 | 5 | variable "iam-instance-profile" { 6 | default = "" 7 | type = string 8 | } 9 | 10 | variable "instance-type" { 11 | type = string 12 | default = "t2.micro" 13 | } 14 | 15 | variable "name" { 16 | type = string 17 | } 18 | 19 | variable "key-pair" { 20 | type = string 21 | } 22 | 23 | variable "network-interface-id" { 24 | type = string 25 | } 26 | 27 | variable "device-index" { 28 | type = number 29 | } 30 | 31 | variable "repository-url" { 32 | type = string 33 | } 34 | 35 | variable "repository-test-url" { 36 | type = string 37 | } 38 | 39 | variable "repository-staging-url" { 40 | type = string 41 | } 42 | 43 | variable "instance-id" { 44 | type = string 45 | } 46 | 47 | variable "public-dns" { 48 | type = string 49 | } 50 | 51 | variable "admin-username" { 52 | type = string 53 | } 54 | 55 | variable "admin-password" { 56 | type = string 57 | } 58 | 59 | variable "admin-email" { 60 | type = string 61 | } 62 | 63 | variable "admin-fullname" { 64 | type = string 65 | } 66 | 67 | variable "bucket-logs-name" { 68 | type = string 69 | } 70 | 71 | variable "bucket-config-name" { 72 | type = string 73 | } 74 | 75 | variable "remote-repo" { 76 | type = string 77 | } 78 | 79 | variable "job-name" { 80 | type = string 81 | } 82 | 83 | variable "job-id" { 84 | type = string 85 | } -------------------------------------------------------------------------------- /Terraform/jenkins-config/create_admin_user.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | old_password=$(sudo cat /var/lib/jenkins/secrets/initialAdminPassword) 3 | 4 | # NEW ADMIN CREDENTIALS URL ENCODED USING PYTHON 5 | password_URLEncoded=$(python -c "import urllib;print urllib.quote(raw_input(), safe='')" <<< "$password") 6 | username_URLEncoded=$(python -c "import urllib;print urllib.quote(raw_input(), safe='')" <<< "$user") 7 | fullname_URLEncoded=$(python -c "import urllib;print urllib.quote(raw_input(), safe='')" <<< "$admin_fullname") 8 | email_URLEncoded=$(python -c "import urllib;print urllib.quote(raw_input(), safe='')" <<< "$admin_email") 9 | 10 | # GET THE CRUMB AND COOKIE 11 | cookie_jar="$(mktemp)" 12 | full_crumb=$(curl -u "admin:$old_password" --cookie-jar "$cookie_jar" $url/crumbIssuer/api/xml?xpath=concat\(//crumbRequestField,%22:%22,//crumb\)) 13 | arr_crumb=(${full_crumb//:/ }) 14 | only_crumb=$(echo ${arr_crumb[1]}) 15 | 16 | # MAKE THE REQUEST TO CREATE AN ADMIN USER 17 | curl -X POST -u "admin:$old_password" $url/setupWizard/createAdminUser \ 18 | -H "Accept: application/json, text/javascript" \ 19 | -H "X-Requested-With: XMLHttpRequest" \ 20 | -H "$full_crumb" \ 21 | -H "Content-Type: application/x-www-form-urlencoded" \ 22 | --cookie $cookie_jar \ 23 | --data-raw "username=$username_URLEncoded&password1=$password_URLEncoded&password2=$password_URLEncoded&fullname=$fullname_URLEncoded&email=$email_URLEncoded&Jenkins-Crumb=$only_crumb&json=%7B%22username%22%3A%20%22$username_URLEncoded%22%2C%20%22password1%22%3A%20%22$password_URLEncoded%22%2C%20%22%24redact%22%3A%20%5B%22password1%22%2C%20%22password2%22%5D%2C%20%22password2%22%3A%20%22$password_URLEncoded%22%2C%20%22fullname%22%3A%20%22$fullname_URLEncoded%22%2C%20%22email%22%3A%20%22$email_URLEncoded%22%2C%20%22Jenkins-Crumb%22%3A%20%22$only_crumb%22%7D&core%3Aapply=&Submit=Save&json=%7B%22username%22%3A%20%22$username_URLEncoded%22%2C%20%22password1%22%3A%20%22$password_URLEncoded%22%2C%20%22%24redact%22%3A%20%5B%22password1%22%2C%20%22password2%22%5D%2C%20%22password2%22%3A%20%22$password_URLEncoded%22%2C%20%22fullname%22%3A%20%22$fullname_URLEncoded%22%2C%20%22email%22%3A%20%22$email_URLEncoded%22%2C%20%22Jenkins-Crumb%22%3A%20%22$only_crumb%22%7D" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | # Mochawesome 121 | mochawesome-report 122 | 123 | # Terraform 124 | .terraform* 125 | *.tfstate* 126 | *.tfvars 127 | 128 | # Keys 129 | *.key 130 | *.pem -------------------------------------------------------------------------------- /Terraform/jenkins-server/user_data.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | sudo yum update -y 4 | 5 | # Install Git 6 | sudo yum install -y git 7 | 8 | # Install Jenkins 9 | sudo wget -O /etc/yum.repos.d/jenkins.repo \ 10 | https://pkg.jenkins.io/redhat-stable/jenkins.repo 11 | sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key 12 | sudo yum upgrade -y 13 | sudo yum install -y jenkins java-1.8.0-openjdk-devel 14 | sudo systemctl daemon-reload 15 | 16 | # Install Docker 17 | sudo amazon-linux-extras install docker 18 | 19 | # Start Jenkins 20 | sudo systemctl start jenkins 21 | 22 | # Enable jenkins to run on boot 23 | sudo systemctl enable jenkins 24 | 25 | # Start Docker 26 | sudo systemctl start docker 27 | 28 | # Enable Docker to run on boot 29 | sudo systemctl enable docker 30 | 31 | # Let Jenkins and the current user use docker 32 | sudo usermod -a -G docker ec2-user 33 | sudo usermod -a -G docker jenkins 34 | 35 | # Create the opt folder in the jenkins home 36 | sudo mkdir /var/lib/jenkins/opt 37 | sudo chown jenkins /var/lib/jenkins/opt 38 | sudo chgroup jenkins /var/lib/jenkins/opt 39 | 40 | # Download and install arachni as jenkins user 41 | wget https://github.com/Arachni/arachni/releases/download/v1.5.1/arachni-1.5.1-0.5.12-linux-x86_64.tar.gz 42 | tar -zxf arachni-1.5.1-0.5.12-linux-x86_64.tar.gz 43 | rm arachni-1.5.1-0.5.12-linux-x86_64.tar.gz 44 | sudo chown -R jenkins arachni-1.5.1-0.5.12/ 45 | sudo chgrp -R jenkins arachni-1.5.1-0.5.12/ 46 | sudo mv arachni-1.5.1-0.5.12 /var/lib/jenkins/opt 47 | 48 | # Save the instance_id, repositories urls and bucket name to use in the pipeline 49 | sudo /bin/bash -c "echo ${repository_url} > /var/lib/jenkins/opt/repository_url" 50 | sudo /bin/bash -c "echo ${repository_test_url} > /var/lib/jenkins/opt/repository_test_url" 51 | sudo /bin/bash -c "echo ${repository_staging_url} > /var/lib/jenkins/opt/repository_staging_url" 52 | sudo /bin/bash -c "echo ${instance_id} > /var/lib/jenkins/opt/instance_id" 53 | sudo /bin/bash -c "echo ${bucket_logs_name} > /var/lib/jenkins/opt/bucket_name" 54 | 55 | # Change ownership and group of these files 56 | sudo chown -R jenkins /var/lib/jenkins/opt/ 57 | sudo chgrp -R jenkins /var/lib/jenkins/opt/ 58 | 59 | # Wait for Jenkins to boot up 60 | sudo sleep 60 61 | 62 | ##################################################### 63 | ####### SET UP JENKINS ####### 64 | ##################################################### 65 | 66 | #---------------------------------------------# 67 | #------> DEFINE THE GLOBAL VARIABLES <--------# 68 | #---------------------------------------------# 69 | 70 | export url="http://${public_dns}:8080" 71 | export user="${admin_username}" 72 | export password="${admin_password}" 73 | export admin_fullname="${admin_fullname}" 74 | export admin_email="${admin_email}" 75 | export remote="${remote_repo}" 76 | export jobName="${job_name}" 77 | export jobID="${job_id}" 78 | 79 | #---------------------------------------------# 80 | #-----> COPY THE CONFIG FILES FROM S3 <-------# 81 | #---------------------------------------------# 82 | 83 | sudo aws s3 cp s3://${bucket_config_name}/ ./ --recursive 84 | sudo chmod +x *.sh 85 | 86 | #---------------------------------------------# 87 | #----------> RUN THE CONFIG FILES <----------# 88 | #---------------------------------------------# 89 | 90 | ./create_admin_user.sh 91 | ./download_install_plugins.sh 92 | sudo sleep 120 93 | ./confirm_url.sh 94 | ./create_credentials.sh 95 | 96 | # Output the credentials id in a credentials_id file 97 | python -c "import sys;import json;print(json.loads(raw_input())['credentials'][0]['id'])" <<< $(./get_credentials_id.sh) > credentials_id 98 | 99 | ./create_multibranch_pipeline.sh 100 | 101 | #---------------------------------------------# 102 | #---------> DELETE THE CONFIG FILES <---------# 103 | #---------------------------------------------# 104 | 105 | sudo rm *.sh credentials_id 106 | 107 | reboot -------------------------------------------------------------------------------- /Terraform/iam.tf: -------------------------------------------------------------------------------- 1 | # Web App 2 | 3 | resource "aws_iam_instance_profile" "simple-web-app" { 4 | name = "simple-web-app" 5 | role = aws_iam_role.simple-web-app.name 6 | } 7 | 8 | resource "aws_iam_role" "simple-web-app" { 9 | name = "simple-web-app" 10 | 11 | assume_role_policy = jsonencode({ 12 | Version = "2012-10-17" 13 | Statement = [ 14 | { 15 | Action = "sts:AssumeRole" 16 | Effect = "Allow" 17 | Sid = "" 18 | Principal = { 19 | Service = "ec2.amazonaws.com" 20 | } 21 | }, 22 | ] 23 | }) 24 | 25 | managed_policy_arns = [aws_iam_policy.ecr-access.arn] 26 | } 27 | 28 | # Jenkins 29 | 30 | resource "aws_iam_instance_profile" "jenkins" { 31 | name = "jenkins" 32 | role = aws_iam_role.jenkins.name 33 | } 34 | 35 | resource "aws_iam_role" "jenkins" { 36 | name = "jenkins" 37 | 38 | assume_role_policy = jsonencode({ 39 | Version = "2012-10-17" 40 | Statement = [ 41 | { 42 | Action = "sts:AssumeRole" 43 | Effect = "Allow" 44 | Sid = "" 45 | Principal = { 46 | Service = "ec2.amazonaws.com" 47 | } 48 | }, 49 | ] 50 | }) 51 | 52 | managed_policy_arns = [ aws_iam_policy.ecr-access.arn, 53 | aws_iam_policy.s3-access.arn, 54 | aws_iam_policy.ec2-access.arn, 55 | aws_iam_policy.secrets-access.arn] 56 | } 57 | 58 | 59 | # Policy: Ec2 Reboot access 60 | 61 | resource "aws_iam_policy" "ec2-access" { 62 | name = "ec2-reboot-access" 63 | policy = < AmazonEC2ContainerRegistryPowerUser 82 | 83 | resource "aws_iam_policy" "ecr-access" { 84 | name = "ecr-access" 85 | policy = < associate Jenkins subnet to route table 58 | 59 | resource "aws_route_table_association" "jenkins-subnet" { 60 | subnet_id = aws_subnet.subnet-public-jenkins.id 61 | route_table_id = aws_route_table.allow-outgoing-access.id 62 | } 63 | 64 | # 5.2 Create a Route Table Association --> associate Simple Web App subnet to route table 65 | 66 | resource "aws_route_table_association" "web-app-subnet" { 67 | subnet_id = aws_subnet.subnet-public-web-app.id 68 | route_table_id = aws_route_table.allow-outgoing-access.id 69 | } 70 | 71 | # 6.1 Create a Security Group for inbound web traffic 72 | 73 | resource "aws_security_group" "allow-web-traffic" { 74 | name = "allow-web-traffic" 75 | description = "Allow HTTP / HTTPS inbound traffic" 76 | vpc_id = aws_vpc.simple-web-app.id 77 | 78 | ingress { 79 | description = "HTTP" 80 | from_port = 80 81 | to_port = 80 82 | protocol = "tcp" 83 | cidr_blocks = ["0.0.0.0/0"] 84 | } 85 | 86 | ingress { 87 | description = "HTTPS" 88 | from_port = 443 89 | to_port = 443 90 | protocol = "tcp" 91 | cidr_blocks = ["0.0.0.0/0"] 92 | } 93 | } 94 | 95 | # 6.2 Create a Security Group for inbound ssh 96 | 97 | resource "aws_security_group" "allow-ssh-traffic" { 98 | name = "allow-ssh-traffic" 99 | description = "Allow SSH inbound traffic" 100 | vpc_id = aws_vpc.simple-web-app.id 101 | 102 | ingress { 103 | description = "SSH" 104 | from_port = 22 105 | to_port = 22 106 | protocol = "tcp" 107 | cidr_blocks = ["0.0.0.0/0"] 108 | } 109 | } 110 | 111 | # 6.3 Create a Security Group for inbound traffic to Jenkins 112 | 113 | resource "aws_security_group" "allow-jenkins-traffic" { 114 | name = "allow-jenkins-traffic" 115 | description = "Allow jenkins inbound traffic" 116 | vpc_id = aws_vpc.simple-web-app.id 117 | 118 | ingress { 119 | description = "Jenkins" 120 | from_port = 8080 121 | to_port = 8080 122 | protocol = "tcp" 123 | cidr_blocks = ["0.0.0.0/0"] 124 | } 125 | } 126 | 127 | # 6.4 Create a Security Group for inbound security checks 128 | 129 | resource "aws_security_group" "allow-staging-traffic" { 130 | name = "allow-stagin-traffic" 131 | description = "Allow Inbound traffic for security checks" 132 | vpc_id = aws_vpc.simple-web-app.id 133 | 134 | ingress { 135 | description = "Staging" 136 | from_port = 8000 137 | to_port = 8000 138 | protocol = "tcp" 139 | cidr_blocks = ["0.0.0.0/0"] 140 | } 141 | } 142 | 143 | # 6.5 Create a Security Group for outbound traffic 144 | 145 | resource "aws_security_group" "allow-all-outbound" { 146 | name = "allow-all-outbound" 147 | description = "Allow all outbound traffic" 148 | vpc_id = aws_vpc.simple-web-app.id 149 | 150 | egress { 151 | from_port = 0 152 | to_port = 0 153 | protocol = "-1" 154 | cidr_blocks = ["0.0.0.0/0"] 155 | } 156 | } 157 | 158 | # 7.1 Create a Network Interface for jenkins 159 | 160 | resource "aws_network_interface" "jenkins" { 161 | subnet_id = aws_subnet.subnet-public-jenkins.id 162 | private_ips = ["10.0.1.50"] 163 | security_groups = [aws_security_group.allow-all-outbound.id, 164 | aws_security_group.allow-ssh-traffic.id, 165 | aws_security_group.allow-jenkins-traffic.id, 166 | aws_security_group.allow-staging-traffic.id] 167 | } 168 | 169 | # 7.2 Create a Network Interface for Simple Web App 170 | 171 | resource "aws_network_interface" "simple-web-app" { 172 | subnet_id = aws_subnet.subnet-public-web-app.id 173 | private_ips = ["10.0.3.50"] 174 | security_groups = [ aws_security_group.allow-all-outbound.id, 175 | aws_security_group.allow-ssh-traffic.id, 176 | aws_security_group.allow-web-traffic.id ] 177 | } 178 | 179 | # 8.1 Assign an Elastic IP to the Network Interface of Jenkins 180 | 181 | resource "aws_eip" "jenkins" { 182 | vpc = true 183 | network_interface = aws_network_interface.jenkins.id 184 | associate_with_private_ip = "10.0.1.50" 185 | depends_on = [ 186 | aws_internet_gateway.simple-web-app 187 | ] 188 | } 189 | 190 | # 8.2 Assign an Elastic IP to the Network Interface of Simple Web App 191 | 192 | resource "aws_eip" "simple-web-app" { 193 | vpc = true 194 | network_interface = aws_network_interface.simple-web-app.id 195 | associate_with_private_ip = "10.0.3.50" 196 | depends_on = [ 197 | aws_internet_gateway.simple-web-app 198 | ] 199 | } -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | def testImage 2 | def stagingImage 3 | def productionImage 4 | def REPOSITORY 5 | def REPOSITORY_TEST 6 | def RESPOSITORY_STAGING 7 | def GIT_COMMIT_HASH 8 | def INSTANCE_ID 9 | def ACCOUNT_REGISTRY_PREFIX 10 | def S3_LOGS 11 | def DATE_NOW 12 | def SLACK_TOKEN 13 | def CHANNEL_ID = "" 14 | 15 | pipeline { 16 | agent any 17 | stages { 18 | stage("Set Up") { 19 | steps { 20 | echo "Logging into the private AWS Elastic Container Registry" 21 | script { 22 | // Set environment variables 23 | GIT_COMMIT_HASH = sh (script: "git log -n 1 --pretty=format:'%H'", returnStdout: true) 24 | REPOSITORY = sh (script: "cat \$HOME/opt/repository_url", returnStdout: true) 25 | REPOSITORY_TEST = sh (script: "cat \$HOME/opt/repository_test_url", returnStdout: true) 26 | REPOSITORY_STAGING = sh (script: "cat \$HOME/opt/repository_staging_url", returnStdout: true) 27 | INSTANCE_ID = sh (script: "cat \$HOME/opt/instance_id", returnStdout: true) 28 | S3_LOGS = sh (script: "cat \$HOME/opt/bucket_name", returnStdout: true) 29 | DATE_NOW = sh (script: "date +%Y%m%d", returnStdout: true) 30 | 31 | // To parse and extract the Slack Token from the JSON response of AWS 32 | SLACK_TOKEN = sh (script: "python -c \"import sys;import json;print(json.loads(json.loads(raw_input())['SecretString'])['slackToken'])\" <<< \$(aws secretsmanager get-secret-value --secret-id simple-web-app --region us-east-1)", returnStdout: true) 33 | 34 | REPOSITORY = REPOSITORY.trim() 35 | REPOSITORY_TEST = REPOSITORY_TEST.trim() 36 | REPOSITORY_STAGING = REPOSITORY_STAGING.trim() 37 | S3_LOGS = S3_LOGS.trim() 38 | DATE_NOW = DATE_NOW.trim() 39 | SLACK_TOKEN = SLACK_TOKEN.trim() 40 | 41 | ACCOUNT_REGISTRY_PREFIX = (REPOSITORY.split("/"))[0] 42 | 43 | // Log into ECR 44 | sh """ 45 | /bin/sh -e -c 'echo \$(aws ecr get-login-password --region us-east-1) | docker login -u AWS --password-stdin $ACCOUNT_REGISTRY_PREFIX' 46 | """ 47 | } 48 | } 49 | } 50 | stage("Build Test Image") { 51 | steps { 52 | echo 'Start building the project docker image for tests' 53 | script { 54 | testImage = docker.build("$REPOSITORY_TEST:$GIT_COMMIT_HASH", "-f ./Dockerfile.test .") 55 | testImage.push() 56 | } 57 | } 58 | } 59 | stage("Run Unit Tests") { 60 | steps { 61 | echo 'Run unit tests in the docker image' 62 | script { 63 | def textMessage 64 | def inError 65 | try { 66 | testImage.inside('-v $WORKSPACE:/output -u root') { 67 | sh """ 68 | cd /opt/app/server 69 | npm run test:unit 70 | 71 | # Save reports to be uploaded afterwards 72 | if test -d /output/unit ; then 73 | rm -R /output/unit 74 | fi 75 | mv mochawesome-report /output/unit 76 | """ 77 | } 78 | 79 | // Fill the slack message with the success message 80 | textMessage = "Commit hash: $GIT_COMMIT_HASH -- Has passed unit tests" 81 | inError = false 82 | 83 | } catch(e) { 84 | 85 | echo "$e" 86 | // Fill the slack message with the failure message 87 | textMessage = "Commit hash: $GIT_COMMIT_HASH -- Has failed on unit tests" 88 | inError = true 89 | 90 | } finally { 91 | 92 | // Upload the unit tests results to S3 93 | sh "aws s3 cp ./unit/ s3://$S3_LOGS/$DATE_NOW/$GIT_COMMIT_HASH/unit/ --recursive" 94 | 95 | // Send Slack notification with the result of the tests 96 | sh""" 97 | curl --location --request POST 'https://slack.com/api/chat.postMessage' \ 98 | --header 'Authorization: Bearer $SLACK_TOKEN' \ 99 | --header 'Content-Type: application/json' \ 100 | --data-raw '{ 101 | "channel": \"$CHANNEL_ID\", 102 | "text": \"$textMessage\" 103 | }' 104 | """ 105 | if(inError) { 106 | // Send an error signal to stop the pipeline 107 | error("Failed unit tests") 108 | } 109 | } 110 | } 111 | } 112 | } 113 | stage("Run Integration Tests") { 114 | steps { 115 | echo 'Run Integration tests in the docker image' 116 | script { 117 | def textMessage 118 | def inError 119 | try { 120 | testImage.inside('-v $WORKSPACE:/output -u root') { 121 | sh """ 122 | cd /opt/app/server 123 | npm run test:integration 124 | 125 | # Save reports to be uploaded afterwards 126 | if test -d /output/integration ; then 127 | rm -R /output/integration 128 | fi 129 | mv mochawesome-report /output/integration 130 | """ 131 | } 132 | 133 | // Fill the slack message with the success message 134 | textMessage = "Commit hash: $GIT_COMMIT_HASH -- Has passed integration tests" 135 | inError = false 136 | 137 | } catch(e) { 138 | 139 | echo "$e" 140 | // Fill the slack message with the failure message 141 | textMessage = "Commit hash: $GIT_COMMIT_HASH -- Has failed on integration tests" 142 | inError = true 143 | 144 | } finally { 145 | 146 | // Upload the unit tests results to S3 147 | sh "aws s3 cp ./integration/ s3://$S3_LOGS/$DATE_NOW/$GIT_COMMIT_HASH/integration/ --recursive" 148 | 149 | // Send Slack notification with the result of the tests 150 | sh""" 151 | curl --location --request POST 'https://slack.com/api/chat.postMessage' \ 152 | --header 'Authorization: Bearer $SLACK_TOKEN' \ 153 | --header 'Content-Type: application/json' \ 154 | --data-raw '{ 155 | "channel": \"$CHANNEL_ID\", 156 | "text": \"$textMessage\" 157 | }' 158 | """ 159 | if(inError) { 160 | // Send an error signal to stop the pipeline 161 | error("Failed integration tests") 162 | } 163 | } 164 | } 165 | } 166 | } 167 | stage("Build Staging Image") { 168 | steps { 169 | echo 'Build the staging image for more tests' 170 | script { 171 | stagingImage = docker.build("$REPOSITORY_STAGING:$GIT_COMMIT_HASH") 172 | stagingImage.push() 173 | } 174 | } 175 | } 176 | stage("Run Load Balancing tests / Security Checks") { 177 | steps { 178 | echo 'Run load balancing tests and security checks' 179 | script { 180 | stagingImage.inside('-v $WORKSPACE:/output -u root') { 181 | sh """ 182 | cd /opt/app/server 183 | npm rm loadtest 184 | npm i loadtest 185 | npm run test:load > /output/load_test.txt 186 | """ 187 | } 188 | // Upload the load test results to S3 189 | sh "aws s3 cp ./load_test.txt s3://$S3_LOGS/$DATE_NOW/$GIT_COMMIT_HASH/" 190 | 191 | stagingImage.withRun('-p 8000:8000 -u root'){ 192 | sh """ 193 | # run arachni to check for common vulnerabilities 194 | \$HOME/opt/arachni-1.5.1-0.5.12/bin/arachni http://\$(hostname):8000 --check=xss,code_injection --report-save-path=simple-web-app.com.afr 195 | 196 | # Save report in html (zipped) 197 | \$HOME/opt/arachni-1.5.1-0.5.12/bin/arachni_reporter simple-web-app.com.afr --reporter=html:outfile=arachni_report.html.zip 198 | """ 199 | } 200 | // Upload the Arachni tests' results to S3 201 | sh "aws s3 cp ./arachni_report.html.zip s3://$S3_LOGS/$DATE_NOW/$GIT_COMMIT_HASH/" 202 | 203 | // Inform via slack that the Load Balancing and Security checks are completed 204 | sh""" 205 | curl --location --request POST 'https://slack.com/api/chat.postMessage' \ 206 | --header 'Authorization: Bearer $SLACK_TOKEN' \ 207 | --header 'Content-Type: application/json' \ 208 | --data-raw '{ 209 | "channel": \"$CHANNEL_ID\", 210 | "text": "Commit hash: $GIT_COMMIT_HASH -- Load Balancing tests and security checks have finished" 211 | }' 212 | """ 213 | } 214 | } 215 | } 216 | stage("Deploy to Fixed Server") { 217 | steps { 218 | echo 'Deploy release to production' 219 | script { 220 | productionImage = docker.build("$REPOSITORY:release") 221 | productionImage.push() 222 | sh "aws ec2 reboot-instances --region us-east-1 --instance-ids $INSTANCE_ID" 223 | } 224 | } 225 | } 226 | stage("Clean Up") { 227 | steps { 228 | echo 'Clean up local docker images' 229 | script { 230 | sh """ 231 | # Change the :latest with the current ones 232 | docker tag $REPOSITORY_TEST:$GIT_COMMIT_HASH $REPOSITORY_TEST:latest 233 | docker tag $REPOSITORY_STAGING:$GIT_COMMIT_HASH $REPOSITORY_STAGING:latest 234 | docker tag $REPOSITORY:release $REPOSITORY:latest 235 | 236 | # Remove the images 237 | docker image rm $REPOSITORY_TEST:$GIT_COMMIT_HASH 238 | docker image rm $REPOSITORY_STAGING:$GIT_COMMIT_HASH 239 | docker image rm $REPOSITORY:release 240 | 241 | # Remove dangling images 242 | docker image prune -f 243 | """ 244 | } 245 | echo 'Clean up config.json file with ECR Docker Credentials' 246 | script { 247 | sh """ 248 | rm $HOME/.docker/config.json 249 | """ 250 | } 251 | } 252 | } 253 | } 254 | } --------------------------------------------------------------------------------