├── 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 | }
--------------------------------------------------------------------------------