From 0282e1814091e99bd09dd65df5b005ef85fe47fc Mon Sep 17 00:00:00 2001 From: lenape Date: Sat, 12 Jul 2025 08:51:48 +0000 Subject: [PATCH] automated terminal push --- Dockerfile | 12 ++++ Jenkinsfile | 114 +++++++++++++++++++++++++++++++ ansible/configure_ecs.yml | 39 +++++++++++ ansible/hosts | 2 + app.py | 10 +++ lenape_key.pub | 1 + requirements.txt | 2 + terraform/backend.tf | 8 +++ terraform/main.tf | 139 ++++++++++++++++++++++++++++++++++++++ terraform/variables.tf | 34 ++++++++++ 10 files changed, 361 insertions(+) create mode 100644 Dockerfile create mode 100644 Jenkinsfile create mode 100644 ansible/configure_ecs.yml create mode 100644 ansible/hosts create mode 100644 app.py create mode 100644 lenape_key.pub create mode 100644 requirements.txt create mode 100644 terraform/backend.tf create mode 100644 terraform/main.tf create mode 100644 terraform/variables.tf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b96c9e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +EXPOSE 8080 + +CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"] \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..0cb99ca --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,114 @@ +pipeline { + agent any + environment { + GITEA_REPO = 'https://code.jacquesingram.online/lenape/nvhi-atsila-microservice.git' + GITEA_CREDS = '52ee0829-6e65-4951-925b-4186254c3f21' + SONAR_HOST = 'https://sonar.jacquesingram.online' + SONAR_TOKEN = credentials('sonar-token') + ARTIFACTORY_URL = 'https://art.jacquesingram.online/artifactory/nvhi-atsila-docker' + ARTIFACTORY_CREDS = 'artifactory-api' + AWS_CRED_ID = 'aws-ci' + AWS_REGION = 'us-east-2' + TF_BACKEND_BUCKET = 'nvhi-atsila-tf-state' + TF_BACKEND_PREFIX = 'ecs/terraform.tfstate' + TF_DDB_TABLE = 'nvhi-atsila-locks' + SSH_CRED_ID = 'jenkins-ssh' + + TF_VAR_cluster_name = 'nvhi-atsila-cluster' + TF_VAR_vpc_cidr = '10.0.0.0/16' + TF_VAR_public_subnets = '10.0.1.0/24,10.0.2.0/24' + TF_VAR_instance_type = 't2.micro' + TF_VAR_key_pair_name = 'nvhi-atsila-deployer' + // Injected from Jenkins Global Env + TF_VAR_jenkins_ip_cidr = env.JENKINS_SSH_CIDR + + IMAGE_NAME = 'lenape/nvhi-atsila-microservice' + IMAGE_TAG = "v1.0.${env.BUILD_NUMBER}" + } + stages { + stage('Checkout') { + steps { + git url: env.GITEA_REPO, credentialsId: env.GITEA_CREDS + } + } + stage('SonarQube Scan') { + steps { + withSonarQubeEnv('SonarQube') { + sh "sonar-scanner -Dsonar.projectKey=nvhi-atsila-microservice -Dsonar.sources=." + } + } + } + stage('Build & Push Docker Image') { + steps { + script { + docker.withRegistry(env.ARTIFACTORY_URL, env.ARTIFACTORY_CREDS) { + def img = docker.build("${env.IMAGE_NAME}:${env.IMAGE_TAG}") + img.push() + } + } + } + } + stage('Terraform Init & Apply') { + steps { + withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: env.AWS_CRED_ID]]) { + dir('terraform') { + sh """ + terraform init \ + -backend-config="bucket=${TF_BACKEND_BUCKET}" \ + -backend-config="key=${TF_BACKEND_PREFIX}" \ + -backend-config="region=${AWS_REGION}" \ + -backend-config="dynamodb_table=${TF_DDB_TABLE}" + terraform apply -auto-approve \ + -var="cluster_name=${TF_VAR_cluster_name}" \ + -var="vpc_cidr=${TF_VAR_vpc_cidr}" \ + -var="public_subnets=${TF_VAR_public_subnets}" \ + -var="instance_type=${TF_VAR_instance_type}" \ + -var="key_pair_name=${TF_VAR_key_pair_name}" \ + -var="jenkins_ip_cidr=${TF_VAR_jenkins_ip_cidr}" + """ + } + } + } + } + stage('Configure EC2 with Ansible') { + steps { + script { + def ec2_ip = sh( + script: "terraform -chdir=terraform output -raw ecs_instance_public_ip", + returnStdout: true + ).trim() + writeFile file: 'ansible/hosts', text: "[inventory_hosts]\n${ec2_ip} ansible_user=ubuntu" + } + ansiblePlaybook( + playbook: 'ansible/configure_ecs.yml', + inventory: 'ansible/hosts', + credentialsId: env.SSH_CRED_ID + ) + } + } + stage('Register & Deploy to ECS') { + steps { + withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: env.AWS_CRED_ID]]) { + sh """ + aws ecs register-task-definition \ + --family ${TF_VAR_cluster_name} \ + --network-mode bridge \ + --container-definitions '[{ + "name":"health-workload", + "image":"${env.ARTIFACTORY_URL}/${env.IMAGE_NAME}:${env.IMAGE_TAG}", + "essential":true, + "portMappings":[{"containerPort":8080,"hostPort":8080}] + }]' \ + --region ${AWS_REGION} + + aws ecs update-service \ + --cluster ${TF_VAR_cluster_name} \ + --service ${TF_VAR_cluster_name}-service \ + --force-new-deployment \ + --region ${AWS_REGION} + """ + } + } + } + } +} diff --git a/ansible/configure_ecs.yml b/ansible/configure_ecs.yml new file mode 100644 index 0000000..32d974a --- /dev/null +++ b/ansible/configure_ecs.yml @@ -0,0 +1,39 @@ +--- +- name: Configure EC2 for ECS Cluster + hosts: inventory_hosts + become: yes + + vars: + ecs_cluster_name: "nvhi-atsila-cluster" + + tasks: + - name: Install Docker + apt: + name: docker.io + state: present + update_cache: yes + + - name: Start and enable Docker + service: + name: docker + state: started + enabled: true + + - name: Write ECS config file + copy: + dest: /etc/ecs/ecs.config + content: | + ECS_CLUSTER={{ ecs_cluster_name }} + + - name: Run ECS agent container + docker_container: + name: ecs-agent + image: amazon/amazon-ecs-agent:latest + state: started + restart_policy: always + env_file: /etc/ecs/ecs.config + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /var/log/ecs:/log + - /var/lib/ecs/data:/data + network_mode: host \ No newline at end of file diff --git a/ansible/hosts b/ansible/hosts new file mode 100644 index 0000000..99871df --- /dev/null +++ b/ansible/hosts @@ -0,0 +1,2 @@ +[inventory_hosts] +# overwritten dynamically by Jenkins with the EC2 public IP \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..e896c2e --- /dev/null +++ b/app.py @@ -0,0 +1,10 @@ +from flask import Flask, jsonify + +app = Flask(__name__) + +@app.route('/health') +def health(): + return jsonify(status='OK') + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8080) diff --git a/lenape_key.pub b/lenape_key.pub new file mode 100644 index 0000000..c4755fb --- /dev/null +++ b/lenape_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDDFBAOogBj/GHKXQs6FLROGQfXkZe2uKbRron0We7ZOLgt6e1bI7U8IMe+DIH250CHSi4R5DBYFQF5Bk1TkS5cgMtPIAb87vRUGI3sLs29DQA/kllYiZlQi9ejxcEz2+TRWn10Q/Kltlb6ESNLnnnTsIUUxKUeY3MKFFd+V13FleSVLGYondwPWYwD/XJ6a3VwSTJ1wFKO+lpKknSjDl2ZOgYpWFALPH+EwMlRGVMrUXAB604zqR1XOzYXAAWnhmmC9IGgCzU/5JnEgFyhfZbR3kpEH8SmSXahvdFZERp+3j9d3ROjchqnf0Z0zZ7vzX+G+jvzT/jGOkzH9tx0/OqIO9f47OFF8iUfZgUtJU1QGbepdsmQqognhxfJQfMZbVtKUw7zt+mzJz3A0XcRp7IwVHaqJ2QW2dpXi4UbWtejtZqROg6byWq2FpvFGNIT3eiKTf+EpCoOec6YGSrRQlj73Ob0+FhmsyQ6e8KKncaRYx38PqtnWsI3UnLtdKmEJmDBPI0ipxJzmKJKtb0vtJPVYvFEpgiXSwnDX883rAUQrXR/EhOMmbMwk7JSes6/GXH9rWN10JHh1/i1LLpl+rg6VyktFgVBHzVw++y29QSfFixeTvFkkTS5kl//CpKd1GDQb9ZBH6SPgkgOjmASPUo+p5e/NiN/SIBSpYpMjOKs7Q== jacques@Xochiquetzal diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2cfee10 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==2.0.3 +gunicorn==20.1.0 \ No newline at end of file diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 0000000..dad5147 --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,8 @@ +terraform { + backend "s3" { + bucket = "nvhi-atsila-tf-state" + key = "ecs/terraform.tfstate" + region = "us-east-2" + dynamodb_table = "nvhi-atsila-locks" + } +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..4912b50 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,139 @@ +provider "aws" { + region = var.aws_region +} + +data "aws_availability_zones" "azs" {} + +# Hardened remote-state S3 bucket +resource "aws_s3_bucket" "tfstate" { + bucket = "nvhi-atsila-tf-state" + + server_side_encryption_configuration { + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } + } + + versioning { + enabled = true + } + + tags = { + Name = "nvhi-atsila-tf-state" + Environment = "Production" + } +} + +resource "aws_s3_bucket_public_access_block" "tfstate_block" { + bucket = aws_s3_bucket.tfstate.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_dynamodb_table" "locks" { + name = "nvhi-atsila-locks" + billing_mode = "PAY_PER_REQUEST" + hash_key = "LockID" + + attribute { + name = "LockID" + type = "S" + } + + point_in_time_recovery { + enabled = true + } + + tags = { + Name = "nvhi-atsila-locks" + Environment = "Production" + } +} + +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + tags = { + Name = "${var.cluster_name}-vpc" + } +} + +resource "aws_subnet" "public" { + count = length(split(",", var.public_subnets)) + vpc_id = aws_vpc.main.id + cidr_block = element(split(",", var.public_subnets), count.index) + availability_zone = data.aws_availability_zones.azs.names[count.index] + map_public_ip_on_launch = true + tags = { + Name = "${var.cluster_name}-public-${count.index}" + } +} + +resource "aws_security_group" "ecs_sg" { + name = "${var.cluster_name}-sg" + description = "Allow SSH & HTTP to ECS" + vpc_id = aws_vpc.main.id + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [var.jenkins_ip_cidr] + } + + ingress { + from_port = 8080 + to_port = 8080 + 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"] + } + + tags = { + Name = "${var.cluster_name}-sg" + } +} + +resource "aws_key_pair" "deployer" { + key_name = var.key_pair_name + public_key = file("${path.module}/../lenape_key.pub") +} + +data "aws_ami" "ubuntu" { + most_recent = true + owners = ["099720109477"] + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } +} + +resource "aws_ecs_cluster" "main" { + name = var.cluster_name +} + +resource "aws_instance" "ecs_instance" { + ami = data.aws_ami.ubuntu.id + instance_type = var.instance_type + subnet_id = aws_subnet.public[0].id + vpc_security_group_ids = [aws_security_group.ecs_sg.id] + key_name = aws_key_pair.deployer.key_name + + tags = { + Name = "${var.cluster_name}-instance" + } +} + +output "ecs_instance_public_ip" { + value = aws_instance.ecs_instance.public_ip +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..f113b36 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,34 @@ +variable "aws_region" { + type = string + default = "us-east-2" +} + +variable "jenkins_ip_cidr" { + description = "CIDR block for SSH access from Jenkins (injected by pipeline)" + type = string +} + +variable "cluster_name" { + description = "Name of the ECS cluster" + type = string +} + +variable "vpc_cidr" { + description = "VPC CIDR block" + type = string +} + +variable "public_subnets" { + description = "Comma-separated public subnet CIDRs" + type = string +} + +variable "instance_type" { + description = "EC2 instance type" + type = string +} + +variable "key_pair_name" { + description = "EC2 Key Pair name" + type = string +}