一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

在容器上構建持續(xù)部署,這份超詳細實踐指南不要錯過!

 520jefferson 2020-09-12

要想理解持續(xù)集成和持續(xù)部署,先要了解它的部分組成,以及各個組成部分之間的關系。下面這張圖是我見過的最簡潔、清晰的持續(xù)部署和集成的關系圖。
圖片來源(https://www./products-overview)

持續(xù)部署

如圖所示,開發(fā)的流程是這樣的:
程序員從源碼庫(Source Control)中下載源代碼,編寫程序,完成后提交代碼到源碼庫,持續(xù)集成(Continuous Integration)工具從源碼庫中下載源代碼,編譯源代碼,然后提交到運行庫(Repository),然后持續(xù)交付(Continuous Delivery)工具從運行庫(Repository)中下載代碼,生成發(fā)布版本,并發(fā)布到不同的運行環(huán)境(例如DEV,QA,UAT, PROD)。
圖中,左邊的部分是持續(xù)集成,它主要跟開發(fā)和程序員有關;右邊的部分是持續(xù)部署,它主要跟測試和運維有關。持續(xù)交付(Continuous Delivery)又叫持續(xù)部署(Continuous Deployment),它們如果細分的話還是有一點區(qū)別的,但我們這里不分得那么細,統(tǒng)稱為持續(xù)部署。本文側重講解持續(xù)部署。
持續(xù)集成和部署有下面幾個主要參與者:
  • 源代碼庫:負責存儲源代碼,常用的有Git和SVN;

  • 持續(xù)集成與部署工具:負責自動編譯和打包以及把可運行程序存儲到可運行庫。比較流行的有Jenkins,GitLab,Travis CI,CircleCI 等;

  • 庫管理器(Repository Manager):也就是圖中的Repository,我們又叫運行庫,負責管理程序組件。最常用的是Nexus。它是一個私有庫,它的作用是管理程序組件。

庫管理器有兩個職能:
  1. 管理第三方庫:應用程序常常要用到很多第三方庫,并且不同的技術棧需要的庫不同,它們經(jīng)常是存放在第三方公共庫里,管理起來不是很方便。一般公司會建立一個私有管理庫,來集中統(tǒng)一管理各種第三方軟件,例如它既可以做為Maven庫(Java),也可以做為鏡像庫(Docker),還可以做為NPM庫(JavaScript),來保證公司軟件的規(guī)范性;

  2. 管理內部程序的交付:所有公司在各種環(huán)境(例如DEV,QA,UAT, PROD)發(fā)布的程序都由它來管理,并賦予統(tǒng)一的版本號,這樣任何交付都有據(jù)可查,同時便利于程序回滾。

持續(xù)部署步驟

各個公司對持續(xù)部署(Continuous Deployment)的要求不同,它的步驟也不相同,但主要包括下面幾個步驟:
  • 下載源碼:從源代碼庫(例如github)中下載源代碼;

  • 編譯代碼:編譯語言都需要有這一步;

  • 測試:對程序進行測試;

  • 生成鏡像:這里包含兩個步驟,一個是創(chuàng)建鏡像,另一個是存儲鏡像到鏡像庫;

  • 部署鏡像:把生成的鏡像部署到容器上。

上面的流程是廣義的持續(xù)部署流程,狹義的流程是從庫管理器中檢索可運行程序,這樣就省去了下載源碼和編譯代碼環(huán)節(jié),改由直接從庫管理器中下載可執(zhí)行程序。但由于并不是每個公司都有單獨的庫管理器,這里就采用了廣義的持續(xù)部署流程,這樣對每個公司都適用。

持續(xù)部署實例

下面我們通過一個具體的實例來展示如何完成持續(xù)部署。我們用Jenkins來做為持續(xù)部署工具,用它部署一個Go程序到k8s環(huán)境。
我們的流程基本是上面講的狹義流程,但由于沒有Nexus,我們稍微變通了一下,改由從源碼庫直接下載源程序,步驟如下:
  1. 下載源碼:從github下載源代碼到Jenkins的運行環(huán)境;

  2. 測試:這一步暫時沒有實際內容;

  3. 生成鏡像:創(chuàng)建鏡像,并上傳到Docker hub;

  4. 部署鏡像:將生成的鏡像部署到k8s。

在創(chuàng)建Jenkins項目之前,先要做些準備工作:
建立Docker Hub賬戶
需要在Docker Hub上創(chuàng)建賬戶和鏡像庫,這樣才能上傳鏡像。具體過程這里就不詳細講解了,請查閱相關資料。
在Jenkins上創(chuàng)建憑證(Credentials)
需要設置訪問Docker hub的用戶和口令,以后在Jenkins腳本里可以通過變量的方式進行引用,這樣口令就不會以明碼的方式出現(xiàn)在程序里。
用管理員賬戶登錄 Jenkins主頁面后,找到 Manage Jenkins-》Credentials-》System -》Global Credentials -》Add Credentials,如下圖所示輸入你的Docker Hub的用戶名和口令?!癐D”是后面你要在腳本里引用的。
創(chuàng)建預裝Docker和k8s的Jenkins鏡像
Jenkins的默認容器里面沒有Docker和k8s,因此我們需要在Jenkins鏡像的基礎上重新創(chuàng)建新的鏡像,后面還會詳細講解。
下面是鏡像文件(Dockerfile-modified-jenkins)
FROM jenkins/jenkins:lts

USER root

ENV DOCKERVERSION=19.03.4

RUN curl -fsSLO https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKERVERSION}.tgz \
  && tar xzvf docker-${DOCKERVERSION}.tgz --strip 1 \
                 -C /usr/local/bin docker/docker \
  && rm docker-${DOCKERVERSION}.tgz

RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl \
    && chmod  x ./kubectl \
    && mv ./kubectl /usr/local/bin/kubectl

上面的鏡像在“jenkins/jenkins:lts”的基礎上又安裝了Docker和kubectl,這樣就支持這兩個軟件了。鏡像里使用的是docker的19.03.4版本。這里裝的只是“Docker CLI”,沒有Docker引擎。用的時候還是要把虛擬機的卷掛載到容器上,使用虛機的Docker引擎。因此最好保證容器里的Docker版本和虛機的Docker版本一致。
使用如下命令查看Docker版本:
vagrant@ubuntu-xenial:/$ docker version

詳細情況請參見Configure a CI/CD pipeline with Jenkins on Kubernetes
準備工作已經(jīng)完成,現(xiàn)在要正式創(chuàng)建Jenkins項目:
Jenkins腳本:
項目的創(chuàng)建是在Jenkins的主頁上來完成,它的名字是“jenkins-k8sdemo”,它的最主要部分是腳本代碼,它也跟Go程序存放在相同的源碼庫中,文件的名字也是“jenkins-k8sdemo”。項目的腳本頁面如下圖所示。

如果你不熟悉安裝和創(chuàng)建Jenkins項目,請參閱在k8s上安裝Jenkins及常見問題

下面就是jenkins-k8sdemo腳本文件:
def POD_LABEL = 'k8sdemopod-${UUID.randomUUID().toString()}'
podTemplate(label: POD_LABEL, cloud: 'kubernetes', containers: [
    containerTemplate(name: 'modified-jenkins', image: 'jfeng45/modified-jenkins:1.0', ttyEnabled: truecommand'cat')
  ],
  volumes: [
     hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock')
  ]) {

    node(POD_LABEL) {
       def kubBackendDirectory = '/script/kubernetes/backend'
       stage('Checkout') {
            container('modified-jenkins') {
                sh 'echo get source from github'
                git 'https://github.com/jfeng45/k8sdemo'
            }
          }
       stage('Build image') {
            def imageName = 'jfeng45/jenkins-k8sdemo:${env.BUILD_NUMBER}'
            def dockerDirectory = '${kubBackendDirectory}/docker/Dockerfile-k8sdemo-backend'
             container('modified-jenkins') {
               withCredentials([[$class'UsernamePasswordMultiBinding',
                 credentialsId: 'dockerhub',
                 usernameVariable: 'DOCKER_HUB_USER',
                 passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
                 sh '''
                   docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
                   docker build -f ${WORKSPACE}${dockerDirectory} -t ${imageName} .
                   docker push ${imageName}
                   '''

               }
             }
           }
       stage('Deploy') {
           container('modified-jenkins') {
               sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml'
               sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-service.yaml'
             }
       }
    }
}

我們逐段看一下代碼:
設定容器鏡像:
podTemplate(label: POD_LABEL, cloud: 'kubernetes', containers: [
    containerTemplate(name: 'modified-jenkins', image: 'jfeng45/modified-jenkins:1.0', ttyEnabled: truecommand'cat')
  ],
  volumes: [
     hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock')
  ])

這里設定Jenkins子節(jié)點Pod的容器鏡像,用的是“jfeng45/modified-jenkins:1.0”,也就是我們在上個步驟創(chuàng)建的。所有的腳本里的步驟(stage)都用的是這個鏡像。“volumes:”用來掛載卷到Jenkins容器中,這樣Jenkins子節(jié)點就可以使用虛機的Docker引擎。
關于Jenkins腳本命令和設置掛載卷請參閱jenkinsci/kubernetes-plugin
創(chuàng)建鏡像:
下面的代碼生成Go程序的Docker鏡像文件,這里我們沒有用Docker插件,而是直接調用Docker命令,它的好處后面會講到。它引用了我們前面設置的“Docker hub”的憑證去訪問Docker庫。在腳本里,我們先登錄到“Docker hub”,然后使用上一步從GitHub下載的源代碼來創(chuàng)建鏡像,最后上傳鏡像到“Docker hub”。其中“WORKSPACE”是Jenkins預定義變量,從GitHub下載的源代碼就存放在“ {WORKSPACE}”是Jenkins預定義變量,從GitHub下載的源代碼就存放在“WORKSPACE”是Jenkins預定義變量,從GitHub下載的源代碼就存放在“{WORKSPACE}”里。
stage('Build image') {

            def imageName = 'jfeng45/jenkins-k8sdemo:${env.BUILD_NUMBER}'
            def dockerDirectory = '${kubBackendDirectory}/docker/Dockerfile-k8sdemo-backend'
             container('modified-jenkins') {
               withCredentials([[$class'UsernamePasswordMultiBinding',
                 credentialsId: 'dockerhub',
                 usernameVariable: 'DOCKER_HUB_USER',
                 passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
                 sh '''
                   docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
                   docker build -f ${WORKSPACE}${dockerDirectory} -t ${imageName} .
                   docker push ${imageName}
                   '''

               }
             }
           }

如果你想了解Jenkins命令詳情,請參閱Set Up a Jenkins CI/CD Pipeline with Kubernetes
我們這里并沒有重新生成Go程序的鏡像文件,而是復用了以前就有的k8s創(chuàng)建Go程序的鏡像文件,Go程序的鏡像文件路徑是“\script\kubernetes\backend\docker\Dockerfile-k8sdemo-backend”。
它的代碼如下。后面還會講到這樣做的好處。
# vagrant@ubuntu-xenial:~/app/k8sdemo/script/kubernetes/backend$
# docker build -t k8sdemo-backend .

FROM golang:latest as builder

# Set the Current Working Directory inside the container
WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY . .

WORKDIR /app/cmd

# Build the Go app
#RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main.exe

RUN go build -o main.exe

######## Start a new stage from scratch #######
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

# Copy the Pre-built binary file from the previous stage
COPY --from=builder /app/cmd/main.exe .

# Command to run the executable
# CMD exec /bin/bash -c 'trap : TERM INT; sleep infinity & wait'
CMD 

關于Go鏡像文件詳情,請參閱創(chuàng)建優(yōu)化的Go鏡像文件以及踩過的坑

部署鏡像:
下面部署Go程序到k8s上,這里也沒有用kubectl插件,而是直接用kubectl命令調用已經(jīng)存在的k8s的部署和服務配置文件(文件里會引用生成的Go鏡像),它的好處后面也會講到。
 stage('Deploy') {
           container('modified-jenkins') {
               sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml'
               sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-service.yaml'
             }
       }

關于k8s的部署和服務配置文件詳情,請參閱把應用程序遷移到k8s需要修改什么?

為什么沒用Declarative?
用腳本來寫Pipeline有兩種方法,“Scripted Pipleline”和“Declarative Pipleline”,這里用的是第一種方法?!癉eclarative Pipleline”是新的方法,之所以沒用它,是因為開始用的是Declarative模式但沒調出來,然后就改用“Scripted Pipleline”,結果成功了。后來才發(fā)現(xiàn)設置Declarative的方法,特別是如何掛載卷,但看了一下,比起“Scripted Pipleline”要復雜不少,就偷了一下懶,沒有再改。
如果你想知道怎樣在Declarative模式下設置掛載卷,請參閱Jenkins Pipeline Kubernetes Agent shared Volumes
自動執(zhí)行項目:
現(xiàn)在的Jenkins中的項目需要手動啟動,如果你需要自動啟動項目的話就要創(chuàng)建webhook,GitHub和dockerhub都支持webhook,在它們的頁面上都有設置選項。“webhook”是一個反向調用的URL,每當有新的代碼或鏡像提交時,GitHub和dockerhub都會調用這個URL,URL被設置成Jenkins的項目地址,這樣相關的項目就會自動啟動。
檢驗結果:
現(xiàn)在Jenkins的項目就完全配置好了,需要運行項目,檢驗結果。啟動項目后,
查看“Console Output”,下面是部分輸出(全部輸出太長,請看附錄),說明部署成功。
。。。
  kubectl apply -f /home/jenkins/workspace/test1/script/kubernetes/backend/backend-deployment.yaml
deployment.apps/k8sdemo-backend-deployment created
[Pipeline] sh  kubectl apply -f /home/jenkins/workspace/test1/script/kubernetes/backend/backend-service.yaml
service/k8sdemo-backend-service created
[Pipeline] }
[Pipeline// container
[Pipeline] }
[Pipeline// stage
[Pipeline] }
[Pipeline// node
[Pipeline] }
[Pipeline// podTemplate
[Pipeline] End of Pipeline
Finished: SUCCESS

 stage('Deploy'
{
           container('modified-jenkins') {
               sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml'
               sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-service.yaml'
             }
       }

查看運行結果:

獲得Pod名字:
vagrant@ubuntu-xenial:/home$ kubectl get pod
NAME                                           READY   STATUS    RESTARTS   AGE
envar-demo                                     1/1     Running   15         32d
k8sdemo-backend-deployment-6b99dc6b8c-8kxt9    1/1     Running   0          50s
k8sdemo-database-deployment-578fc88c88-mm6x8   1/1     Running   9          20d
k8sdemo-jenkins-deployment-675dd574cb-r57sb    1/1     Running   0          2d23h

登錄Pod并運行程序:

vagrant@ubuntu-xenial:/home$ kubectl exec -ti k8sdemo-backend-deployment-6b99dc6b8c-8kxt9 -- /bin/sh
# ./main.exe
DEBU[0000] connect to database
DEBU[0000dataSourceName:dbuser:dbuser@tcp(k8sdemo-database-service:3306)/service_config?charset=utf8
DEBU[0000] FindAll()
DEBU[0000] created=2019-10-21
DEBU[0000] find user:{1 Tony IT 2019-10-21}
DEBU[0000] find user list:[{1 Tony IT 2019-10-21}]
DEBU[0000] user lst:[{1 Tony IT 2019-10-21}]

結果正確。


Jenkins原理

實例部分已經(jīng)結束,下面來探討最佳實踐。在這之前,先要搞清楚Jenkins的原理。
可執(zhí)行命令
我一直有一個問題就是那些命令是Jenkins可以通過shell執(zhí)行的?Jenkins和Docker、k8s不同,后者有自己的一套命令,只要把它們學會了就行了。而Jenkins是通過與別的系統(tǒng)集成來工作的,因此它的可執(zhí)行命令與其他系統(tǒng)有關,這導致了你很難知道那些命令是可以執(zhí)行的,那些不行。你需要弄懂它的原理,才能得到答案。當Jenkins執(zhí)行腳本時,主節(jié)點會自動生成一個子節(jié)點(Docker容器),所有的Jenkins命令都是在這個容器里執(zhí)行的。所以能執(zhí)行的命令與容器密切相關。一般來講,你可以通過shell來運行Linux命令。那下面的問題就來了:
  1. 為什么我不能用Bash?

    因為你使用的子節(jié)點的容器可能使用的是精簡版的Linux,例如Alpine,它是沒有Bash的。
  2. 為什么我不能運行Docker命令或Kubectl?


因為它的默認容器是jenkinsci/jnlp-slave,而它里面沒有預裝Docker或        kubectl。你可以不使用默認容器,而是指定你自己的容器,并在其中預          裝上述軟件,那么就可以執(zhí)行這些命令了。
如何共享文件
一個Jenkins項目通常要分成幾個步驟(stage)來完成,例如你下載的源碼要在幾個步驟之間共享,那怎么共享呢?Jenkins為每個項目分配了一個WORKSPACE(磁盤空間), 里面存儲了所有從源碼庫和其他地方下載的文件,不同stage之間可以通過WORKSPACE來共享文件。
關于WORKSPACE詳情,請參閱Jenkins Project Artifacts and Workspace
最佳實踐
要總結最佳實踐就要理解持續(xù)部署在整個開發(fā)流程中的作用和位置,它主要起一個串接各個環(huán)節(jié)的作用。而程序的部署是由k8s和Docker來完成的,因此程序部署的腳本也都在k8s中,并由k8s來維護。我們不想在Jenkins里再維護一套類似的腳本,因此最好的辦法是把Jenkins的腳本壓縮到最小,盡可能多地直接調用k8s的腳本。
另外能寫代碼就不要在頁面上配置,只有代碼是可以重復執(zhí)行并保證穩(wěn)定結果的,頁面配置不能移植,而且不能保證每次配置都產(chǎn)生一樣的結果。
盡量少使用插件
Jenkins有許多插件,基本上你想要完成什么功能都有相應的插件。例如你需要使用Docker功能,就有“Docker Pipeline”插件,你要使用k8s功能就有“kubectl”插件。但它會帶來很多的問題。
第一,每個插件都有他自己的設置方式(一般要在Jenkins插件頁面進行設置),但這種設置是與其他持續(xù)部署工具不兼容的。如果以后你要遷移到其他持續(xù)部署工具,這些設置都需要廢棄;
第二,每個插件都有自己的命令格式,因此你需要另外學習一套新的命令;
第三,這些插件往往只支持部分功能,使你能做的事情受到了限制。
例如,你需要創(chuàng)建一個Docker鏡像文件,命令如下,它將創(chuàng)建一個名為'jfeng45/jenkins-k8sdemo'的鏡像,鏡像的默認文件是在項目的根目錄下的Dockerfile。
app = docker.build('jfeng45/jenkins-k8sdemo'
但創(chuàng)建Docker鏡像文件命令有許多參數(shù)選項,例如,你的鏡像文件名不是Dockerfile,并且目錄不是在項目根目錄下,應如何寫呢?這在以前的版本是不支持的,后來的版本支持了,但畢竟不太方便,還要學新的命令。最好的辦法是能直接使用Docker命令,這樣就完美的解決了上面說的三個問題。答案就在前面講的Jenkins原理里,其實絕大多數(shù)插件都是不需要的,你只要自己創(chuàng)建一個Jenkins子節(jié)點容器,并安裝相應的軟件就能圓滿解決。
下面是使用插件的腳本和不使用的對比,不使用的看起來更長,那時因為使用插件的腳本和Jenkins里的憑證設置有更好的集成,而不使用的腳本沒有。但除了這個小缺點,其他方面不使用的腳本都要遠遠優(yōu)于使用插件的。
使用插件的腳本(用插件命令):
stage('Create Docker images') {
  container('docker') {
      app = docker.build('jfeng45/codedemo''-f ${WORKSPACE}/script/kubernetes/backend/docker/Dockerfile-k8sdemo-test .')
      docker.withRegistry('''dockerhub') {
          // Push image and tag it with our build number for versioning purposes.
          app.push('${env.BUILD_NUMBER}')
      }
    }
  }
不使用插件的腳本(直接用Docker命令):
stage('Create a d ocker image') {
     def imageName = 'jfeng45/codedemo:${env.BUILD_NUMBER}'
     def dockerDirectory = '${kubBackendDirectory}/docker/Dockerfile-k8sdemo-backend'
      container('modified-jenkins') {
        withCredentials([[$class'UsernamePasswordMultiBinding',
          credentialsId: 'dockerhub',
          usernameVariable: 'DOCKER_HUB_USER',
          passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
          sh '''
            docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
            docker build -f ${WORKSPACE}${dockerDirectory} -t ${imageName} .
            docker push ${imageName}
            '''

        }
      }
    }

盡量多使用k8s和Dcoker
例如我們要創(chuàng)建一個應用程序的鏡像,我們可以寫一個Docker文件,并在Jenkins腳本里調用這個Docker文件來創(chuàng)建,也可以寫一個Jenkins腳本,在腳本里來創(chuàng)建鏡像。比較好的方法是前者。因為Docker和k8s都是事實上的標準,移植起來很方便。
Jenkins腳本的代碼越少越好
如果你認同前面兩個原則,那么這一條就是順理成章的,原因也和上面是一樣的。

常見問題

1.變量要放在雙引號里
Jenkins的腳本即可以使用單引號也可以使用雙引號,但如果你在引號里引用了變量,那么就要使用雙引號。
正確的命令:
sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml'
錯誤的命令:
sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml'
2.docker not found
如果Jenkins的容器里沒有Docker,但你又調用了Docker命令,那么“Console Output”里就會有如下錯誤:
  docker inspect -f . k8sdemo-backend:latest
/var/jenkins_home/workspace/k8sdec@2@tmp/durable-01e26997/script.sh: 1:     /var/jenkins_home/workspace/k8sdec@2@tmp/durable-01e26997/script.sh: docker:     not found
3.Jenkins宕機了
在調試Jenkins時,我新創(chuàng)建了一個鏡像文件并上傳到“Docker hub”之后就發(fā)現(xiàn)Jenkins宕機了。檢查了Pod,發(fā)現(xiàn)了問題,k8s找不到Jenkins的鏡像文件了(鏡像文件從磁盤上消失了)。因為Jenkins的部署文件的設置是“imagePullPolicy: Never”,所以一旦鏡像沒有了,它不會自動重新下載。后來找到了原因,Vagrant的默認磁盤大小是10G,如果空間不夠,它會自動從磁盤上刪除其他鏡像文件,騰出空間,結果就把Jenkins的鏡像文件給刪了,解決方案是擴充Vagrant的磁盤大小。
下面是修改之后的Vagrantfile,把磁盤空間改成了16G。
Vagrant.configure(2do |config|
     。。。
     config.vm.box = 'ubuntu/xenial64'
     config.disksize.size = '16GB'
     。。。
end
詳情請見How can I increase disk size on a Vagrant VM?

源碼

完整源碼的github鏈接
下面是項目中與本文有關的部分:

不堆砌術語,不羅列架構,不迷信權威,不盲從流行,堅持獨立思考
版權聲明:本文為CSDN博主「倚天碼農(nóng)」的原創(chuàng)文章。

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    五月天婷亚洲天婷综合网| 亚洲av首页免费在线观看| 国内自拍偷拍福利视频| 欧美日韩无卡一区二区| 中文人妻精品一区二区三区四区| 免费特黄欧美亚洲黄片| 大胆裸体写真一区二区| 91后入中出内射在线| 九九热在线视频观看最新| 九九热精品视频在线观看| 国产精品人妻熟女毛片av久| 国产成人精品在线一区二区三区| 国产91色综合久久高清| 亚洲精品偷拍一区二区三区| 欧美又大又黄刺激视频| 精品综合欧美一区二区三区| 激情爱爱一区二区三区| 男女午夜视频在线观看免费| 两性色午夜天堂免费视频| 91亚洲精品综合久久| 国产欧美性成人精品午夜| 99国产精品国产精品九九 | 欧美一区二区三区性视频| 日韩国产中文在线视频| 欧美成人国产精品高清| 好吊日在线观看免费视频| 免费性欧美重口味黄色| 日韩一区二区三区在线欧洲| 久久亚洲精品中文字幕| 国产精品偷拍视频一区| 日本三区不卡高清更新二区| 又色又爽又黄的三级视频| 欧美黑人精品一区二区在线| 麻豆看片麻豆免费视频| 国产欧美日韩在线精品一二区| 欧美日韩国产精品黄片| 欧美日韩国产亚洲三级理论片 | 亚洲一区二区精品国产av| 十八禁日本一区二区三区| 免费亚洲黄色在线观看| 欧美国产精品区一区二区三区|