diff --git a/.gitignore b/.gitignore index 75ec3f0..c8618e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -.vscode/* \ No newline at end of file +.vscode/* +# terraform/ +terraform/.terraform +terraform/.terraform.lock.hcl +terraform/terraform.tfstate +terraform/terraform.tfstate.backup diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6c65a82 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.9-slim + +ENV PYTHONUNBUFFERED 1 + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE ${PORT} + +CMD ["python", "hello.py"] diff --git a/Project DevOps DEPI.docx b/Project DevOps DEPI.docx new file mode 100644 index 0000000..ae93018 Binary files /dev/null and b/Project DevOps DEPI.docx differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6ddd81b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + environment: + - ENVIRONMENT=DEV + - HOST=0.0.0.0 + - PORT=8000 + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_DB=0 + ports: + - "8000:8000" + depends_on: + - redis + command: python hello.py + container_name: app-container + image: johnsami/dockerized-app-build-app:latest + redis: + image: "redis:6.0-alpine" + ports: + - "6379:6379" + container_name: redis-container \ No newline at end of file diff --git a/jenkinsfile b/jenkinsfile new file mode 100644 index 0000000..bdc10ca --- /dev/null +++ b/jenkinsfile @@ -0,0 +1,224 @@ +pipeline { + agent any + + environment { + DOCKER_HUB_CREDENTIALS = credentials('docker-Hub-credentials') + DOCKER_IMAGE_NAME = 'johnsami/dockerized-app-build-app' + DOCKER_IMAGE_TAG = 'latest' + EMAIL_RECIPIENTS = 'johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com' + } + + stages { + stage('Cleanup') { + steps { + deleteDir() + } + } + + stage('Checkout') { + steps { + git url: 'https://github.com/johnsamey/DEPI-Project.git', branch: 'master', credentialsId: 'github' + } + } + + stage('Prepare Docker Image') { + steps { + script { + sh ''' + cd $WORKSPACE + + # Check if the Docker image exists + IMAGE_EXISTS=$(docker images -q $DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG) + + if [ -n "$IMAGE_EXISTS" ]; then + echo "Docker image exists. Deleting existing image..." + docker rmi $DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG + else + echo "Docker image does not exist. Proceeding to build a new image..." + fi + ''' + } + } + } + + stage('Build Docker Image') { + steps { + script { + sh ''' + cd $WORKSPACE + docker-compose build + docker tag $DOCKER_IMAGE_NAME $DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG + ''' + } + } + } + + stage('Run Tests') { + steps { + sh 'echo "Running tests..."' + } + } + + stage('Push to Docker-Hub') { + steps { + script { + sh ''' + cd $WORKSPACE + echo "$DOCKER_HUB_CREDENTIALS_PSW" | docker login -u "$DOCKER_HUB_CREDENTIALS_USR" --password-stdin + docker-compose push + ''' + } + } + } + } + + // triggers { + // pollSCM('H/5 * * * *') // Poll GitHub every 5 minutes + // } + post { + // success { + // mail to: "${EMAIL_RECIPIENTS}", + // subject: "✅ Build Successful: ${JOB_NAME} - Build #${BUILD_NUMBER}", + // body: """

Good news! The build was successful.

+ //

Job: ${JOB_NAME}

+ //

Build Number: ${BUILD_NUMBER}

+ //

Check the details at ${BUILD_URL}

""", + // mimeType: 'text/html' + // // attachLog: true + // } + + failure { + mail to: "${EMAIL_RECIPIENTS}", + subject: "❌ Build Failed: ${JOB_NAME} - Build #${BUILD_NUMBER}", + body: """

Oops! The build has failed.

+

Job: ${JOB_NAME}

+

Build Number: ${BUILD_NUMBER}

+

Check the console output at ${BUILD_URL}

""", + mimeType: 'text/html' + // attachLog: true + } + + unstable { + mail to: "${EMAIL_RECIPIENTS}", + subject: "⚠️ Build Unstable: ${JOB_NAME} - Build #${BUILD_NUMBER}", + body: """

The build is unstable.

+

Job: ${JOB_NAME}

+

Build Number: ${BUILD_NUMBER}

+

Check the console output at ${BUILD_URL}

""", + mimeType: 'text/html' + // attachLog: true + } + } +} + // post { + // always { + // emailext( + // to: 'mostafa77744333@gmail.com, johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com', + // subject: 'Test Email from Jenkins', + // body: 'This is a test email to verify Jenkins email notifications.', + // attachLog: false + // ) + + // mail to: 'mostafa77744333@gmail.com, johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com', + // subject: 'Test Email from Jenkins', + // body: 'This is a test email to verify Jenkins email notifications.' + // } + + // failure { + // emailext( + // to: 'mostafa77744333@gmail.com, johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com', + // subject: 'Build failed in Jenkins: ${JOB_NAME} - Build #${BUILD_NUMBER}', + // body: """

Build failed in Jenkins:

+ //

Job: ${JOB_NAME} - Build #${BUILD_NUMBER}

+ //

Check console output at ${BUILD_URL}

""", + // attachLog: true + // ) + // } + // unstable { + // emailext( + // to: 'mostafa77744333@gmail.com, johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com', + // subject: 'Build failed in Jenkins: ${JOB_NAME} - Build #${BUILD_NUMBER}', + // body: """

Build is unstable:

+ //

Job: ${JOB_NAME} - Build #${BUILD_NUMBER}

+ //

Check console output at ${BUILD_URL}

""", + // attachLog: true + // ) + // } + // success { + // emailext( + // subject: 'Build successful: ${JOB_NAME} [${BUILD_NUMBER}]', + // body: '''The build was successful: + // - Job: ${JOB_NAME} + // - Build number: ${BUILD_NUMBER} + // - Check console output at: ${BUILD_URL}''', + // to: 'mostafa77744333@gmail.com, johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com', + // attachLog: true + // ) + // } + // } + + +// post { +// failure { +// emailext( +// subject: 'Build failed: ${JOB_NAME} [${BUILD_NUMBER}]', +// body: '''Build failed in Jenkins: +// - Job: ${JOB_NAME} +// - Build number: ${BUILD_NUMBER} +// - Check console output at: ${BUILD_URL}''', +// recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']], +// to: 'mostafa77744333@gmail.com, johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com', +// attachLog: true +// ) +// } +// unstable { +// emailext( +// subject: 'Build unstable: ${JOB_NAME} [${BUILD_NUMBER}]', +// body: '''The build is unstable: +// - Job: ${JOB_NAME} +// - Build number: ${BUILD_NUMBER} +// - Check console output at: ${BUILD_URL}''', +// to: 'mostafa77744333@gmail.com, johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com', +// attachLog: true +// ) +// } +// success { +// emailext( +// subject: 'Build successful: ${JOB_NAME} [${BUILD_NUMBER}]', +// body: '''The build was successful: +// - Job: ${JOB_NAME} +// - Build number: ${BUILD_NUMBER} +// - Check console output at: ${BUILD_URL}''', +// to: 'mostafa77744333@gmail.com, johnhana567@gmail.com, khalid.salman1996@gmail.com, andrewadel3322@gmail.com', +// attachLog: true +// ) +// } +// } + + + + // post { + // always { + // script { + // emailext subject: "Jenkins Job: ${env.JOB_NAME} #${env.BUILD_NUMBER} - ${currentBuild.currentResult}", + // body: """ + // Job Name: ${env.JOB_NAME} + // Build Number: ${env.BUILD_NUMBER} + // Build Status: ${currentBuild.currentResult} + // Build URL: ${env.BUILD_URL} + // """, + // recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']], + // to: 'johnhana567@gmail.com' + // } + // } + // success { + // emailext subject: "SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER}", + // body: "Good news, the build was successful!", + // to: 'johnhana567@gmail.com' + // } + // failure { + // emailext subject: "FAILURE: ${env.JOB_NAME} #${env.BUILD_NUMBER}", + // body: "Unfortunately, the build has failed.", + // to: 'johnhana567@gmail.com' + // } + // } diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..f1d4126 --- /dev/null +++ b/plan.md @@ -0,0 +1,6 @@ +1. jenkins on an ec2 instance +2. configure docker & ansible agents in jenkins + 2.1. docker agent is used to build the image then run tests then push it to an image registry + 2.2. ansible agent is used to configure the ec2 ( install docker & pull the image ) +3. configure the notification channel in jenkins +4. testing jenkins auto build diff --git a/terraform/auto_var.auto.tfvars b/terraform/auto_var.auto.tfvars new file mode 100644 index 0000000..e3ffdf6 --- /dev/null +++ b/terraform/auto_var.auto.tfvars @@ -0,0 +1,10 @@ + # vpc + vpc_Name = "test-vpc" + vpc_cidr_block = "10.0.0.0/16" + public_subnets_cidr = ["10.0.1.0/24","10.0.2.0/24"] + availability_Zones_subnet = ["us-east-1a","us-east-1b"] # us-east-1a +#ec2 + ec2_Name = ["Docker","Jenkins"] + ami_id = "ami-0e86e20dae9224db8" # ami-0e86e20dae9224db8 + key_Name = "private_key" + instance_type = "t2.micro" # t2.micro \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..79067d0 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,16 @@ +module "vpc" { + source = "./modules/vpc" + vpc_Name = var.vpc_Name + vpc_cidr_block = var.vpc_cidr_block + public_subnets_cidr = var.public_subnets_cidr + availability_Zones_subnet = var.availability_Zones_subnet +} +module "ec2" { + source = "./modules/ec2" + ec2_Name = var.ec2_Name + vpc_id = module.vpc.vpc_id + public_subnet_ids = module.vpc.public_subnet_ids + ami_id = var.ami_id + key_Name = var.key_Name + instance_type = var.instance_type +} \ No newline at end of file diff --git a/terraform/modules/ec2/main.tf b/terraform/modules/ec2/main.tf new file mode 100644 index 0000000..5a7d47c --- /dev/null +++ b/terraform/modules/ec2/main.tf @@ -0,0 +1,96 @@ + + +resource "aws_instance" "public_ec2"{ + count =length(var.public_subnet_ids) + ami = var.ami_id + instance_type = var.instance_type + subnet_id = var.public_subnet_ids[count.index] + vpc_security_group_ids = [aws_security_group.public_security-group.id] + key_name = aws_key_pair.kp.key_name #key pair attach + associate_public_ip_address = "true" + user_data =file("./modules/ec2/public_user_data.sh") + tags = { + Name = "${var.ec2_Name[count.index]}", + created-by="john" + } + provisioner "local-exec" { + command = <> inventory.ini + elif [ "$instance_name" = "Jenkins" ]; then + echo "[Jenkins]\n$instance_ip" >> inventory.ini + fi + EOT + } +} + + + + +resource "aws_security_group" "public_security-group" { + name = "public-security-group" + vpc_id = var.vpc_id + + ingress { + from_port = var.ssh_port + to_port = var.ssh_port + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + + ingress { + from_port = var.HTTP_port + to_port = var.HTTP_port + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + ingress { + from_port = var.jenkins_port + to_port = var.jenkins_port + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + + +# Generate a new RSA private key with a key size of 4096 bits +resource "tls_private_key" "pk" { + algorithm = "RSA" + rsa_bits = 4096 +} +# Create an AWS key pair using the generated public key +resource "aws_key_pair" "kp" { + key_name = var.key_Name # Create "myKey" to AWS!! + public_key = tls_private_key.pk.public_key_openssh + + # Use a local-exec provisioner to save the private key to a file on the local machine +# provisioner "local-exec" { +# command = < ./${var.key_Name}.pem +# chmod 400 ./${var.key_Name}.pem +# EOT +# } + +} + +# Save the private key to a file locally (will be destroyed with the key_pair resource) +resource "local_file" "private_key" { + content = tls_private_key.pk.private_key_pem + filename = "${path.cwd}/${var.key_Name}.pem" + provisioner "local-exec" { + command = < 0 ? 1 : 0 + vpc_id = aws_vpc.aws_vpc.id + + tags = { + Name = "igw-${var.vpc_Name}" + created-by="john" + } + +} + +# Create a route table for public subnets +resource "aws_route_table" "public_route_table" { + count = length(var.public_subnets_cidr) > 0 ? 1 : 0 #use to prevent creation of rw if not public subnet entered + vpc_id = aws_vpc.aws_vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.internet_gateway[0].id + } + + tags = { + Name = "route_table_${var.vpc_Name}" + created-by="john" + } +} + + + +# Associate public subnets with the public route table +resource "aws_route_table_association" "public_association" { + count = length(var.public_subnets_cidr) + subnet_id = aws_subnet.public_subnet[count.index].id + route_table_id = aws_route_table.public_route_table[0].id + +} diff --git a/terraform/modules/vpc/outputs.tf b/terraform/modules/vpc/outputs.tf new file mode 100644 index 0000000..815e887 --- /dev/null +++ b/terraform/modules/vpc/outputs.tf @@ -0,0 +1,8 @@ + +output "vpc_id" { + value = aws_vpc.aws_vpc.id +} +output "public_subnet_ids" { + description = "List with IDs of the public subnets" + value = aws_subnet.public_subnet.*.id +} diff --git a/terraform/modules/vpc/variables.tf b/terraform/modules/vpc/variables.tf new file mode 100644 index 0000000..3b06add --- /dev/null +++ b/terraform/modules/vpc/variables.tf @@ -0,0 +1,23 @@ +variable "vpc_cidr_block" { + description = "this is cidr block of created vpc " + type = string + +} + +variable "public_subnets_cidr" { + description = "cider blocks for public subnets" + type = list(string) +} + +variable "vpc_Name" { + description = "this name vpc " + type = string +} + + +variable "availability_Zones_subnet" { + description = "Availability Zones subnet for each subnet" + type = list(string) +} + + diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..44cc68e --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,18 @@ +terraform { + + required_providers { + aws = { + + source = "hashicorp/aws" + version = "~> 5.0.0" + } + } + +} + +provider "aws" { + shared_config_files = [ "~/.aws/config" ] + shared_credentials_files = [ "~/.aws/credentials" ] + profile = "default" + region = "us-east-1" +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..7fbce37 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,40 @@ +variable "vpc_Name" { + description = "Name of the VPC." + type = string +} + +variable "vpc_cidr_block" { + description = "CIDR block for the VPC." + type = string +} + +variable "public_subnets_cidr" { + description = "List of CIDR blocks for public subnets." + type = list(string) +} + +variable "availability_Zones_subnet" { + description = "List of Availability Zones for subnets." + type = list(string) +} + +variable "ec2_Name" { + description = "Name of the EC2 instance." + type = list(string) +} + +variable "ami_id" { + description = "AMI ID for the EC2 instance." + type = string +} + +variable "instance_type" { + description = "Type of EC2 instance." + type = string +} + +variable "key_Name" { + description = "SSH key pair name for instance access." + type = string +} + diff --git a/tests/test.py b/tests/test.py index 3419901..84fd438 100644 --- a/tests/test.py +++ b/tests/test.py @@ -11,7 +11,7 @@ def test_isupper(self): self.assertFalse('Foo'.isupper()) def test_split(self): - s = 'hello world' + s = 'helloO0 world' self.assertEqual(s.split(), ['hello', 'world']) # check that s.split fails when the separator is not a string with self.assertRaises(TypeError): @@ -19,4 +19,4 @@ def test_split(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main()