diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..24b543b5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Step 1 - Use an official Python runtime as a parent image. You can use `python:3.8-slim`. +FROM python:3.8-slim +# Step 2 - Set the working directory in the container +WORKDIR /app +# Step 3 Copy the application files in the container +COPY . ./app +# Install system dependencies and ODBC driver +RUN apt-get update && apt-get install -y \ + unixodbc unixodbc-dev odbcinst odbcinst1debian2 libpq-dev gcc && \ + apt-get install -y gnupg && \ + apt-get install -y wget && \ + wget -qO- https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ + wget -qO- https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list && \ + apt-get update && \ + ACCEPT_EULA=Y apt-get install -y msodbcsql18 && \ + apt-get purge -y --auto-remove wget && \ + apt-get clean + +# Install pip and setuptools +RUN pip install --upgrade pip setuptools + +# Step 4 - Install Python packages specified in requirements.txt +RUN pip install --trusted-host pypi.python.org -r ./app/requirements.txt +# Step 5 - Expose port +EXPOSE 5000 +# Step 6 - Define Startup Command +# CMD [ "flask", "run","--host","127.0.0.1","--port","5000"] +entrypoint ["python", "./app/app.py"] diff --git a/README.md b/README.md index 08407749..cd0f17d9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Welcome to the Web App DevOps Project repo! This application allows you to effic - [Features](#features) - [Getting Started](#getting-started) - [Technology Stack](#technology-stack) +- [Developer Information](#developer-information) - [Contributors](#contributors) - [License](#license) @@ -36,6 +37,8 @@ For the application to succesfully run, you need to install the following packag - pyodbc (version 4.0.39) - SQLAlchemy (version 2.0.21) - werkzeug (version 2.2.3) +- azure-identity +- azure-keyvault-secrets ### Usage @@ -53,9 +56,41 @@ To run the application, you simply need to run the `app.py` script in this repos - **Database:** The application employs an Azure SQL Database as its database system to store order-related data. +## Developer Information + +- **Future Modifications:** +Any new feature to be added in future will need to modify code in both the app.py and orders.html files. For example, Delivery Date feature can be added by by modifying the order class, @app_route and new_order sections in the app.py. Also Delivery Date will need to be added to the order.html in the table and form elements. + +- **Containerization Process:** +Containerization involves: +1. Creating a Docker File where base image, work directory, required libraries and dependencies and necessary commands are listed +1. Building Docker Image using the Docker File + docker build -t +1. Run the Docker Image to test all the features + docker run -p 5000:5000 +1. Tag and Push the image to the Docker Hub + docker tag docker-user-name>/:tag +1. Verify the Docker Hub Image by pulling it from the hub +1. Cleanup + - Romove Containers + - docker ps -a + - docker rm + + - Remove Images + - docker images -a + - docker rmi + +- **Infrastructure as Code IaC:** +The folder aka-terraform has all the necessary files to provision the resources on the Kubernetes and then to AKS. These details include the following: +- Terraform Modules +- Cluster Module +- Networking Modules +- Input and Output Variables + ## Contributors - [Maya Iuga]([https://github.com/yourusername](https://github.com/maya-a-iuga)) +- [Muhammad Nadeem Khan]([https://github.com/ndm621](https://github.com/ndm621)) ## License diff --git a/aka-terraform/aks-cluster-module/main.tf b/aka-terraform/aks-cluster-module/main.tf new file mode 100644 index 00000000..70034dc6 --- /dev/null +++ b/aka-terraform/aks-cluster-module/main.tf @@ -0,0 +1,23 @@ +# aks-cluster-module/main.tf +# Create the AKS cluster +resource "azurerm_kubernetes_cluster" "aks_cluster" { + name = var.cluster_name + location = var.cluster_location + resource_group_name = var.resource_group_name + dns_prefix = var.dns_prefix + kubernetes_version = var.kubernetes_version + + default_node_pool { + name = "default" + node_count = 1 + vm_size = "Standard_DS2_v2" + enable_auto_scaling = true + min_count = 1 + max_count = 3 + } + + service_principal { + client_id = var.service_principal_client_id + client_secret = var.service_principal_client_secret + } +} diff --git a/aka-terraform/aks-cluster-module/outputs.tf b/aka-terraform/aks-cluster-module/outputs.tf new file mode 100644 index 00000000..edf63788 --- /dev/null +++ b/aka-terraform/aks-cluster-module/outputs.tf @@ -0,0 +1,14 @@ +output "cluster_name" { + description = "Name of the AKS cluster." + value = azurerm_kubernetes_cluster.aks_cluster.name +} + +output "aks_cluster_id" { + description = "ID of the AKS cluster." + value = azurerm_kubernetes_cluster.aks_cluster.id +} + +output "aks_kubeconfig" { + description = "Kubeconfig file for accessing the AKS cluster." + value = azurerm_kubernetes_cluster.aks_cluster.kube_config_raw +} diff --git a/aka-terraform/aks-cluster-module/variables.tf b/aka-terraform/aks-cluster-module/variables.tf new file mode 100644 index 00000000..d349f6e3 --- /dev/null +++ b/aka-terraform/aks-cluster-module/variables.tf @@ -0,0 +1,59 @@ +variable "cluster_name" { + description = "The name of the AKS cluster" + type = string + default = "aks-cluster" +} + +variable "cluster_location" { + description = "The Azure Region of the AKS cluster" + type = string + default = "UK South" +} + +variable "dns_prefix" { + description = "DNS prefix of the AKS cluster" + type = string + default = "value" +} + +variable "kubernetes_version" { + description = "Kubernetes version of the AKS cluster" + type = string + default = "value" +} + +variable "service_principal_client_id" { + description = "Client ID of the service principal of the AKS cluster" + type = string + default = "value" +} + +variable "service_principal_client_secret" { + description = "Client Secret of the service principal" + type = string + default = "value" +} + +variable "resource_group_name" { + description = "Resource Group Name in Azure" + type = string + default = "aks-resources" +} + +variable "vnet_id" { + description = "ID of the previously created VNet" + type = string + default = "aks-vnet" +} + +variable "control_plane_subnet_id" { + description = "ID of the control plane subnet within the VNet" + type = string + default = "control-plane-subnet" +} + +variable "worker_node_subnet_id" { + description = "ID of the worker node subnet within the VNet" + type = string + default = "worker-node-subnet" +} diff --git a/aka-terraform/application-manifest.yaml b/aka-terraform/application-manifest.yaml new file mode 100644 index 00000000..7cbf5271 --- /dev/null +++ b/aka-terraform/application-manifest.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flask-app-deployment + labels: + app: flask-app +spec: + replicas: 2 + selector: + matchLabels: + app: flask-app + template: + metadata: + labels: + app: flask-app + spec: + containers: + - name: flask-app + image: ndm621/web-app-img:v1.0 + ports: + - containerPort: 5000 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + +--- +apiVersion: v1 +kind: Service +metadata: + name: flask-app-service +spec: + selector: + app: flask-app + type: ClusterIP + ports: + - protocol: TCP + port: 80 + targetPort: 5000 diff --git a/aka-terraform/main.tf b/aka-terraform/main.tf new file mode 100644 index 00000000..dc2f8c45 --- /dev/null +++ b/aka-terraform/main.tf @@ -0,0 +1,45 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.0.0" + } + } +} + +provider "azurerm" { + features {} + client_id = var.client_id + client_secret = var.client_secret + subscription_id = "07bd18f6-a3df-4eda-91a2-e133a16dbf5c" + tenant_id = "47d4542c-f112-47f4-92c7-a838d8a5e8ef" +} + +module "networking" { + source = "./networking-module" + + # Input variables for the networking module + resource_group_name = "networking-rg" + location = "UK South" + vnet_address_space = ["10.0.0.0/16"] + + # Define more input variables as needed... +} + +module "aks_cluster" { + source = "./aks-cluster-module" + + # Input variables for the AKS cluster module + cluster_name = "terraform-aks-cluster" + cluster_location = "UK South" + dns_prefix = "myaks-project" + kubernetes_version = "1.26.6" # Adjust the version as needed + service_principal_client_id = var.client_id + service_principal_client_secret = var.client_secret + + # Input variables referencing outputs from the networking module + resource_group_name = module.networking.resource_group_name + vnet_id = module.networking.vnet_id + control_plane_subnet_id = module.networking.control_plane_subnet_id + worker_node_subnet_id = module.networking.worker_node_subnet_id +} diff --git a/aka-terraform/networking-module/main.tf b/aka-terraform/networking-module/main.tf new file mode 100644 index 00000000..565addeb --- /dev/null +++ b/aka-terraform/networking-module/main.tf @@ -0,0 +1,75 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.0.0" + } + } +} + +provider "azurerm" { + features {} +} + +# 1. Define the Azure resource group resource +resource "azurerm_resource_group" "aks" { + name = var.resource_group_name + location = "UK South" +} + +# 2. Define Virtual Network (VNet) +resource "azurerm_virtual_network" "aks_vnet" { + name = "aks-vnet" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.aks.location + resource_group_name = azurerm_resource_group.aks.name + depends_on = [ azurerm_resource_group.aks ] +} + +resource "azurerm_subnet" "control_plane_subnet" { + name = "control-plane-subnet" + resource_group_name = azurerm_resource_group.aks.name + virtual_network_name = azurerm_virtual_network.aks_vnet.name + address_prefixes = ["10.0.1.0/24"] +} + +resource "azurerm_subnet" "worker_node_subnet" { + name = "worker-node-subnet" + resource_group_name = azurerm_resource_group.aks.name + virtual_network_name = azurerm_virtual_network.aks_vnet.name + address_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_network_security_group" "aks_nsg" { + name = "aks-nsg" + location = azurerm_resource_group.aks.location + resource_group_name = azurerm_resource_group.aks.name +} + +resource "azurerm_network_security_rule" "kube_apiserver_rule" { + name = "kube-apiserver" + resource_group_name = azurerm_resource_group.aks.name + network_security_group_name = azurerm_network_security_group.aks_nsg.name + priority = 1001 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "6443" + source_address_prefix = "*" + destination_address_prefix = "*" +} + +resource "azurerm_network_security_rule" "ssh_rule" { + name = "ssh " + resource_group_name = azurerm_resource_group.aks.name + network_security_group_name = azurerm_network_security_group.aks_nsg.name + priority = 1002 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "22" + source_address_prefix = "*" + destination_address_prefix = "*" +} diff --git a/aka-terraform/networking-module/outputs.tf b/aka-terraform/networking-module/outputs.tf new file mode 100644 index 00000000..48736e4b --- /dev/null +++ b/aka-terraform/networking-module/outputs.tf @@ -0,0 +1,27 @@ +# networking-module/outputs.tf + +output "vnet_id" { + description = "ID of the Virtual Network (VNet)." + value = azurerm_virtual_network.aks_vnet.id +} + +output "control_plane_subnet_id" { + description = "ID of the control plane subnet." + value = azurerm_subnet.control_plane_subnet.id +} + +output "worker_node_subnet_id" { + description = "ID of the worker node subnet." + value = azurerm_subnet.worker_node_subnet.id +} + +output "resource_group_name" { + description = "Name of the Azure Resource Group for networking resources." + value = azurerm_resource_group.aks.name +} + +# Define more output variables as needed... +output "aks_nsg_id" { + description = "ID of the Network Security Group (NSG) for AKS." + value = azurerm_network_security_group.aks_nsg.id +} \ No newline at end of file diff --git a/aka-terraform/networking-module/variables.tf b/aka-terraform/networking-module/variables.tf new file mode 100644 index 00000000..c1c1d49a --- /dev/null +++ b/aka-terraform/networking-module/variables.tf @@ -0,0 +1,17 @@ +variable "resource_group_name" { + description = "Resource Group Name in Azure" + type = string + default = "aks-resources" +} + +variable "location" { + description = "location of account" + type = string + default = "UK South" +} + +variable "vnet_address_space" { + description = "Address space for the Virtual Network" + type = list(string) + default = ["10.0.0.0/16"] +} \ No newline at end of file diff --git a/aka-terraform/variables.tf b/aka-terraform/variables.tf new file mode 100644 index 00000000..69e2ec35 --- /dev/null +++ b/aka-terraform/variables.tf @@ -0,0 +1,13 @@ +# variables.tf + +variable "client_id" { + description = "Access key for the provider" + type = string + sensitive = true +} + +variable "client_secret" { + description = "Secret key for the provider" + type = string + sensitive = true +} \ No newline at end of file diff --git a/aks-terraform b/aks-terraform new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/aks-terraform @@ -0,0 +1 @@ + diff --git a/app.py b/app.py index 50f4e29d..045f4b26 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,5 @@ +from azure.identity import ManagedIdentityCredential +from azure.keyvault.secrets import SecretClient from flask import Flask, render_template, request, redirect, url_for from sqlalchemy import create_engine, Column, Integer, String, DateTime from sqlalchemy.orm import sessionmaker @@ -6,14 +8,26 @@ import pyodbc import os +# Key Vault details +key_vault_url = "https://AzureDevOpsProject.vault.azure.net/" + +# Set up Azure Key Vault client with Managed Identity +credential = ManagedIdentityCredential() +secret_client = SecretClient(vault_url=key_vault_url, credential=credential) + # Initialise Flask App app = Flask(__name__) # database connection -server = 'devops-project-server.database.windows.net' -database = 'orders-db' -username = 'maya' -password = 'AiCore1237' +# Access the secret values from Key Vault +secret = secret_client.get_secret("server") +server = secret.value +secret = secret_client.get_secret("databse") +database = secret.value +secret = secret_client.get_secret("username") +username = secret.value +secret = secret_client.get_secret("password") +password = secret.value driver= '{ODBC Driver 18 for SQL Server}' # Create the connection string diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..b353fec6 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,29 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +trigger: +- main + +pool: + vmImage: ubuntu-latest + parallel: 1 + +steps: +- task: Docker@2 + inputs: + containerRegistry: 'Docker Hub' + repository: 'ndm621/web-app-img' + command: 'buildAndPush' + Dockerfile: '**/Dockerfile' + tags: 'latest' +- task: KubernetesManifest@1 + inputs: + action: 'deploy' + connectionType: 'azureResourceManager' + azureSubscriptionConnection: 'webapp' + azureResourceGroup: 'networking-rg' + kubernetesCluster: 'terraform-aks-cluster' + useClusterAdmin: true + manifests: './aka-terraform/application-manifest.yaml' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 47ec762e..1fda926e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ flask==2.2.2 pyodbc==4.0.39 SQLAlchemy==2.0.21 -werkzeug===2.2.3 \ No newline at end of file +werkzeug===2.2.3 +azure-identity +azure-keyvault-secrets