├── .github └── workflows │ └── e2ecicd.yaml ├── .gitignore ├── README.md ├── VERSION ├── app ├── .dockerignore ├── Dockerfile ├── Dockerfile-python ├── app.py ├── calculator.js ├── calculator.test.js ├── index.js └── package.json ├── kustomize ├── base │ ├── deploy.yaml │ ├── ingress.yaml │ ├── kustomization.yaml │ └── svc.yaml └── overlays │ ├── dev │ ├── deploy-dev.yaml │ ├── ingress-dev.yaml │ ├── kustomization.yaml │ └── svc-dev.yaml │ ├── prod │ ├── deploy-prod.yaml │ ├── ingress-prod.yaml │ ├── kustomization.yaml │ └── svc-prod.yaml │ └── staging │ ├── deploy-staging.yaml │ ├── ingress-staging.yaml │ ├── kustomization.yaml │ └── svc-staging.yaml └── terraform ├── ingress-nginx.tf ├── main.tf ├── outputs.tf ├── terraform.tf └── variables.tf /.github/workflows/e2ecicd.yaml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | paths-ignore: 8 | - 'VERSION' 9 | branches: 10 | - dev 11 | push: 12 | branches: 13 | - prod 14 | - dev 15 | - staging 16 | paths-ignore: 17 | - 'VERSION' 18 | - 'README.md' 19 | permissions: 20 | contents: write 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | outputs: 25 | releaseTrue: ${{steps.release-status.outptus.rel-status}} 26 | currentVersion: ${{ steps.updated-version.outputs.version }} 27 | currentENV: ${{ steps.current-env.outputs.ENV }} 28 | steps: 29 | - name: Checkout Code 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup Node 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: 18 36 | 37 | - name: Install Dependencies 38 | run: | 39 | cd app 40 | npm install 41 | 42 | - name: Run Tests 43 | run: | 44 | cd app 45 | npm test 46 | 47 | - name: Determine version increment 48 | id: version-increment 49 | run: | 50 | commit_message=$(git log -1 --pretty=%B) 51 | if [[ "$commit_message" == *"major:"* ]]; then 52 | echo "increment=major" >> "$GITHUB_OUTPUT" 53 | echo "release=true" >> "$GITHUB_ENV" 54 | elif [[ "$commit_message" == "minor:"* ]]; then 55 | echo "increment=minor" >> "$GITHUB_OUTPUT" 56 | echo "release=true" >> "$GITHUB_ENV" 57 | elif [[ "$commit_message" == "patch:"* ]]; then 58 | echo "increment=patch" >> "$GITHUB_OUTPUT" 59 | echo "release=true" >> "$GITHUB_ENV" 60 | else 61 | echo "release=false" >> "$GITHUB_ENV" 62 | fi 63 | - name: Publish Release Status 64 | id: release-status 65 | run: echo "rel-status=${{ env.release }}" >> "$GITHUB_OUTPUT" 66 | 67 | 68 | - name: Publish Current Environment 69 | id: current-env 70 | run: | 71 | export ENVIRONMENT=$(echo "${{ github.ref }}" | sed -e "s/refs\/heads\///g") 72 | echo "ENV=${ENVIRONMENT}" >> "$GITHUB_OUTPUT" 73 | 74 | 75 | - name: Get current version 76 | id: get-version 77 | run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" 78 | 79 | - name: Increment version 80 | id: increment-version 81 | run: echo "new_version=$(docker run --rm -v ${PWD}:/app treeder/bump ${{ steps.version-increment.outputs.increment }})" >> "$GITHUB_OUTPUT" 82 | if: | 83 | github.ref == 'refs/heads/dev' && 84 | env.release == 'true' 85 | 86 | - name: Update VERSION file 87 | run: echo "${{ steps.increment-version.outputs.new_version }}" > VERSION 88 | if: | 89 | github.ref == 'refs/heads/dev' && 90 | env.release == 'true' 91 | 92 | - name: Get updated version 93 | id: updated-version 94 | run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT" 95 | 96 | - uses: EndBug/add-and-commit@v9 97 | name: Commit VERSION file to Dev 98 | with: 99 | add: 'VERSION' 100 | author_name: 'Afraz Ahmed' 101 | author_email: 'aphraz@live.com' 102 | fetch: false 103 | message: 'Updating VERSION file' 104 | pathspec_error_handling: ignore 105 | push: origin dev --force 106 | tag: "${{ steps.updated-version.outputs.version }} --force" 107 | tag_push: '--force' 108 | if: | 109 | github.ref == 'refs/heads/dev' && 110 | env.release == 'true' 111 | 112 | 113 | 114 | - name: Login to DockerHub 115 | uses: docker/login-action@v2 116 | with: 117 | username: ${{ secrets.DOCKER_USERNAME }} 118 | password: ${{ secrets.DOCKER_PASSWORD }} 119 | 120 | - name: Docker Build and Push Dev 121 | run: | 122 | cd app 123 | docker build -t ${{ secrets.DOCKER_USERNAME }}/node-app:${{ steps.updated-version.outputs.version }}-dev . 124 | docker push ${{ secrets.DOCKER_USERNAME }}/node-app:${{ steps.updated-version.outputs.version }}-dev 125 | if: github.ref == 'refs/heads/dev' 126 | 127 | 128 | - name: Docker Build and Push Staging 129 | run: | 130 | cd app 131 | docker build -t ${{ secrets.DOCKER_USERNAME }}/node-app:${{ steps.updated-version.outputs.version }}-staging . 132 | docker push ${{ secrets.DOCKER_USERNAME }}/node-app:${{ steps.updated-version.outputs.version }}-staging 133 | if: github.ref == 'refs/heads/staging' 134 | 135 | - name: Docker Build and Push Prod 136 | run: | 137 | cd app 138 | docker build -t ${{ secrets.DOCKER_USERNAME }}/node-app:${{ steps.updated-version.outputs.version }}-prod . 139 | docker push ${{ secrets.DOCKER_USERNAME }}/node-app:${{ steps.updated-version.outputs.version }}-prod 140 | if: github.ref == 'refs/heads/prod' 141 | 142 | - name: Notify Slack - Release 143 | uses: 8398a7/action-slack@v3 144 | with: 145 | status: custom 146 | fields: workflow,job,commit,repo,ref,author,took 147 | custom_payload: | 148 | { 149 | attachments: [{ 150 | color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', 151 | text: `Workflow: ${process.env.AS_WORKFLOW}\nJob: ${process.env.AS_JOB}\nCommit: ${process.env.AS_COMMIT}\nVersion: ${process.env.AS_VERSION}\nRepository: ${process.env.AS_REPO}@${process.env.AS_REF}\nAuthor: ${process.env.AS_AUTHOR}\nStatus: ${{ job.status }}\nDuration: ${process.env.AS_TOOK}`, 152 | }] 153 | } 154 | env: 155 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} 156 | AS_VERSION: ${{ steps.updated-version.outputs.version }} 157 | if: | 158 | github.ref == 'refs/heads/dev' && 159 | env.release == 'true' 160 | 161 | - name: Notify Slack 162 | uses: 8398a7/action-slack@v3 163 | with: 164 | status: custom 165 | fields: workflow,job,commit,repo,ref,author,took 166 | custom_payload: | 167 | { 168 | attachments: [{ 169 | color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', 170 | text: `Workflow: ${process.env.AS_WORKFLOW}\nJob: ${process.env.AS_JOB}\nCommit: ${process.env.AS_COMMIT}\nVersion: ${process.env.AS_VERSION}\nRepository: ${process.env.AS_REPO}@${process.env.AS_REF}\nAuthor: ${process.env.AS_AUTHOR}\nStatus: ${{ job.status }}\nDuration: ${process.env.AS_TOOK}`, 171 | }] 172 | } 173 | env: 174 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} 175 | AS_VERSION: ${{ steps.updated-version.outputs.version }} 176 | if: env.release == 'false' 177 | 178 | deployment: 179 | needs: build 180 | runs-on: ubuntu-latest 181 | 182 | env: 183 | TF_IN_AUTOMATION: "1" 184 | S3_BUCKET: ${{ secrets.S3_BUCKET }} 185 | S3_REGION: ${{ secrets.S3_REGION }} 186 | DYNAMODB_TABLE: ${{ secrets.DYNAMODB_TABLE }} 187 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 188 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 189 | AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} 190 | ENVIRONMENT: ${{ needs.build.outputs.currentENV }} 191 | TF_VAR_environment: ${{ needs.build.outputs.currentENV }} 192 | 193 | steps: 194 | - name: Checkout Code 195 | uses: actions/checkout@v3 196 | 197 | - name: Terraform CLI Setup 198 | uses: hashicorp/setup-terraform@v2 199 | 200 | - name: Install AWS CLI 201 | run: | 202 | curl -sLk "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 203 | unzip awscliv2.zip 204 | sudo ./aws/install --update 205 | 206 | - name: Terraform Init EKS 207 | run: | 208 | cd terraform 209 | terraform init \ 210 | -backend-config="bucket=${S3_BUCKET}" \ 211 | -backend-config="key=state/${ENVIRONMENT}-env.tfstate" \ 212 | -backend-config="region=${S3_REGION}" \ 213 | -backend-config="encrypt=true" \ 214 | -backend-config="dynamodb_table=${DYNAMODB_TABLE}" 215 | 216 | 217 | - name: Install Kubectl 218 | uses: azure/setup-kubectl@v3 219 | with: 220 | version: 'latest' 221 | 222 | - name: Configure Kubectl if EKS running 223 | run: aws eks --region ${{ secrets.S3_REGION }} update-kubeconfig --name e2ecicd-eks-${ENVIRONMENT} || install -m 600 -D /dev/null ~/.kube/config 224 | 225 | - name: Terraform Plan EKS 226 | id: tf-plan-eks 227 | run: | 228 | cd terraform 229 | export exitcode=0 230 | terraform plan -detailed-exitcode -no-color -out tfplan 231 | export exitcode=$? 232 | 233 | 234 | if [ $exitcode -eq 1 ]; then 235 | echo "Terraform Plan Failed!" 236 | exit 1 237 | elif [ $exitcode -eq 2 ]; then 238 | echo "Drift detected. Need to run terraform apply" 239 | echo "exitcode=$exitcode" >> $GITHUB_OUTPUT 240 | exit 0 241 | else 242 | exit 0 243 | fi 244 | 245 | - name: Deploy EKS Cluster 246 | run: | 247 | cd terraform 248 | terraform apply -auto-approve tfplan 249 | if: ${{ steps.tf-plan-eks.outputs.exitcode == '2' }} 250 | 251 | 252 | - name: Create Namespace 253 | run: | 254 | NAMESPACE=app-${ENVIRONMENT} 255 | kubectl get ns ${NAMESPACE} || kubectl create namespace ${NAMESPACE} 256 | 257 | - name: Update Image 258 | run: | 259 | image=${{ secrets.DOCKER_USERNAME }}/node-app:${{ needs.build.outputs.currentVersion }}-${ENVIRONMENT} 260 | sed -i "s|image: .*$|image: ${image}|" kustomize/overlays/${ENVIRONMENT}/deploy-${ENVIRONMENT}.yaml 261 | 262 | 263 | - name: Install Kustomized Manifests 264 | id: kustomize-deploy 265 | run: | 266 | kubectl apply -k kustomize/overlays/${ENVIRONMENT} | tee kubeout 267 | 268 | if grep -q "deployment.*unchanged" kubeout; then 269 | echo "needRestart=yes" >> "$GITHUB_OUTPUT" 270 | else 271 | echo "needRestart=no" >> "$GITHUB_OUTPUT" 272 | fi 273 | 274 | - name: Rollout Restart Deployment 275 | run: kubectl -n app-${ENVIRONMENT} rollout restart deployment web-deployment 276 | if: ${{ steps.kustomize-deploy.outputs.needRestart == 'yes' }} 277 | 278 | - name: Get LB hostname 279 | id: lb-hostname 280 | run: | 281 | kubectl -n ingress-nginx get svc ingress-nginx-controller \ 282 | -o jsonpath={'.status.loadBalancer.ingress[0].hostname'} > lb-hostname 283 | echo -n "hostname=$(cat lb-hostname)" >> "$GITHUB_OUTPUT" 284 | 285 | 286 | - name: Set DNS on Cloudflare 287 | uses: rez0n/create-dns-record@v2.1 288 | with: 289 | type: "CNAME" 290 | name: "${{ needs.build.outputs.currentENV }}.afraz.dev" 291 | content: ${{ steps.lb-hostname.outputs.hostname }} 292 | ttl: 1 293 | proxied: true 294 | token: ${{ secrets.CLOUDFLARE_TOKEN }} 295 | zone: ${{ secrets.CLOUDFLARE_ZONE }} 296 | 297 | - name: Notify Slack - Deployment 298 | uses: 8398a7/action-slack@v3 299 | with: 300 | status: custom 301 | fields: workflow,job,commit,repo,ref,author,took 302 | custom_payload: | 303 | { 304 | attachments: [{ 305 | color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', 306 | text: `Workflow: ${process.env.AS_WORKFLOW}\nJob: ${process.env.AS_JOB}\nCommit: ${process.env.AS_COMMIT}\nVersion: ${process.env.AS_VERSION}\nDNS: ${process.env.AS_DNS}\nRepository: ${process.env.AS_REPO}@${process.env.AS_REF}\nAuthor: ${process.env.AS_AUTHOR}\nStatus: ${{ job.status }}\nDuration: ${process.env.AS_TOOK}`, 307 | }] 308 | } 309 | env: 310 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} 311 | AS_VERSION: ${{ needs.build.outputs.currentVersion }} 312 | AS_DNS: "https://${{ needs.build.outputs.currentENV }}.afraz.dev" 313 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.terraform/* 2 | *.tfstate 3 | *.tfstate.* 4 | *.tfplan 5 | *.log 6 | *.tfvars 7 | override.tf 8 | override.tf.json 9 | *_override.tf 10 | *_override.tf.json 11 | .terraformrc 12 | terraform.rc 13 | .terraform.lock.hcl 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # End-to-End CI/CD Pipeline for Simple Node App Deployment on EKS 2 | 3 | ## Table of Contents 4 | 5 | - [Repository Structure](#repository-structure) 6 | - [CI/CD Workflow](#cicd-workflow) 7 | - [Build Job](#build-job) 8 | - [Deployment Job](#deployment-job) 9 | - [Infrastructure Details](#infrastructure-details) 10 | - [GitOps Principles](#gitops-principles) 11 | - [Notifications](#notifications) 12 | 13 | ## Repository Structure 14 | 15 | The repository is organized into several key directories: 16 | 17 | ``` 18 | ├── app 19 | │ ├── app.py 20 | │ ├── calculator.js 21 | │ ├── calculator.test.js 22 | │ ├── Dockerfile 23 | │ ├── Dockerfile-python 24 | │ ├── index.js 25 | │ └── package.json 26 | ├── kustomize 27 | │ ├── base 28 | │ │ ├── deploy.yaml 29 | │ │ ├── ingress.yaml 30 | │ │ ├── kustomization.yaml 31 | │ │ └── svc.yaml 32 | │ └── overlays 33 | │ ├── dev 34 | │ │ ├── deploy-dev.yaml 35 | │ │ ├── ingress-dev.yaml 36 | │ │ ├── kustomization.yaml 37 | │ │ └── svc-dev.yaml 38 | │ ├── prod 39 | │ │ ├── deploy-prod.yaml 40 | │ │ ├── ingress-prod.yaml 41 | │ │ ├── kustomization.yaml 42 | │ │ └── svc-prod.yaml 43 | │ └── staging 44 | │ ├── deploy-staging.yaml 45 | │ ├── ingress-staging.yaml 46 | │ ├── kustomization.yaml 47 | │ └── svc-staging.yaml 48 | ├── README.md 49 | ├── terraform 50 | │ ├── ingress-nginx.tf 51 | │ ├── main.tf 52 | │ ├── outputs.tf 53 | │ ├── terraform.tf 54 | │ └── variables.tf 55 | └── VERSION 56 | ``` 57 | 58 | 59 | ## CI/CD Workflow 60 | 61 | #### Build Job 62 | 63 | The `build` job performs several key tasks: 64 | 65 | 1. **Environment Setup**: Node.js environment is set up and dependencies are installed. 66 | 2. **Run Tests**: Executes unit tests for the application. 67 | 3. **Determine Version Increment**: Checks the commit message to determine if the version needs to be incremented using Semantic Versioning scheme. 68 | 4. **Docker Build and Push**: Builds a Docker image and pushes it to a registry. 69 | 70 | #### Deployment Job 71 | 72 | The `deployment` job handles the following: 73 | 74 | 1. **Terraform Setup**: Initializes Terraform and sets up the backend with different state files. 75 | 2. **Terraform Plan and Apply**: Executes `terraform plan` and `terraform apply` to provision environment specific infrastructure. 76 | 3. **Kubernetes Configuration**: Configures `kubectl` to interact with the Kubernetes cluster. 77 | 4. **Ingress Controller Setup**: Uses Helm to install the ingress controller. 78 | 5. **Application Deployment**: Uses `kubectl` to deploy the `Kustomized` application manifests. 79 | 80 | ## Infrastructure Details 81 | 82 | - **Dev Environment**: Uses `t3.small` EC2 instances and deploys a single replica. 83 | - **Staging Environment**: Uses `t3.medium` EC2 instances and deploys three replicas. 84 | - **Prod Environment**: Uses `t3.large` EC2 instances and deploys three replicas. 85 | 86 | DNS for all environments is automatically managed via Cloudflare and environment-specific subdomains are assigned and pointed to their respective LB hostname (using CNAME) ie., `dev.afraz.dev`, `staging.afraz.dev` and `prod.afraz.dev`. 87 | 88 | ## Notifications 89 | 90 | Slack notifications are configured to send updates at the end of each job. This provides immediate feedback on the success or failure of the pipeline and also updates on the DNS changes if applicable. 91 | 92 | ## GitOps Principles 93 | 94 | The pipeline adheres to GitOps principles, where Git serves as the single source of truth. Any change to the application or infrastructure is expected to be made through a Git commit. 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | -------------------------------------------------------------------------------- /app/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | node_modules 3 | npm-debug.log 4 | app.py 5 | Dockerfile-python 6 | terraform 7 | kustomize -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | COPY . . 8 | RUN npm install 9 | EXPOSE 3000 10 | CMD [ "node", "index.js" ] -------------------------------------------------------------------------------- /app/Dockerfile-python: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | WORKDIR /app 3 | RUN pip install flask==2.3 4 | COPY . /app/ 5 | ENV FLASK_APP=app.py 6 | CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"] -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from flask import Flask 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def hello(): 9 | return 'Hello, World!' -------------------------------------------------------------------------------- /app/calculator.js: -------------------------------------------------------------------------------- 1 | function add(numbers) { 2 | return numbers 3 | .split(',') 4 | .map(x => parseInt(x)) 5 | .reduce((a, b) => a + b ) 6 | } 7 | 8 | exports.add = add; -------------------------------------------------------------------------------- /app/calculator.test.js: -------------------------------------------------------------------------------- 1 | const calculator = require('./calculator') 2 | 3 | test('string with a single number should result in the number itself', () => { 4 | expect(calculator.add('1')).toBe(1); 5 | }); 6 | 7 | test('string with two numbers separated by comma should result in the sum of the numbers', () => { 8 | expect(calculator.add('4,5')).toBe(9); 9 | }); 10 | 11 | test('string with three numbers separated by comma should result in the sum of the numbers', () => { 12 | expect(calculator.add('2,8,4')).toBe(14); 13 | }); 14 | 15 | test('string with four numbers separated by comma should result in the sum of the numbers', () => { 16 | expect(calculator.add('2,0,4,5')).toBe(11); 17 | }); -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const qs = require('querystring') 3 | const calculator = require('./calculator') 4 | 5 | const server = http.createServer(function(request, response) { 6 | console.dir(request.param) 7 | 8 | if (request.method == 'POST') { 9 | console.log('POST') 10 | var body = '' 11 | request.on('data', function(data) { 12 | body += data 13 | }) 14 | 15 | request.on('end', function() { 16 | const post = qs.parse(body) 17 | const numbers = post.numbers 18 | const result = calculator.add(numbers) 19 | response.writeHead(200, {'Content-Type': 'text/html'}) 20 | response.end('Result: ' + result) 21 | }) 22 | } else { 23 | var html = ` 24 | 25 | 26 |

Input comma separated integers to add.

27 |
Numbers: 28 | 29 | 30 |
31 | 32 | ` 33 | response.writeHead(200, {'Content-Type': 'text/html'}) 34 | response.end(html) 35 | } 36 | }) 37 | 38 | const port = 3000 39 | const host = '0.0.0.0' 40 | server.listen(port, host) 41 | console.log(`Listening at http://${host}:${port}`) 42 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "addition-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "dependencies": { 10 | "jest": "^29.6.1" 11 | }, 12 | "keywords": [], 13 | "author": "Afraz", 14 | "license": "MIT" 15 | } -------------------------------------------------------------------------------- /kustomize/base/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: web-deployment 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: web 11 | template: 12 | metadata: 13 | labels: 14 | app: web 15 | -------------------------------------------------------------------------------- /kustomize/base/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: e2ecicd-ingress 5 | annotations: 6 | nginx.ingress.kubernetes.io/rewrite-target: / 7 | spec: 8 | ingressClassName: nginx-example 9 | rules: 10 | - host: "site.afraz.com" 11 | http: 12 | paths: 13 | - path: / 14 | pathType: Prefix 15 | backend: 16 | service: 17 | name: web-service 18 | port: 19 | number: 80 20 | -------------------------------------------------------------------------------- /kustomize/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - deploy.yaml 6 | - svc.yaml 7 | - ingress.yaml 8 | -------------------------------------------------------------------------------- /kustomize/base/svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: web-service 5 | spec: 6 | ports: 7 | - port: 80 8 | targetPort: 3000 9 | selector: 10 | app: web 11 | 12 | -------------------------------------------------------------------------------- /kustomize/overlays/dev/deploy-dev.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: web-deployment 5 | spec: 6 | replicas: 1 7 | template: 8 | spec: 9 | containers: 10 | - name: my-node-app 11 | image: aphraz/node-app:1.1.1-dev 12 | imagePullPolicy: Always 13 | resources: 14 | limits: 15 | cpu: "200m" 16 | memory: "256Mi" 17 | requests: 18 | cpu: "100m" 19 | memory: "128Mi" 20 | -------------------------------------------------------------------------------- /kustomize/overlays/dev/ingress-dev.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: e2ecicd-ingress 5 | annotations: 6 | nginx.ingress.kubernetes.io/rewrite-target: / 7 | spec: 8 | ingressClassName: nginx 9 | rules: 10 | - host: dev.afraz.dev 11 | http: 12 | paths: 13 | - path: / 14 | pathType: Prefix 15 | backend: 16 | service: 17 | name: web-service 18 | port: 19 | number: 80 20 | -------------------------------------------------------------------------------- /kustomize/overlays/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../base 6 | 7 | patches: 8 | - path: deploy-dev.yaml 9 | - path: svc-dev.yaml 10 | - path: ingress-dev.yaml 11 | namespace: app-dev 12 | -------------------------------------------------------------------------------- /kustomize/overlays/dev/svc-dev.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: web-service 5 | spec: 6 | ports: 7 | - port: 80 8 | targetPort: 3000 9 | type: ClusterIP 10 | -------------------------------------------------------------------------------- /kustomize/overlays/prod/deploy-prod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: web-deployment 5 | spec: 6 | replicas: 3 7 | template: 8 | spec: 9 | containers: 10 | - name: nginx 11 | image: aphraz/node-app:1.1.1-dev 12 | imagePullPolicy: Always 13 | resources: 14 | limits: 15 | cpu: "1" 16 | memory: "1Gi" 17 | requests: 18 | cpu: "500m" 19 | memory: "512Mi" 20 | strategy: 21 | type: RollingUpdate 22 | rollingUpdate: 23 | maxSurge: 1 24 | maxUnavailable: 1 25 | -------------------------------------------------------------------------------- /kustomize/overlays/prod/ingress-prod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: e2ecicd-ingress 5 | annotations: 6 | nginx.ingress.kubernetes.io/rewrite-target: / 7 | spec: 8 | ingressClassName: nginx 9 | rules: 10 | - host: prod.afraz.dev 11 | http: 12 | paths: 13 | - path: / 14 | pathType: Prefix 15 | backend: 16 | service: 17 | name: web-service 18 | port: 19 | number: 80 20 | -------------------------------------------------------------------------------- /kustomize/overlays/prod/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../base 6 | 7 | patches: 8 | - path: deploy-prod.yaml 9 | - path: svc-prod.yaml 10 | - path: ingress-prod.yaml 11 | 12 | namespace: app-prod 13 | -------------------------------------------------------------------------------- /kustomize/overlays/prod/svc-prod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: web-service 5 | spec: 6 | ports: 7 | - port: 80 8 | targetPort: 3000 9 | type: ClusterIP 10 | -------------------------------------------------------------------------------- /kustomize/overlays/staging/deploy-staging.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: web-deployment 5 | spec: 6 | replicas: 3 7 | template: 8 | spec: 9 | containers: 10 | - name: nginx 11 | image: aphraz/node-app:1.1.1-dev 12 | imagePullPolicy: Always 13 | resources: 14 | limits: 15 | cpu: "750m" 16 | memory: "512Mi" 17 | requests: 18 | cpu: "500m" 19 | memory: "512Mi" 20 | strategy: 21 | type: RollingUpdate 22 | rollingUpdate: 23 | maxSurge: 1 24 | maxUnavailable: 1 25 | -------------------------------------------------------------------------------- /kustomize/overlays/staging/ingress-staging.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: e2ecicd-ingress 5 | annotations: 6 | nginx.ingress.kubernetes.io/rewrite-target: / 7 | spec: 8 | ingressClassName: nginx 9 | rules: 10 | - host: staging.afraz.dev 11 | http: 12 | paths: 13 | - path: / 14 | pathType: Prefix 15 | backend: 16 | service: 17 | name: web-service 18 | port: 19 | number: 80 20 | -------------------------------------------------------------------------------- /kustomize/overlays/staging/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../base 6 | 7 | patches: 8 | - path: deploy-staging.yaml 9 | - path: svc-staging.yaml 10 | - path: ingress-staging.yaml 11 | namespace: app-staging 12 | -------------------------------------------------------------------------------- /kustomize/overlays/staging/svc-staging.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: web-service 5 | spec: 6 | ports: 7 | - port: 80 8 | targetPort: 3000 9 | type: ClusterIP 10 | -------------------------------------------------------------------------------- /terraform/ingress-nginx.tf: -------------------------------------------------------------------------------- 1 | provider "kubernetes" { 2 | config_path = "~/.kube/config" 3 | 4 | } 5 | 6 | provider "helm" { 7 | kubernetes { 8 | config_path = "~/.kube/config" 9 | } 10 | } 11 | 12 | resource "null_resource" "kubectl" { 13 | provisioner "local-exec" { 14 | command = "aws eks --region ${var.region} update-kubeconfig --name ${local.cluster_name}" 15 | } 16 | depends_on = [module.eks] 17 | } 18 | 19 | resource "helm_release" "ingress_nginx" { 20 | name = "ingress-nginx" 21 | repository = "https://kubernetes.github.io/ingress-nginx" 22 | chart = "ingress-nginx" 23 | 24 | 25 | namespace = var.ingress_nginx_namespace 26 | create_namespace = true 27 | 28 | 29 | depends_on = [ 30 | module.eks, 31 | null_resource.kubectl 32 | ] 33 | 34 | } 35 | 36 | resource "null_resource" "wait_for_ingress_nginx" { 37 | triggers = { 38 | key = uuid() 39 | } 40 | 41 | provisioner "local-exec" { 42 | command = <