c03dddbab4b2be117bb63b873f1c6ec9.png

本篇文章是「DevOps云学堂」与你共同进步的第 61


DevSecOps 流程
先决条件:
1) Git
2) Jenkins
3) Sonar-Scanner
4) Snyk
5) Java、Maven、Node.js、Python 等(您为项目选择的语言将取决于适用的安装要求。
6) Docker
7) Aqua Trivy
8) Kubernetes
9) Zaproxy

Jenkinsfile(Groovy 脚本)

// Define the detectJavaVersion function outside of the pipeline block
def detectJavaVersion() {
    def javaVersionOutput = sh(script: 'java -version 2>&1', returnStatus: false, returnStdout: true).trim()
    def javaVersionMatch = javaVersionOutput =~ /openjdk version "(\d+\.\d+)/

    if (javaVersionMatch) {
        def javaVersion = javaVersionMatch[0][1]

        if (javaVersion.startsWith("1.8")) {
            return '8'
        } else if (javaVersion.startsWith("11")) {
            return '11'
        } else if (javaVersion.startsWith("17")) {
            return '17'
        } else {
            error("Unsupported Java version detected: ${javaVersion}")
        }
    } else {
        error("Java version information not found in output.")
    }
}
pipeline {
    agent any
    environment {
        SONARCLOUD = 'Sonarcloud'
        SNYK_INSTALLATION = 'snyk@latest'
        SNYK_TOKEN = 'Snyk'
        DOCKER_REGISTRY_CREDENTIALS = 'Docker_Server'
        DOCKER_IMAGE = 'ganesharavind124/anacart:latest'
        DOCKER_TOOL = 'Docker'
        DOCKER_URL = 'https://index.docker.io/v1/'
        KUBE_CONFIG = 'kubernetes'
    }
    stages {
        stage('Clean Workspace') {
            steps {
                cleanWs()
            }
        }
        stage('Git-Checkout') {
            steps {
                checkout scm
            }
        }
        // /opt/sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner
        stage('Compile and Run Sonar Analysis') {
            steps {
                script {
                    withSonarQubeEnv(credentialsId: SONARCLOUD, installationName: 'Sonarcloud') {
                        try {
                            if (fileExists('pom.xml')) {
                                sh 'mvn verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar'
                            } else if (fileExists('package.json')) {
                                sh "${sonarscanner} -Dsonar.organization=jenkeen -Dsonar.projectKey=jenkeen_testjs -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=b8c55c159b1fd559baaccf9bee42344faed0a7b4"
                            } else if (fileExists('go.mod')) {
                                sh "${sonarscanner} -Dsonar.organization=jenkeen -Dsonar.projectKey=jenkeen_go -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=b8c55c159b1fd559baaccf9bee42344faed0a7b4"
                            } else if (fileExists('Gemfile')) {
                                sh "${sonarscanner} -Dsonar.organization=jenkeen -Dsonar.projectKey=jenkeen_ruby -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=b8c55c159b1fd559baaccf9bee42344faed0a7b4"
                            } else if (fileExists('requirements.txt')) {
                                sh "${sonarscanner} -Dsonar.organization=jenkeen -Dsonar.projectKey=jenkeen_python -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=b8c55c159b1fd559baaccf9bee42344faed0a7b4"
                            } else {
                                currentBuild.result = 'FAILURE'
                                pipelineError = true
                                error("Unsupported application type: No compatible build steps available.")
                            }
                        } catch (Exception e) {
                            currentBuild.result = 'FAILURE'
                            pipelineError = true
                            error("Error during Sonar analysis: ${e.message}")
                        }
                    }
                }
            }
        }
        stage('snyk_analysis') {
            steps {
                script {
                    echo 'Testing...'
                    try {
                        snykSecurity(
                            snykInstallation: SNYK_INSTALLATION,
                            snykTokenId: SNYK_TOKEN,
                            failOnIssues: false,
                            monitorProjectOnBuild: true,
                            additionalArguments: '--all-projects --d'
                        )
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        pipelineError = true
                        error("Error during snyk_analysis: ${e.message}")
                    }
                }
            }
        }
        stage('Detect and Set Java') {
            steps {
                script {
                    try {
                        def javaVersion = detectJavaVersion()
                        tool name: "Java_${javaVersion}", type: 'jdk'
                        sh 'java --version'
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        pipelineError = true
                        error("Error during Java version detection: ${e.message}")
                    }
                }
            }
        }
        stage('Frontend Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('package.json')) {
                            //sh 'npm install --force'
                            //sh 'npm test'
                        } else {
                            echo 'No package.json found, skipping Frontend build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        pipelineError = true
                        error("Error during Frontend build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Java Spring Boot Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('pom.xml')) {
                            sh 'mvn clean package'
                            sh 'mvn test'
                        } else {
                            // If pom.xml doesn't exist, print a message and continue
                            echo 'No pom.xml found, skipping Java Spring Boot build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Java Spring Boot build and test: ${e.message}")
                    }
                }
            }
        }

        stage('.NET Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('YourSolution.sln')) {
                            sh 'dotnet build'
                            sh 'dotnet test'
                        } else {
                            // If YourSolution.sln doesn't exist, print a message and continue
                            echo 'No YourSolution.sln found, skipping .NET build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during .NET build and test: ${e.message}")
                    }
                }
            }
        }
        stage('PHP Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('composer.json')) {
                            sh 'composer install'
                            sh 'phpunit'
                        } else {
                            // If composer.json doesn't exist, print a message and continue 
                            echo 'No composer.json found, skipping PHP build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during PHP build and test: ${e.message}")
                    }
                }
            }
        }
        stage('iOS Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('YourProject.xcodeproj')) {
                            xcodebuild(buildDir: 'build', scheme: 'YourScheme')
                        } else {
                            // If YourProject.xcodeproj doesn't exist, print a message and continue
                            echo 'No YourProject.xcodeproj found, skipping iOS build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during iOS build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Android Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('build.gradle')) {
                            sh './gradlew build'
                            sh './gradlew test'
                        } else {
                            // If build.gradle doesn't exist, print a message and continue
                            echo 'No build.gradle found, skipping Android build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Android build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Ruby on Rails Build and Test') {
            steps {
                script {
                    try {
                        // Check if Gemfile.lock exists
                        if (fileExists('Gemfile.lock')) {
                            sh 'bundle install' // Install Ruby gem dependencies
                            sh 'bundle exec rake db:migrate' // Run database migrations
                            sh 'bundle exec rails test' // Run Rails tests (adjust as needed)
                        } else {
                            // If Gemfile.lock doesn't exist, print a message and continue
                            echo 'No Gemfile.lock found, skipping Ruby on Rails build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Ruby on Rails build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Flask Build and Test') { // To build and run a Python Flask Framework Application
            steps {
                script {
                    try {
                        if (fileExists('app.py')) {
                            sh 'pip install -r requirements.txt' // Install dependencies
                            sh 'python -m unittest discover' // Run Flask unit tests
                        } else {
                            // If app.py doesn't exist, print a message and continue
                            echo 'No app.py found, skipping Flask build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Flask build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Django Build and Test') { // To build and run a Python Django Framework Application
            steps {
                script {
                    try {
                        if (fileExists('manage.py')) {
                            sh 'pip install -r requirements.txt' // Install dependencies
                            sh 'python manage.py migrate' // Run Django migrations
                            sh 'python manage.py test' // Run Django tests
                        } else {
                            // If manage.py doesn't exist, print a message and continue
                            echo 'No manage.py found, skipping Django build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Django build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Rust Build and Test') { //To build and run a Rust Application
            steps {
                script {
                    try {
                        if (fileExists('Cargo.toml')) { // Check if Cargo.toml file exists 
                            env.RUST_BACKTRACE = 'full' // Set the RUST_BACKTRACE environment variable to full for better error messages
                            sh 'cargo build' // Build the Rust project
                            sh 'cargo test' // Run the Rust tests
                        } else {
                            // If Cargo.toml doesn't exist, print a message and continue
                            echo "No Cargo.toml file found. Skipping Rust build and test."
                        }
                    } catch (Exception e) {
                        // Set the build result to FAILURE and print an error message
                        currentBuild.result = 'FAILURE'
                        error("Error during Rust build and test: ${e.message}")
                    }
                }

            }
        }
        stage('Ruby Sinatra Build and Test') { //To build and run a Ruby Application
            steps {
                script {
                    try {
                        if (fileExists('app.rb')) { // Check if app.rb file exists                 
                            sh 'gem install bundler' // Install Bundler
                            sh 'bundle install' // Use bundle exec to ensure gem dependencies are isolated        
                            sh 'bundle exec rake test' // Run the Sinatra tests using Rake
                        } else {
                            // If app.rb doesn't exist, print a message and continue
                            echo "No app.rb file found. Skipping Ruby Sinatra build and test."
                        }
                    } catch (Exception e) {
                        // Set the build result to FAILURE and print an error message
                        currentBuild.result = 'FAILURE'
                        error("Error during Ruby Sinatra build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Build and Push Docker Image') {
            steps {
                script {
                    try {
                        if (fileExists('Dockerfile')) {
                            withDockerRegistry(credentialsId: DOCKER_REGISTRY_CREDENTIALS, toolName: DOCKER_TOOL, url: DOCKER_URL) {
                                def dockerImage = docker.build(DOCKER_IMAGE, ".")
                                // Push the built Docker image
                                dockerImage.push()
                            }
                        } else {
                            echo "Dockerfile not found. Skipping Docker image build and push."
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        pipelineError = true
                        echo "Error during Docker image build and push: ${e.message}"
                    }
                }
            }
        }
        stage('Trivy Scan') {
            steps {
                script {
                    def trivyInstalled = sh(script: 'command -v trivy', returnStatus: true) == 0
                    def imageName = DOCKER_IMAGE
                if (trivyInstalled) {
                    sh "trivy image --format table ${imageName}"
                } else {
                    // Run trivy using Docker
                    sh "docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image --format table ${imageName}"
                }
                
            }
            
        }
    }
    stage('Kubernetes Deployment') {
        steps {
            script {
                def configFile = 'deployment.yaml'
                def namespace = 'anacart' // Replace 'your-namespace' with your actual namespace 
    
                if (fileExists(configFile)) {
                     kubernetesDeploy(configs: configFile, kubeconfigId: KUBE_CONFIG, namespace: namespace)
                } else {
                    error("Error: $configFile does not exist")
                    currentBuild.result = 'FAILURE'
                    pipelineError = true
                }
            }
        }
    }
    stage('Run DAST Using ZAP') {
      steps {
        script {
          try {
            def targetURL =  "http://192.168.58.2:32765" // Use the obtained service URL as the target URL
            def zapCommand = "zaproxy -cmd -quickurl ${targetURL}"
            //sh(zapCommand)
            sh("echo zap_report.html")
            //archiveArtifacts artifacts: 'zap_report.html'
          } catch (Exception e) {
            currentBuild.result = 'FAILURE'
            error("Error during ZAP DAST: ${e.message}")
          }
        }
      }
    }
  }
}

简介

在当今快节奏的软件开发环境中,实施高效的 CI/CD 管道至关重要。本博客概述了使用 Jenkins 构建强大的 CI/CD 管道、集成各种工具以实现多语言应用程序的无缝自动化、安全性和部署的旅程。
准备阶段:
这个项目涉及编排一个 CI/CD 管道,其中包括 GitSonarCloudSynk多语言构建自动化DockerAqua TrivyKubernetesZAP Proxy。利用 Jenkins 的灵活性和 Groovy 脚本编写功能,我简化了这些将工具整合到一个有凝聚力的管道中。

管道配置

进入管道作业的配置页面。将打开此页面。在那里添加您的 Jenkins管道脚本路径。有两种选择。
1. 管道脚本:在这里,您可以轻松编写自己的脚本。
2. 来自 SCM 的管道:它将使用 SCM 存储库的 Jenkins 文件。
这里我选择第二个选项:
66af5496b6b03eaf9d4f4f10b76f8f76.png
因此,选择您的 SCM 并提供您的分支和存储库的 URL,并在脚本路径中提及您的 Jenkinsfile。
3b4635bd784574f54a94cf7fee956640.png

第 1 阶段(清理工作区)

在此阶段,我们将清理工作区,其中之前部署的文件和文档,在此阶段完成后,git 将拉取新更新的文件并运行新的所有内容。
d6c97429c5d5b8b8c9fca0ee0ba25a9f.png

第 2 阶段(Git Checkout)

我们在项目中使用了多种源代码管理系统,包括GitHub、GitLab、AWS codecommit,以及bitbucket、SVN、TFS等;但是,我没有将该信息包含在流程图中。
c733114a9a0872d5d3a84d79ef11c812.png
git 配置: 在上面的 SCM 中提供您的 Git 详细信息;因此,请使用 SCM 中的 git 详细信息的 URL 和分支名称来更新它们。
1267afce5eaff5c5551c7b98f39c5917.png
git 签出:
注意:如果您的 git 存储库是私有的,您应该向您的 Jenkins 帐户提供您的 Gitlab 个人访问令牌或 git 凭据。

第 3 阶段(SonarCloud)

SonarCloud 用于执行 SAST 代码质量扫描,因此通过添加个人访问令牌或身份验证令牌将其与 Jenkins 集成。您还可以将声纳扫描仪工具称为声纳扫描仪,或您选择的任何其他工具,并且不要忘记将其包含在您的管道中。
351875cc104eefdd696b9146ca4dd598.png
有两种选项可以运行 sonarcloud :
1) 在 git 存储库中创建 sonar-project-properties 文件,并提供 sonarcloud 详细信息,如下所示:
43a5c469fb3fc4ffd2d03cb8cf2c633e.png
sonar-project.properties
在这里,将您的声纳扫描仪路径以及您的 pom.xml、csproj、解决方案文件、包添加到 Jenkins 管道脚本中。Json、Gem 文件、requirement.txt 等
2)您可以直接在Jenkins文件中提及您的sonarcloud脚本。
8c25db78cbc85bcb4131e457d5872c15.png
编译并运行Sonar分析

第 4 阶段(Synk安全漏洞扫描)

Synk 用于执行安全漏洞扫描,因此通过为其提供个人访问令牌或身份验证令牌将其与 Jenkins 集成。您还可以将您的 synk 安装工具称为 Snyk@latest,或者您选择的任何其他工具,并且不要忘记将其包含在您的管道中。
7bd800b8e53070638403f444a629e485.png
现在,在您的管道中提及您的安装和 Snyk 令牌的名称,以便它知道您正在尝试访问哪个 API。
9f5a10da4c6b05613de0c8a10a64d381.png

第 5 阶段(Java 检测)

正如我之前指出的,Java 可能会被自动检测到,您将能够看到它是否受支持。因此,在执行此操作之前,请确保您已在 Jenkins 工具中设置了 JDK。
1b9025aa0acef85cf35efcc9c24c291e.png
检测Java版本,所以这里 java 检测并设置 java pipeline 脚本如下所示:
0bbe8e60d86bfad66b800a265ebc929b.png
检测并设置 Java

第 6 阶段(多语言构建和部署)

在这个阶段,我提供了多种编程语言,包括前端、后端、iOS、Android、Ruby、Flask等等。根据我提供的语言,系统将从您的存储库中识别源代码,并根据我们之前讨论的管道脚本安装、构建和执行测试。


Java、Maven、Node.js、Python 等(您为项目选择的语言将取决于适用的安装要求。)在这里,我在项目中使用 Node.js。
d8698f6c2982b419ec9d68331f204406.png
多语言构建阶段,您可以在上图中看到多语言构建的管道脚本。

第 7 阶段(Docker 构建和推送)

在此阶段,我们将在构建源代码后对我们的项目进行 dockerize。我们的pipeline脚本会自动识别dockerfile是否存在,如果不存在则生成dockerfile,否则会显示dockerfile not find。


注意:请确保在环境阶段正确指定 Docker 镜像的名称(变量名称将自动识别并获取镜像名称)。Dockerfile 名称区分大小写,在 Jenkins 中添加 docker 工具和 docker API。
253336dce63ad812c45cecd980ccdfe2.png
构建并推送 Docker 镜像
在此阶段,我们将把我们的镜像推送并存储在 Docker Hub、AWS ECR、GCP GCR、Harbor 等容器注册表中。在本例中,我通过提供我的凭据并指示我要推送到我的集线器存储库的 Docker API 来使用 Docker Hub。在此之前,不要忘记在 Docker Hub 上设置一个存储库。


要链接到您的容器注册表,请确保向 Jenkins 提供您的凭据或个人访问令牌。在环境阶段提及您的凭据。
bb5bd8005f4520a6e1ccbd0cb0c1cf98.png
环境
注意:通过在本地使用 docker run 命令,您可以验证 Docker 映像是否已启动并正在运行。

第 8 阶段(Aqua Trivy 镜像扫描)

现在 Docker 构建已经完成并且我们的映像已成功生成,是时候通过扫描来检测任何漏洞了。我们将使用 Aqua Trivy Scan 进行图像扫描。


验证 Aqua Trivy 是否已安装在您的本地系统上。如果您的系统上尚未安装 trivy,请从 docker 获取它并运行 trivy 映像。完成后,尝试使用 docker trivy image 扫描您的映像。使用以下 docker trivy 命令将映像名称放在映像命令后面:
docker run ghcr.io/aquasecurity/trivy:最新镜像 DOCKER_IMAGE
881e3d6e79279f4242556bf6a94813ae.png
Aqua Trivy 扫描
在这里提及您的 docker 映像,它将扫描并检测漏洞。

第 9 阶段(Kubernetes)

这是我们现在所处的主要阶段。到目前为止,一切都按计划进行,我们构建、部署和 Docker 化了我们的镜像并将其推送到中心。但是,我们必须在运行时托管我们的程序。流程是怎样的?应用 Kubernetes 是前进的方向。


在集成 Kubernetes 和 Jenkins 之前,请确保您已安装集群;它们是 minikube、kind 还是 kubeadm 并不重要。如果您使用负载均衡器,请安装 kubeadm 并构建您的主节点和工作节点。如果您使用的是 nodeport,请在 Jenkins 从机上安装 minikube 或 kind 集群。


注意:您可以使用 kube 配置文件将 Jenkins 与 Kubernetes 集群集成。
30cdc9731ba90d3b974aaa47a0ea418c.png
Kubernetes 部署
在环境阶段,提供您的 kube 配置凭据并添加部署.yaml 文件的名称来代替配置文件。
c5d045db42f8e19ba18f8412a70b1fc6.png
环境
在成功创建部署后,应用程序现在将在您的 Pod 上运行。您可以通过使用服务名称运行 (kubectl get svc) 进行测试。如果您使用负载均衡器,您将收到外部 IP 并能够通过它访问您的应用程序。
如果您使用 minikube 运行(minikube 服务 MY-SERVICE-NAME),您将收到您的 IP 和端口号,并能够通过它访问您的应用程序。

第 10 阶段(Zaproxy 测试)

我们已经进行了 SAST 扫描和应用测试;展望未来,我们将执行 DAST,其目的是在整个软件开发和测试阶段协助检测 Web 应用程序中的安全漏洞。


基本上,ZAP 测试将涉及使用该 URL 来测试 PROD 或 DEV 中托管的应用程序。我们将使用各种扫描方法,包括蜘蛛、主动、被动、模糊器、代理拦截和脚本攻击。不过,目前我只是进行基本的 zap 测试,生成并向我们提供报告。


确保 ZAPROXY 已安装在您的本地或实例或服务器系统上。
这里我使用了 minikube,所以我直接在 Jenkins 管道中提供了 URL。
23c35d731c29764dcc2bb36f528eba2c.png
使用 Zaproxy 进行 DAST 扫描
使用Loadbalancer时,会自动执行zap命令,无需手动输入,并且自动生成IP和端口。使用以下脚本自动检测 URL。
ed0b5d3ae4466df6f3932bc345d82c55.png

让我们通过运行管道脚本来实际看看:

创建管道作业并为其指定一个您选择的名称,例如 Devsecops。
049fd253c049a34e46c6dbcf32aba8c3.png
创建新的管道作业: 创建管道作业后将如下所示
2c6f8c1bf6ca669edd10746fb6b2561d.png
新的 DevSecOps 工作
进入管道作业的配置页面。将打开此页面。在那里添加您的 Jenkins 管道脚本。
有两种选择。
1)管道脚本:在这里,您可以轻松编写自己的脚本。
2)来自 SCM 的管道:它将使用 SCM 存储库的 Jenkins 文件。
c2fa09b0aaa0fa05fca0f01936a647b0.png
管道配置
我从 SCM 选择 Pipeline 脚本,因为我的 SCM 中有 Jenkinsfile(groovy 脚本)。
我也会向您展示另一种方法第二种方法。
ea3ddb3df912556750cd48ae28ebcd48.png
在保存和应用之前检查所有行、大括号和凭据。您还应该确保环境和阶段中的变量名称相同,因为很多人在这个特定区域会犯错误。接下来,单击“应用”。如果遇到任何问题,该行中会出现一个 X。如果您更改“保存”,页面将重定向到主站点。之后,单击“立即构建”按钮。
bf9a948fc045f99b13c496c7b39eb030.png
构建历史
作业将开始执行。您可以在控制台查看作业结果,看看是否有问题。
ce5f0f0e9ad91da0de6d4eca405f473d.png
控制台输出
我们可以看到我们的工作输出已经成功。来输出一下吧
75c686d1f7d0e520fa71f41cc5808739.png
管道构建阶段
Snyk:
2a0b2d9d7ba4c7be38cc9f503cc19085.png
Snyk
ba8c19a5e443a3f4ee249a6449f3a669.png
SonarCloud:
2e4e7049b6175296cd2c8fbb5b5ed70a.png
Docker hub:
3221f6a8cce36193939623c8aa147311.png
Aqua Trivy 报告:
55efba7a90d24b8232d5baa69b6ce59a.png
Kubernetes 部署:
5829d29ebd8e3092458f1fa0c02dc183.png
ZapProxy:
6475ce9af805d1a62ce23ca0a02015bb.png

ANACART(我们可以看到我们的应用程序已成功托管):
f0b04640f5888e6227d686318e6e9239.png

文章翻译 https://medium.com/@ganesharavind124/devsecops-pipeline-automating-ci-cd-pipeline-for-secure-multi-language-applications-using-jenkins-e66107dc4c04

往期推荐

【开放视频+文档】Spinnaker多云持续部署实践

开放DevOps,ArgoCD,Terraform实践文档

Atlassian & JFrog:重塑 DevOps 的软件安全之路

Kubernetes Operator简介与构建

2024 - 推动DevOps 工程落地的领域相关工具

Kargo-面向K8s的下一代持续交付和应用生命周期编排平台

降低DevOps入门门槛,企业级实践,持续累计5年打造一套精品实践课程!邀共赏!

73a37ba8c70930de8c7ddb1ebb062595.png

遇到devops工程实践问题无从下手,圈内提问,400+同行共同探讨!邀共享!

abfdede7469c9fbdc1d6ebbd6be77acd.png

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐