diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 274f3b1..b51b886 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -1,55 +1,150 @@ -name: CI - -on: - push: - branches: [master, develop] - pull_request: - branches: [master, develop] - -env: - NODE_VERSION: '18' - -jobs: - server: - name: Server - Install, Test - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: server/package-lock.json - - - name: Install dependencies - working-directory: server - run: npm ci - - - name: Run tests - working-directory: server - run: npm test - - client: - name: Client - Install, Build - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - cache-dependency-path: client/package-lock.json - - - name: Install dependencies - working-directory: client - run: npm ci - - - name: Build - working-directory: client - run: npm run build +name: CI + +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +env: + NODE_VERSION: '18' + +jobs: + server: + name: Server - Install, Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: server/package-lock.json + + - name: Install dependencies + working-directory: server + run: npm ci + + - name: Run tests + working-directory: server + run: npm test + + client: + name: Client - Install, Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: client/package-lock.json + + - name: Install dependencies + working-directory: client + run: npm ci + + - name: Build + working-directory: client + run: npm run build + + acr-push: + name: Docker - Build & Push to ACR + runs-on: ubuntu-latest + needs: [server, client] + if: github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Azure Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.ACR_NAME }}.azurecr.io + username: ${{ secrets.ACR_USERNAME }} + password: ${{ secrets.ACR_PASSWORD }} + + - name: Build and Push Server Image + uses: docker/build-push-action@v5 + with: + context: server + file: server/Dockerfile + push: true + tags: | + ${{ secrets.ACR_NAME }}.azurecr.io/server:${{ github.sha }} + ${{ secrets.ACR_NAME }}.azurecr.io/server:latest + + - name: Build and Push Client Image + uses: docker/build-push-action@v5 + with: + context: client + file: client/Dockerfile + push: true + tags: | + ${{ secrets.ACR_NAME }}.azurecr.io/client:${{ github.sha }} + ${{ secrets.ACR_NAME }}.azurecr.io/client:latest + + - name: Scan Server Image with Trivy + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ secrets.ACR_NAME }}.azurecr.io/server:${{ github.sha }} + format: 'table' + exit-code: '0' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + + - name: Scan Client Image with Trivy + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ secrets.ACR_NAME }}.azurecr.io/client:${{ github.sha }} + format: 'table' + exit-code: '0' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + + deploy-to-aks: + name: Kubernetes - Deploy to AKS + runs-on: ubuntu-latest + needs: [acr-push] + if: github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up kubectl + uses: azure/setup-kubectl@v4 + + - name: Set up Kubernetes Context + uses: azure/k8s-set-context@v4 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBE_CONFIG }} + + - name: Create or Update Secrets + run: | + kubectl create secret generic server-secrets \ + --from-literal=JWT_SECRET=${{ secrets.JWT_SECRET }} \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy with Kustomize (Git SHA Tagging) + run: | + cd kubernetes + kustomize edit set image devopscommerce.azurecr.io/server=${{ secrets.ACR_NAME }}.azurecr.io/server:${{ github.sha }} + kustomize edit set image devopscommerce.azurecr.io/client=${{ secrets.ACR_NAME }}.azurecr.io/client:${{ github.sha }} + kubectl apply -k . + + - name: Verify Rollout Status + run: | + kubectl rollout status deployment/server --timeout=180s + kubectl rollout status deployment/client --timeout=180s \ No newline at end of file diff --git a/.gitignore b/.gitignore index cc7acc4..b5c1220 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,17 @@ yarn-error.log* DOC.md ROADMAP.md ecommerce_cicd_capstone_project.md + +# terraform +**/.terraform/* +*.tfstate +*.tfstate.* +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraformrc +terraform.rc + diff --git a/infra/.terraform.lock.hcl b/infra/.terraform.lock.hcl new file mode 100644 index 0000000..b77dda7 --- /dev/null +++ b/infra/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.117.1" + constraints = "~> 3.70" + hashes = [ + "h1:Y9J+emydHX7LpJYKdF9Zmng0zcRR86Awm/t28LNzb68=", + "zh:0c513676836e3c50d004ece7d2624a8aff6faac14b833b96feeac2e4bc2c1c12", + "zh:50ea01ada95bae2f187db9e926e463f45d860767a85ebc59160414e00e76c35d", + "zh:52c2a9edacc06b3f72153f5ef6daca0761c6292158815961fe37f60bc576a3d7", + "zh:618eed2a06b19b1a025b45b05891846d570a6a1cca4d23f4942f5a99e1f747ae", + "zh:61cde5d3165d7e5ec311d5d89486819cd605c1b2d54611b5c97bd4e97dba2762", + "zh:6a873358d5031fc222f5e05f029d1237f3dce8345c767665f393283dfa2627f6", + "zh:afdd80064b2a04da311856feb4ed45f77ff4df6c356e8c2b10afb51fe7e61c70", + "zh:b09113df7e0e8c8959539bd22bae6c39faeb269ba3c4cd948e742f5cf58c35fb", + "zh:d340db7973109761cfc27d52aa02560363337c908b2c99b3628adc5a70a99d5b", + "zh:d5a577226ebc8c65e8f19384878a86acc4b51ede4b4a82d37c3b331b0efcd4a7", + "zh:e2962b147f9e71732df8dbc74940c10d20906f3c003cbfaa1eb9fabbf601a9f0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 0000000..5bd33c8 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,80 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.70" + } + } +} + +provider "azurerm" { + features {} +} + +# 1. Reference existing Azure Container Registry from Phase 4 +data "azurerm_container_registry" "acr" { + name = var.acr_name + resource_group_name = var.resource_group_name +} + +# 2. Virtual Network for AKS Cluster +resource "azurerm_virtual_network" "vnet" { + name = "vnet-ecommerce-aks" + location = var.location + resource_group_name = var.resource_group_name + address_space = ["10.0.0.0/16"] + + tags = { + Environment = "DevOps-Capstone" + ManagedBy = "Terraform" + } +} + +# 3. Subnet for AKS Node Pool +resource "azurerm_subnet" "aks_subnet" { + name = "subnet-aks" + resource_group_name = var.resource_group_name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = ["10.0.1.0/24"] +} + +# 4. Azure Kubernetes Service (AKS) Cluster +resource "azurerm_kubernetes_cluster" "aks" { + name = var.cluster_name + location = var.location + resource_group_name = var.resource_group_name + dns_prefix = var.dns_prefix + + default_node_pool { + name = "default" + node_count = var.node_count + vm_size = var.vm_size + vnet_subnet_id = azurerm_subnet.aks_subnet.id + } + + identity { + type = "SystemAssigned" + } + + network_profile { + network_plugin = "azure" + load_balancer_sku = "standard" + service_cidr = var.service_cidr + dns_service_ip = var.dns_service_ip + } + + tags = { + Environment = "DevOps-Capstone" + Project = "MERN-Ecommerce" + ManagedBy = "Terraform" + } +} + +# 5. Role Assignment: Grant AKS permission to pull images from ACR (AcrPull) +resource "azurerm_role_assignment" "aks_acr_pull" { + principal_id = azurerm_kubernetes_cluster.aks.kubelet_identity[0].object_id + role_definition_name = "AcrPull" + scope = data.azurerm_container_registry.acr.id + skip_service_principal_aad_check = true +} diff --git a/infra/outputs.tf b/infra/outputs.tf new file mode 100644 index 0000000..c3565fa --- /dev/null +++ b/infra/outputs.tf @@ -0,0 +1,19 @@ +output "resource_group_name" { + description = "The Resource Group containing all resources" + value = var.resource_group_name +} + +output "kubernetes_cluster_name" { + description = "The name of the AKS cluster" + value = azurerm_kubernetes_cluster.aks.name +} + +output "acr_login_server" { + description = "The login server for the Azure Container Registry" + value = data.azurerm_container_registry.acr.login_server +} + +output "connect_command" { + description = "Command to configure kubectl to connect to the new cluster" + value = "az aks get-credentials --resource-group ${var.resource_group_name} --name ${azurerm_kubernetes_cluster.aks.name}" +} diff --git a/infra/variables.tf b/infra/variables.tf new file mode 100644 index 0000000..82e5219 --- /dev/null +++ b/infra/variables.tf @@ -0,0 +1,53 @@ +variable "resource_group_name" { + description = "Name of the Azure Resource Group" + type = string + default = "rg-ecommerce-devops" +} + +variable "location" { + description = "Azure region where resources will be created" + type = string + default = "southindia" +} + +variable "acr_name" { + description = "Name of the Azure Container Registry created in Phase 4" + type = string + default = "devopscommerce" +} + +variable "cluster_name" { + description = "Name of the Azure Kubernetes Service (AKS) cluster" + type = string + default = "aks-ecommerce-devops" +} + +variable "dns_prefix" { + description = "DNS prefix for the AKS cluster" + type = string + default = "ecom-aks" +} + +variable "node_count" { + description = "Number of worker nodes in the default node pool" + type = number + default = 2 +} + +variable "vm_size" { + description = "Azure Virtual Machine size for the AKS worker nodes" + type = string + default = "Standard_B2s_v2" +} + +variable "service_cidr" { + description = "Network range used by Kubernetes services (must not overlap with VNet/subnet)" + type = string + default = "10.1.0.0/16" +} + +variable "dns_service_ip" { + description = "IP address within service_cidr used by CoreDNS" + type = string + default = "10.1.0.10" +} diff --git a/kubernetes/client.yaml b/kubernetes/client.yaml new file mode 100644 index 0000000..1239967 --- /dev/null +++ b/kubernetes/client.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client +spec: + replicas: 1 + selector: + matchLabels: + app: client + template: + metadata: + labels: + app: client + spec: + containers: + - name: client + image: devopscommerce.azurecr.io/client:latest + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: client +spec: + selector: + app: client + ports: + - port: 80 + targetPort: 8080 + type: LoadBalancer diff --git a/kubernetes/kustomization.yaml b/kubernetes/kustomization.yaml new file mode 100644 index 0000000..de71df3 --- /dev/null +++ b/kubernetes/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - mongo.yaml + - server.yaml + - client.yaml + +images: + - name: devopscommerce.azurecr.io/server + newTag: latest + - name: devopscommerce.azurecr.io/client + newTag: latest diff --git a/kubernetes/mongo.yaml b/kubernetes/mongo.yaml new file mode 100644 index 0000000..65adf0a --- /dev/null +++ b/kubernetes/mongo.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongo +spec: + replicas: 1 + selector: + matchLabels: + app: mongo + template: + metadata: + labels: + app: mongo + spec: + containers: + - name: mongo + image: mongo:latest + ports: + - containerPort: 27017 +--- +apiVersion: v1 +kind: Service +metadata: + name: mongo +spec: + selector: + app: mongo + ports: + - port: 27017 + targetPort: 27017 diff --git a/kubernetes/server.yaml b/kubernetes/server.yaml new file mode 100644 index 0000000..88ab248 --- /dev/null +++ b/kubernetes/server.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: server +spec: + replicas: 1 + selector: + matchLabels: + app: server + template: + metadata: + labels: + app: server + spec: + containers: + - name: server + image: devopscommerce.azurecr.io/server:latest + ports: + - containerPort: 3000 + env: + - name: PORT + value: '3000' + - name: BASE_API_URL + value: 'api' + - name: MONGO_URI + value: 'mongodb://mongo:27017/mern_ecommerce' + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: server-secrets + key: JWT_SECRET +--- +apiVersion: v1 +kind: Service +metadata: + name: server +spec: + selector: + app: server + ports: + - port: 3000 + targetPort: 3000 + type: LoadBalancer