一、前言

Java应用部署到K8S(Kubernetes),一般需要镜像制作、镜像存储与应用部署这三个步骤。

  • 镜像制作:根据Java源代码生成符合OCI(开放容器标准)的镜像。可以使用Docker完成镜像制作。
  • 镜像存储:将镜像上传到镜像仓库以便后续使用。这里的镜像仓库一般是指Harbor
  • 应用部署:根据镜像仓库中的镜像及其对应配置,将应用部署到K8S集群。K8S原生支持手动编写配置文件实现应用部署,Helm简化了应用部署的过程。

为了更好地理解云环境部署,可以类比传统的虚机部署。在云环境部署中:

  • 镜像制作就相当于:maven打包生成war包(实际上war包是镜像的一部分)。
  • 镜像存储就相当于:将war包上传到Maven仓库。
  • 应用部署就相当于:将Maven仓库中的war包安装到JBoss容器。

在此正式介绍上述三个过程之前,还需要准备一些环境。除了Java基础环境之外,还需要准备一个Docker环境与Kubernete集群(可以使用minikube)。

国内网络环境启动minikube时,可以使用:minikube start --image-mirror-country='cn' --kubernetes-version=v1.23.13指令,其中,cn表示使用国内镜像,v1.23.13指定K8S版本小于v1.24,具体原因参见Kubernetes 1.24 中的移除和弃用

二、Docker镜像制作

Docker制作的镜像符合OCI标准,可以直接在K8S中运行。本地制作的镜像一般需要上传到Harbor仓库

(一)准备一个Java项目

使用spring-web搭建一个简单的web项目,pom文件如下:

<project>
    ...
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    ...
</project>

启动类如下:

@SpringBootApplication
@RestController
public class Application {
    @RequestMapping("/index.html")
    public String home() throws InterruptedException {
        return "Hello World";
    }
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

(二)编写Dockerfile脚本

在项目根目录下新建文件Dockerfile,内容如下:

FROM maven:3.8.4-openjdk-8-slim AS base
WORKDIR /app
COPY pom.xml ./
RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline
COPY src ./src

FROM base AS build
RUN --mount=type=cache,target=/root/.m2 mvn package

FROM openjdk:8-jre-slim-buster AS production
EXPOSE 8080
COPY --from=build /app/target/*.jar /app/app.jar
CMD java -jar /app/app.jar

其中,--mount语法需要启用Docker的BuildKit功能,mvn dependency:go-offline表示缓存maven包以供后续重复使用。

(三)生成Docker镜像

在项目根目录进入终端,执行docker build命令:

docker build -t app:0.0.1 .

查看镜像是否成功生成:

docker images

如果有看到REPOSITORYappTAG0.0.1的记录,证明镜像制作完成。

三、Harbor镜像存储

Harbor是用来存储符合OCI标准镜像的一种仓库实现,Docker客户端、K8S集群都可以从Harbor仓库拉取指定镜像。在Helm应用部署中的镜像仓库配置中,同样可以使用Harbor仓库。

(一)安装Harbor

Harbor的具体安装过程可以跟着官方安装教程走,安装过程不复杂,简而言之就是:下载安装包、修改配置文件、执行安装脚本三步走。

这里有一点需要特别说明,当前Harbor支持HTTP与HTTPS两种访问方式,正常来说,本地测试使用HTTP就可以了,但Helm限制了Harbor的访问协议必须是HTTPS,因此还需要为HTTPS配置一些额外的证书。

在Harbor启用SSL后,客户端需要配置ca.crt证书,否则在拉取镜像时会报x509验证错误。这里的客户端可以是Docker,也可以是K8S,下面分别介绍这两种客户端的配置方法。

(1) Docker客户端配置SSL

将ca.crt放入指定目录,Linux系统位于/etc/docker/certs.d/,windows系统位于%HOMEPATH%/.docker/certs.d/,有关Docker客户端SSL验证详见官方文档

这里的ca.crt可以手动生成,也可以使用下面介绍的minikube自带的ca.crt。

Windows系统也可以可以双击ca.crt文件以导入证书。

(2) K8S客户端配置SSL(以minikube为例)

minikube的~/.minikube目录下已经有了ca.crt文件,直接使用这个文件即可,可以跳过Harbor配置HTTPS的官方文档中的Generate a Server Certificate这一步。

至此,根据上面的流程已经配置好了可以通过HTTPS访问的Harbor服务:https://harbor.local.com,这个域名是自定义的,可以在Harbor配置安装的时候手动指定。同时,需要在hosts文件中配置对应的dns映射。

(二)验证Harbor

验证Harbor连通性最简单的访问方式就是在浏览器中访问对应的Harbor服务,域名为https://harbor.local.com,默认用户名密码为adminHarbor12345

同时,也可以使用docker login验证Harbor连通性,在终端输入docker login harbor.local.com,接着输入对应的用户名密码,提示Login Succeeded,即证明Harbor正常连通。

(三)上传镜像到Harbor

上传之前,先要确保已经通过docker login完成了Harbor仓库的鉴权。

然后,执行docker push命令将之前制作的镜像推送到Harbor:

docker push app:0.0.1

执行完毕后,会发现报了以下错误:

The push refers to repository [docker.io/library/app]
An image does not exist locally with the tag: app

如果没有指定app的前缀,那么docker push命令会默认使用dockerhub的自带仓库,这与预期不符,因此需要手动给该镜像加上一个仓库路径前缀。如下所示:

docker tag app:0.0.1 harbor.local.com/luanrz/app:0.0.1

此命令会生成一条新的image记录,它的IMAGE ID与原来的镜像一致,现在,重新推送:

docker push harbor.local.com/luanrz/app:0.0.1

又会报一条错:

unauthorized: project luanrz not found: project luanrz not found

可以看到是luanrz这个项目没有创建,登录到Harbor页面,点击“新建项目”,创建对应的项目。

再次执行docker push进行推送,如果没有问题将显示推送成功的结果。至此,镜像成功上传到Harbor仓库。

四、Helm应用部署

Helm是K8s中的包管理器,类似于Linux中pacman、apt、yum等包管理器的职责。Helm简化了安装应用到K8S的过程。

(一)安装Helm

可以参照官方教程完成Helm的安装,这里不再赘述。安装完之后,在控制台输入helm version正常输出结果即证明安装成功。

(二)制作Chart

首先,创建一个Helm Chart:

helm create app

进入app后,修改values.yaml文件:

...
image:
  repository:  harbor.local.com/luanrz/app
  pullPolicy: Always
  tag: "latest"
...
service:
  type: ClusterIP
  port: 8080
...

其中,image.repository与image.tag唯一定位了一个Harbor仓库中的镜像地址,service.port则表明了对外暴露的服务端口。

(三)安装应用

在app目录下执行:

helm install app .

如果执行无误,将会打印出当前Release的状态,通过helm list可查看所有的Release。

正常情况下,helm会将values文件与templates文件组合成应用部署的相关指令,发给K8S集群,后续就可以使用kubelet控制集群中的应用了。

(四)验证应用

执行kubectl get系列命令查看deployments、pods与services的状态:

kubectl get deployments
kubectl get pods
kubectl get services

正常情况下上述资源都会有一条对应的app记录,如果pod状态不是Running,可以通过kubectl describe查看事件(Events)排查原因:

kubectl describe pods <POD_NAME>

比如,在Harbor镜像拉取失败时,就可能出现类似于以下的各种原因:

  • Harbor只配置了HTTP访问模式,此时会报错提示不支持http访问方式或重定向至https失败(解决方案:将Harbor的访问模式配置为HTTPS)
  • Harbor配置了HTTPS访问模式,此时可能也会报错提示x509验证失败(解决方案:详见安装Harbor中的SSL相关说明
  • 其它等等

排查原因解决问题后,如果一切无误,应用也就部署成功了。但外面可能还没办法访问,可以通过ingress或者port-forward进一步暴露服务端口,下面以port-forward为例:

kubectl port-forward services/<pod-name> 8080:8080

现在,可以在外面访问容器应用了:

curl http://localhost:8080/index.html

控制台正常输入Hello World,应用部署到此结束。

五、总结

Java应用部署到K8S按照顺序执行以下步骤:

  • 镜像制作:使用docker build指令生成Docker镜像。
  • 镜像存储:使用docker login指令和docker push指令登录到并推送Harbor仓库,需要针对针对Docker客户端和K8S集群调整Harbor的SSL配置。
  • 应用部署:使用helm create创建一个Chart,使用helm install安装一个Release,使用Kubelet系列指令管理部署完成的应用。

至此,一个Java应用,从源代码,到镜像制作、镜像存储,最后到应用部署,完成了它上云全部步骤,后面的维护和管理,将是更大的挑战。