1. 程式人生 > >如何建立一個安全的Docker基礎映象

如何建立一個安全的Docker基礎映象

這裡寫圖片描述

背景

我最初使用Docker的時候,每個人都在說它用起來有多簡單方便,它內部的機制是有多麼好,它為我們節省了多少時間。但是當我一使用它就發現,幾乎所有映象都是臃腫而且不安全的(沒有使用包簽名,盲目相信上游的映象庫以curl | sh的方式安裝),而且也沒有一個映象能實現Docker的初衷:隔離,單程序,容易分發,簡潔。

Docker映象本來不是為了取代複雜的虛擬機器而設計的,後者有完整的日誌、監控、警報和資源管理模組。而Docker則傾向於利用核心的cgroupsnamespaces特性進行封裝組合,這就好像:

在物理機器環境下,一旦核心完成了初始化,init程序就起來了。

這也是為什麼當你在Dockerfile的CMD指令啟動的程序PID是1,這是與Unix中的程序機制類似的。

現在請檢視一下你的程序列表,使用top或者ps,你會看到init程序佔用的也是這個PID,這是每個類Unix系統的核心程序,所有程序的父程序,一旦你理解這個概念:在類Unix系統上每個程序都是init程序的子程序,你會理解Docker容器裡不應該有無關的修飾檔案,它應該是剛好滿足程序執行需要。

如何開始

現在的應用多數是大型複雜的系統,通常都需要很多依賴庫,例如有排程,編譯和很多其他相關工具類應用,它們的架構通常封裝性良好,通過一層層的抽象和介面把底層細節隱藏了,從某種程度上說,這也算是一種容器,但是從系統架構視角看,我們需要一種比以往虛擬環境更簡單的方案了。

以Java為例

從零開始,思考你要構建一個最通用的基礎容器,想想你的應用本身,它執行需要什麼?

可能性有很多,如果你要執行Java應用,它需要Java執行時;如果執行Rails應用,它需要Ruby直譯器,對Python應用也一樣。Go和其他一些編譯型語言有些許不同,我以下會提到。

在Java例子中,下一步要想的是:JRE需要什麼依賴才能執行?因為它是讓應用能執行的最重要的元件,所以很自然的下一步就是要想清楚JRE執行依賴於什麼。

而實際上JRE並沒太多依賴,它本來就是作為作業系統的抽象層,使程式碼不依賴於宿主系統執行,因此安裝好JRE就基本準備就緒了。

(實際上,對作業系統的獨立性並不是理所當然的事,有非常多的系統特有API和專有的系統擴充套件,但是便於舉例,我們把注意力放在簡單的情況下)

在Linux上,JVM主要是呼叫系統的C語言庫,Oracle的官方JRE,使用的是libc,也就是glibc,這意味著你要執行任何Java程式,都需要先裝好glibc。另外你可能需要某種shell來管理環境,還有一個與外部通訊的介面,例如網路和資源的介面。

我們總結一下Java應用示例需要的最低配置是:

  • JRE,在例子中我們使用Oracle JRE
  • glibc,JRE的依賴
  • 一個基礎環境(包含網路、記憶體、檔案系統等資源管理工具)

走進Alpine Linux

Alpine Linux最近得到很多關注,主要是因為它打包了一系列的經過驗籤的可信任的依賴,並且還保持體積在2MB!而在本文釋出時,其他的一些映象分發版如下:

  • ubuntu:latest: 66MB (已經瘦身了非常多了,以前有些版本超過600MB)
  • debian:latest: 55MB (同上,一開始是200MB以上的)
  • arch:latest: 145MB
  • busybox:latest: 676KB (是的!KB,我稍後會討論它)
  • alpine:latest: 2MB (2MB,包含一個包管理工具的Linux系統)

Busybox是最小的競爭者?

從上邊的對比中你可以看到,在體積上唯一能打敗Alpine Linux的是Busybox,所以現在幾乎所有嵌入式系統都是使用它,它被應用在路由器,交換機,ATM,或者你的吐司機上。它作為一個最最基礎的環境,但是又提供了足夠容易維護的shell介面。

在網上有很多文章解釋了為什麼人們會選擇Alpine Linux而不是Busybox,我在這總結一下:

  • 開放活躍的軟體包倉庫:Alpine Linux使用apk包管理工具,它整合在Docker映象中,而Busybox你需要另外安裝一個包管理器,例如opkg,更甚者,你需要尋找一個穩定的包倉庫源(這幾乎沒有),Alpine的包倉庫中提供了大量常用的依賴包,例如,如果你仍然需要在容器中編譯NodeJS或Ruby之類的程式碼,你可以直接執行apk來新增nodejs和ruby,這在幾秒內便可以完成。
  • 體積確實重要,但是當你在功能性,靈活性,易用性和1.5MB之間衡量,體積就不那麼重要了,Alpine上新增的包使這些方面都大大增強了。
  • 廣泛的支援:Docker公司已經聘請了Alpine Linux的作者來維護它,所有官方映象,在以後都將基於Alpine Linux來構建。沒有比這個更有說服力的理由去讓你在自己的容器中使用它了吧。
  • 希雲cSphere很早就意識到映象越來越龐大的問題,因此在去年推出微映象,也是引導大家如何更好地構建和理解映象,映象只是一種軟體包格式而已。

構建一個Java環境基映象

正如我剛解釋的,Alpine Linux是一個構建自有映象時不錯的選擇,因此,我們在此將使用它來構建簡潔高效的Docker映象,我們開始吧!

組合:Alpine + bash

每個Dockerfile第一個指令都是指定它的父級容器,通常是用於繼承,在我們的例子中是alpine:latest:

FROM alpine:latest
MAINTAINER cSphere <docker@csphere.cn>

我們同時聲明瞭誰為這個映象負責,這個資訊對上傳到Docker Hub的映象是必要的。

就這樣,你就有了往下操作的基礎,接下來安裝我們選好的shell,把下邊的命令加上:

RUN apk add --no-cache --update-cache bash
CMD ["/bin/bash"]

最終的Dockerfile是這樣:

FROM alpine:latest
MAINTAINER cSphere <[email protected].cn>

RUN apk add --no-cache --update-cache bash
CMD ["/bin/bash"]

好了,現在我們構建容器:

$ docker build -t my-java-base-image .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM alpine:latest
 ---> 2314ad3eeb90
Step 2 : MAINTAINER cSphere <[email protected]>
 ---> Running in 63433312d77e
 ---> bfe94713797a
Removing intermediate container 63433312d77e
... 省略若干行
Step 4 : CMD /bin/bash
 ---> Running in d2291684b797
 ---> ecc443d68f27
Removing intermediate container d2291684b797
Successfully built ecc443d68f27

並且執行它:

$ docker run --rm -ti my-java-base-image
bash-4.3#

成功了!我們有了一個執行著bash的Alpine Linux。

glibc and friends

前邊提到,Oracle的JRE依賴於glibc,Alpine Linux上並沒有glibc,它使用一個更小體積的替代版,叫musl libc。glibc發展了這麼多年,幾乎包含了所有C語言中需要的依賴包,顯然這樣會很不靈活,一個glibc庫被編譯進Alpine Linux,勉強能維持在5MB的體積,而它的替代者musl-libc是一個二進位制檔案,只有897KB,並且支援了所有Linux架構上的C依賴。

對Oracle的JRE,沒有辦法不把glibc加上,幸運的是,Andy Shinn已經做過了這些,他提供了一個預編譯的glibc映象給Alpine Linux,在Github上的alpine-pkg-glibc,最新版是2.23-r1。

這樣把這相關依賴加到Dockerfile中:

ENV GLIBC_PKG_VERSION=2.23-r1

RUN apk add --no-cache --update-cache curl ca-certificates bash && \
  curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && \
  curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && \
  curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && \
  curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && \
  apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk && \

現在我們的Dockerfile看起來是這樣:

FROM alpine:latest
MAINTAINER cSphere <docker@csphere.cn>

ENV GLIBC_PKG_VERSION=2.23-r1

RUN apk add --no-cache --update-cache curl ca-certificates bash && \
  curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && \
  curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && \
  curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && \
  curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && \
  apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk

CMD ["/bin/bash"]

我們一句句解釋一下這些指令:

ENV GLIBC_PKG_VERSION=2.23-r1

我們通過變數指定GitHub上的glibc版本,所以每當一個新版本釋出,都不需要更改URL,而直接更改這個變數即可。

RUN apk add --update-cache curl ca-certificates bash && \

這個指令會使用apk命令安裝我們需要的包,包括curl和ca-certificates(以便使用TLS的頁面),最後的bash是我們Dockerfile上個版本已經有的了。

  curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && \
  curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && \
  curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && \
  curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && \

這些命令會接著剛剛的RUN指令,它們會從GitHub下載相關公鑰和依賴包。

  apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk

所有包下載完成後,我們會用這一行命令安裝全部,由於我們之前添加了公鑰,所以它們的簽名會被驗證。

好了!我們現在有了一個能執行幾乎全部依賴於glibc包的環境。

Java執行環境

一般來說,Oracle不提供軟體倉庫的形式讓人們下載,但是人們總是會找到一些方法繞過它,你可以使用以下命令把JRE新增到Docker映象中:


ENV JAVA_VERSION_MAJOR=8 \
    JAVA_VERSION_MINOR=73 \
    JAVA_VERSION_BUILD=02 \
    JAVA_PACKAGE=server-jre

WORKDIR /tmp

RUN curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" \
  "http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && \
  apk del curl ca-certificates && \
  mv jdk1.${JAVA_VERSION_MAJOR}.0_${JAVA_VERSION_MINOR}/jre /jre && \
  rm /jre/bin/jjs && \
  rm /jre/bin/keytool && \
  rm /jre/bin/orbd && \
  rm /jre/bin/pack200 && \
  rm /jre/bin/policytool && \
  rm /jre/bin/rmid && \
  rm /jre/bin/rmiregistry && \
  rm /jre/bin/servertool && \
  rm /jre/bin/tnameserv && \
  rm /jre/bin/unpack200 && \
  rm /jre/lib/ext/nashorn.jar && \
  rm /jre/lib/jfr.jar && \
  rm -rf /jre/lib/jfr && \
  rm -rf /jre/lib/oblique-fonts && \
  rm -rf /tmp/* /var/cache/apk/* && \
  echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf

ENV JAVA_HOME /jre
ENV PATH ${PATH}:${JAVA_HOME}/bin

這堆命令究竟做了什麼,我們還是一句句來看一下吧:

ENV JAVA_VERSION_MAJOR=8 \
    JAVA_VERSION_MINOR=73 \
    JAVA_VERSION_BUILD=02 
    JAVA_PACKAGE=server-jre

WORKDIR /tmp

這句非常簡單,它定義了我們要從Oracle伺服器上要下載的軟體版本,本文編寫時,上邊的版本號是最新的,以後可能會變化,你可以從Oracle官網上檢視。它同時也指定了WORKDIR工作目錄,我們需要從一個臨時目錄開始執行,所以這裡設定了/tmp。

RUN curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" \
  "http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && \

這句稍微有點複雜,它使用curl傳了一個指定的頭資訊(“Cookie: oraclelicense=accept-securebackup-cookie”),以從Oracle上獲取真正的下載包,這是必須的,不然會返回一個錯誤頁。然後它會把下載好的包通過管道傳給gunzip和tar ,換言之,它並不會儲存下載回來的tar包,而是直接解壓出來到磁碟上。

apk del curl ca-certificates && \

這時curl和ca-certificates兩個包都完成了它們的使命,可以刪除了它們以節省空間。

  rm /jre/bin/jjs && \
  rm /jre/bin/keytool && \
  rm /jre/bin/orbd && \
  rm /jre/bin/pack200 && \
  rm /jre/bin/policytool && \
  rm /jre/bin/rmid && \
  rm /jre/bin/rmiregistry && \
  rm /jre/bin/servertool && \
  rm /jre/bin/tnameserv && \
  rm /jre/bin/unpack200 && \
  rm /jre/lib/ext/nashorn.jar && \
  rm /jre/lib/jfr.jar && \
  rm -rf /jre/lib/jfr && \
  rm -rf /jre/lib/oblique-fonts && \
  rm -rf /tmp/* /var/cache/apk/* && \

JRE自帶了一些工具包,可能永遠都不會用到的,我們也將它們刪掉。 最後一行,會把全部臨時檔案和apk的包快取也清理了。

echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf

這一行中,我們修改了nsswitch.conf,以確保網路正常,這會被glibc等包所用到。

最後,我們的Dockerfile會是下邊這樣:

FROM alpine:latest
MAINTAINER cSphere <docker@csphere.cn>

ENV JAVA_VERSION_MAJOR=8 \
    JAVA_VERSION_MINOR=73 \
    JAVA_VERSION_BUILD=02 \
    JAVA_PACKAGE=server-jre \
    GLIBC_PKG_VERSION=2.23-r1 \
    LANG=en_US.UTF8

WORKDIR /tmp

RUN apk add --no-cache --update-cache curl ca-certificates bash && \
  curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && \
  curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && \
  curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && \
  curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && \
  apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk && \
  curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" \
  "http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && \
  apk del curl ca-certificates && \
  mv jdk1.${JAVA_VERSION_MAJOR}.0_${JAVA_VERSION_MINOR}/jre /jre && \
  rm /jre/bin/jjs && \
  rm /jre/bin/keytool && \
  rm /jre/bin/orbd && \
  rm /jre/bin/pack200 && \
  rm /jre/bin/policytool && \
  rm /jre/bin/rmid && \
  rm /jre/bin/rmiregistry && \
  rm /jre/bin/servertool && \
  rm /jre/bin/tnameserv && \
  rm /jre/bin/unpack200 && \
  rm /jre/lib/ext/nashorn.jar && \
  rm /jre/lib/jfr.jar && \
  rm -rf /jre/lib/jfr && \
  rm -rf /jre/lib/oblique-fonts && \
  rm -rf /tmp/* /var/cache/apk/* && \
  echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf

ENV JAVA_HOME=/jre
ENV PATH=${PATH}:${JAVA_HOME}/bin

注意這裡,我整合了兩個ENV和RUN指令,因為最好是用更少的中間層,特別是這個容器是作為通用的構建單元。

簡單來說,有一個規則:你需要更大的靈活性,那你需要更多的層;如果你需要減小體積和降低複雜度,你需要更少的層。這完全取決於你的需求。

在頂部我還加上了這句:

ENV LANG=en_US.UTF-8

這句是為了確保執行在這個系統環境的應用能指定語言。你可以根據需要設定這個LANG環境變數。

另外,JAVA_HOME和PATH也要設定好,以使用剛剛裝好的JRE。

CMD指令會怎麼執行?

我之前提到,我們這是在構建一個能提供給其他服務作為基礎的映象,它不需要帶上CMD指令,因為它永遠不會執行,但是一旦一個服務關聯上它,就需要用到了。

不過你還是可以通過其他方式啟動這個容器,例如docker rundocker exec指令:

$ docker run --rm -ti my-java-base-image /bin/bash

構建最終映象

最後,我們終於到了構建映象這步了:

$ docker build -t my-java-base-image .
Sending build context to Docker daemon 60.42 kB
Step 1 : FROM alpine:latest
 ---> 2314ad3eeb90
Step 2 : MAINTAINER cSphere <[email protected]>
 ---> Using cache
 ---> 93cc2bc0bd60
Step 3 : ENV JAVA_VERSION_MAJOR 8 JAVA_VERSION_MINOR 73 JAVA_VERSION_BUILD 02 JAVA_PACKAGE server-jre GLIBC_PKG_VERSION 2.23-r1 LANG en_US.UTF8
 ---> Running in 3f0ffeaeca78
 ---> 1dcfd34b0f1a
Removing intermediate container 3f0ffeaeca78
... 省略若干行
Removing intermediate container 0a98b36a6e37
Step 7 : ENV PATH ${PATH}:${JAVA_HOME}/bin
 ---> Running in 54d0dfb04f98
 ---> 493399ac9ca6
Removing intermediate container 54d0dfb04f98
Successfully built 493399ac9ca6

哈哈!它執行成功了。我們執行容器裡的java來驗證一下吧:

$ docker run --rm -ti my-java-base-image java -version
java version "1.8.0_73"
Java(TM) SE Runtime Environment (build 1.8.0_73-b02)
Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode)

太好了,這正是我們要看到的結果,我們已經有了一個獨立的Oracle JRE環境,以後我們只需要基於這個映象來構建應用映象即可:

FROM my-java-base-image

[...]

最終映象有多大?

我們來看看:

$ docker images | grep my-java-base-image | awk '{print $7,$8}'
130.4 MB

說實話,這還是挺大的,但是畢竟裡邊裝的是Java嘛~

總結

我們現在構建了一個安全、輕量的Docker映象,基本上可以執行任何Java應用在上面,當然你也可以根據實際情況調整這個Dockerfile,但是主要的思想還是像上邊說的那樣,減小體積,使用安全的軟體源。

一旦你明白Docker容器只是一個基礎的單程序容器,只是一個應用執行的環境,它能讓你專注於應用的構建而不是其他雜七雜八的依賴關係,你就會把Docker應用到得心應手。

以下是簡單的幾點指引:

  • 在每個容器中執行一個程序,如果你需要多個程序,那就構建多個容器,並且使用如docker-compose之類的工具去組合這些元件。
  • 從一個非常小的映象開始構建。你不需要整個Debian或者Ubuntu映象,特別是當你使用的是編譯型語言(例如 C / C++ / Golang)。幾乎所有的應用加上Alpine Linux就足夠了。
  • 高效地使用層:新增更多的檔案層會便於打標籤和除錯,但是這樣會使映象體積膨脹。你需要平衡這兩點。
  • 安全性是非常重要的,確保從安全的倉庫拉取映象,從安全的安裝源安裝相關軟體包。(Alpine Linux映象從Docker官方或希雲微映象拉取,JRE從Oracle官方下載)
docker login index.csphere.cn # 賬號在 https://csphere.cn/hub 上獲取
docker pull index.cspehre.cn/microimages/alpine:3.3
docker pull index.csphere.cn/microimages/alpine-glibc:3.3

關於希雲cSphere

希雲cSphere是一個高度整合、功能強大的Docker私有云平臺和類PaaS解決方案,其架構設計借鑑了VMWare vSphere的思想。系統健壯性比肩VMWare這樣的商業產品,產品經過一年多十多個版本的迭代更新,在內部更是經歷了1000次以上的破壞性測試,目前已經在金融、製造、遊戲、安全、電商、教育等多個領域落地。

cSphere的亮點:

  • 平臺適應應用,不需要應用適應平臺。
  • 多應用架構多場景支援,希雲承諾不論是5年前的應用、現在的甚至5年後的應用架構都可以在希雲上完美支援
  • 搬遷現有業務,程式碼、架構無需任何修改
  • 希雲產品以自研發為主,拋棄了“拼湊”模式,有力保證了工程質量
  • 真正企業級的PaaS,可滿足高複雜專案需求

歡迎聯絡我們: