kiến thức DevOps thực tế và chi tiết

file cài đặt Jenkins CI/CD Pipeline

File cài đặt trên server

jenkins-setup.sh file

#!/bin/bash

apt install openjdk-11-jdk -y
java --version
wget -p -O - https://pkg.jenkins.io/debian/jenkins.io.key | apt-key add -
sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5BA31D57EF5975CA
apt-get update
apt install jenkins -y
systemctl start jenkins
ufw allow 8080

docker-setup.sh file

#!/bin/bash

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

docker-compose.yml (cài đặt sonarqube server)

version: "3"

services:
  sonarqube:
    image: sonarqube:8.9.2-community
    depends_on:
      - db
    environment:
      SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
      SONAR_JDBC_USERNAME: sonar
      SONAR_JDBC_PASSWORD: sonar #tochange
    volumes:
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions
      - sonarqube_logs:/opt/sonarqube/logs
    ports:
      - "9000:9000"
  db:
    image: postgres:12
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: sonar #tochange
    volumes:
      - postgresql:/var/lib/postgresql
      - postgresql_data:/var/lib/postgresql/data

volumes:
  sonarqube_data:
  sonarqube_extensions:
  sonarqube_logs:
  postgresql:
  postgresql_data:

Cài đặt sonarqube đơn giản hơn với docker run

$ docker run -d --name sonarqube -p 9000:9000 sonarqube

File cấu hình trong dự án

Dockerfile (backend)

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["BE.csproj", "."]
RUN dotnet restore "./BE.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "BE.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "BE.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "BE.dll"]

Dockerfile.dotnet6 (backend)

FROM mcr.microsoft.com/dotnet/sdk:6.0

RUN dotnet tool install --global dotnet-ef

ENV PATH="$PATH:/root/.dotnet/tools"

Dockerfile (frontend)

FROM node:16 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./ .
RUN npm run build:stage

FROM nginx as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf

nginx.conf (frontend)

user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
  worker_connections  1024;
}
http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;
  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
  access_log  /var/log/nginx/access.log  main;
  sendfile        on;
  keepalive_timeout  65;
  server {
    listen       80;
    server_name  localhost;
    location / {
      root   /app;
      index  index.html;
      try_files $uri $uri/ /index.html;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
      root   /usr/share/nginx/html;
    }
  }
}

docker-compose.yml

version: '3.4'
services:
  backend:
    container_name: ${NAME_BACKEND}
    build:
      context: ./BE/
      dockerfile: "Dockerfile"
      args:
        IMAGE_TAG: ${IMAGE_TAG:-1.0.0}
    restart: always
    image: ${NAME_BACKEND}:${IMAGE_TAG}
    ports:
      - "8081:80"
    environment:
      - IMAGE_TAG=${IMAGE_TAG:-1.0.0}
      - NAME_BACKEND=${NAME_BACKEND}

  frontend:
    container_name: ${NAME_FRONTEND}
    build:
      context: ./FE/
      dockerfile: "Dockerfile"
      args:
        IMAGE_TAG: ${IMAGE_TAG:-1.0.0}
    restart: always
    image: ${NAME_FRONTEND}:${IMAGE_TAG}
    ports:
      - "80:80"
    environment:
      - IMAGE_TAG=${IMAGE_TAG:-1.0.0}
      - NAME_FRONTEND=${NAME_FRONTEND}

Jenkinsfile

pipeline {
    agent any
    environment {
        PATH_PROJECT = '/home/projects/devopsedu-central'
        SONAR_PROJECT_KEY = 'root_devopsedu-central_AYe5jPZrkpMDT6RJzelC'
        SONAR_TOKEN = credentials('token-devopsedu-central')

        MIGRATION_NAME = sh (script: 'echo $(date +%Y%m%d%H%M%S)', returnStdout: true).trim()
        
        DOCKER_HUB = 'elroydev'
        DOCKERHUB_CREDENTIALS = credentials('dockerhub-credentials')
        NAME_BACKEND = 'devopsedu-central-backend'
        NAME_FRONTEND = 'devopsedu-central-frontend'
        DOCKER_TAG="${GIT_BRANCH.tokenize('/').pop()}-${GIT_COMMIT.substring(0,7)}"
    }
    stages {
        stage('Checkout source') {
            steps {
                sh "sudo cp -r . $PATH_PROJECT"
            }
        }
        stage('Test backend with dotnet') {
            steps {
                sh "cd $PATH_PROJECT/BE \
                && docker build -t dotnet6-app -f Dockerfile.dotnet6 . \
                && docker run --rm -v .:/app -w /app dotnet6-app dotnet test"
                }
        } 
        stage('Test with sonarqube') {
            steps {
                withSonarQubeEnv('Sonarqube server connection') {
                    sh "cd $PATH_PROJECT \
                    && docker run --rm -e SONAR_HOST_URL=${env.SONAR_HOST_URL} \
                    -e SONAR_SCANNER_OPTS='-Dsonar.projectKey=$SONAR_PROJECT_KEY' \
                    -e SONAR_TOKEN=$SONAR_TOKEN \
                    -v '.:/usr/src' \
                    sonarsource/sonar-scanner-cli"
                }
            }
        } 
        stage('Migration database') {
            steps {
                script {
                    try {
                        timeout(time: 5, unit: 'MINUTES') {
                            env.userChoice = input message: 'Do you want to migrate the database?',
                                parameters: [choice(name: 'Versioning Service', choices: 'no\nyes', description: 'Choose "yes" if you want to migrate!')]
                        }
                        if (env.userChoice == 'yes') {
                            echo "Migration success!"
                            // sh "cd $PATH_PROJECT/BE \
                            // && docker run --rm -v .:/app -w /app dotnet6-app dotnet ef migrations add $MIGRATION_NAME \
                            // && docker run --rm -v .:/app -w /app dotnet6-app dotnet ef database update"
                        } else {
                            echo "Migration cancelled."
                        }
                    } catch (Exception err){
                        def user = err.getCauses()[0].getUser()
                        if ('SYSTEM' == user.toString()) {
                            def didTimeout = true
                            echo "Timeout. Migration cancelled."
                        } else {
                            echo "Migration cancelled by: ${user}"
                        }
                    }
                }
            }
        }
        stage('Build and push images') {
            steps {
                script {
                    env.IMAGE_TAG = DOCKER_TAG
                    sh "cd $PATH_PROJECT \
                    && IMAGE_TAG=${IMAGE_TAG} \
                    && NAME_BACKEND=${NAME_BACKEND} \
                    && NAME_FRONTEND=${NAME_FRONTEND} \
                    && docker-compose build --parallel \
                    && docker tag ${NAME_BACKEND}:$DOCKER_TAG ${DOCKER_HUB}/${NAME_BACKEND}:$DOCKER_TAG \
                    && docker tag ${NAME_FRONTEND}:$DOCKER_TAG ${DOCKER_HUB}/${NAME_FRONTEND}:$DOCKER_TAG \
                    && echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin \
                    && docker push ${DOCKER_HUB}/${NAME_BACKEND}:$DOCKER_TAG \
                    && docker push ${DOCKER_HUB}/${NAME_FRONTEND}:$DOCKER_TAG \
                    && docker rmi ${DOCKER_HUB}/${NAME_BACKEND}:$DOCKER_TAG \
                    && docker rmi ${DOCKER_HUB}/${NAME_FRONTEND}:$DOCKER_TAG"
                }
            }
        }
        stage('Deploy to staging')
        {
            steps {
                script {
                    def deploying = "#!/bin/bash\n" +
                        "docker rm -f ${NAME_BACKEND} ${NAME_FRONTEND}\n" +
                        "docker pull ${DOCKER_HUB}/${NAME_BACKEND}:$DOCKER_TAG\n" +
                        "docker pull ${DOCKER_HUB}/${NAME_FRONTEND}:$DOCKER_TAG\n" +
                        "docker run --name=${NAME_BACKEND} -dp 8081:80 ${DOCKER_HUB}/${NAME_BACKEND}:$DOCKER_TAG\n" +
                        "docker run --name=${NAME_FRONTEND} -dp 80:80 ${DOCKER_HUB}/${NAME_FRONTEND}:$DOCKER_TAG"

                    sshagent(credentials: ['jenkins-ssh-key']) {
                        sh """
                            ssh -o StrictHostKeyChecking=no -i jenkins-ssh-key [email protected] "echo \\\"${deploying}\\\" > deploy.sh && chmod +x deploy.sh && ./deploy.sh"
                        """                  
                    }
                }
            }
        }
    }
}