Docker-java实现应用和组件的远程发布

Posted 鮀城小帅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Docker-java实现应用和组件的远程发布相关的知识,希望对你有一定的参考价值。

    • 需求背景

目前项目上有个功能,我们会根据各种软件编写最佳实践,并将最佳实践编写为shell脚本。对于编写的shell脚本进行测试,如果是在虚拟机或服务器,测试完成后可能会有残留文件或配置,考虑到各种因素,最好是在一个开箱即用,用完就丢的环境进行测试,这时候,使用Docker就是一个非常不错的方案。

    • 需要的技术支持

  1. 目前考虑使用的技术是 Docker-java 来编写相关的API

  1. 基于Docker-java需要实现:

  1. 实现远程连接、安全连接

  1. 创建容器、加载镜像、拉取镜像、删除镜像、移除容器、启动容器、停止容器

  1. 执行命令、实现docker 执行脚本文件,并携带参数

  1. 实现路径挂载

  1. 上传shell脚本与软件安装包到镜像环境

  1. 通过目录映射和挂载,将需要测试脚本和安装包上传到本地的目录

  1. 在远程docker镜像执行脚本进行安装软件,这里的脚本来自挂载和映射的文件路径下

    • Docker-java

3.1 创建连接

3.1.1修改 docker 信息,实现远程访问

vi /lib/systemd/system/docker.service
找到ExecStart 开头的配置,注释原配置 进行备份
插入以下内容
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock
保存退出
systemctl daemon-reload
systemctl restart docker

注意:如果是云服务或者防火墙打开的话,记得开放端口

浏览器输入:http://IP:2375/version 如果响应正常,则配置生效

3.1.2导入MAVEN依赖

        <dependency>
            <groupId>com.github.docker-java</groupId>
            <artifactId>docker-java-transport-httpclient5</artifactId>
            <version>3.2.11</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

3.1.3 创建普通连接

package com.docker.java.config;

import com.alibaba.fastjson.JSONObject;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.model.Info;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import java.time.Duration;

/**
 * @author wushaopei
 * @create 2023-01-16 15:05
 */
@Configuration
public class DockerConfig 

    /**
     * 连接docker服务器
     * @return
     */
    @Bean("dockerClient")
    public DockerClient connectDocker()
        DefaultDockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerTlsVerify(false)
                // 这里填最上面填的ip端口号,ip换成服务器ip
                .withDockerHost("tcp://192.168.126.138:2375")
                // 这里也可以用另一种配置的
                // .withDockerHost("unix://var/run/docker.sock")
                // docker API版本号,可以用docker version查看
                .withApiVersion("1.41")
                // 默认
                .withRegistryUrl("https://index.docker.io/v1/").build();

        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
                .dockerHost(config.getDockerHost())
                .sslConfig(config.getSSLConfig())
                .maxConnections(100)
                .connectionTimeout(Duration.ofSeconds(30))
                .responseTimeout(Duration.ofSeconds(45))
                .build();

        DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient);

        Info info = dockerClient.infoCmd().exec();
        String infoStr = JSONObject.toJSONString(info);
        System.out.println("docker的环境信息如下:=================");
        System.out.println(infoStr);
        return dockerClient;
    

如果控制台打印了信息,则代表你已经通过java 连接上了docker

配置说明:

  • DOCKER_HOST Docker的地址,比如: tcp://localhost:2376 或者unix:///var/run/docker.sock

  • DOCKER_TLS_VERIFY 是否开启 TLS 验证 (http 和 https 之间切换)

  • DOCKER_CERT_PATH TLS 验证的证书路径

  • DOCKER_CONFIG 其他docker配置文件的路径 (比如 .dockercfg)

  • api.version API version版本

  • registry.url 下载源地址(docker镜像存放的地址)

  • registry.username 登陆用户名 (推送镜像到docker云仓库时需要)

  • registry.password 登陆用户密码(推送镜像到docker云仓库时需要)

  • registry.email 登陆账户的邮箱(推送镜像到docker云仓库时需要)

3.1.4创建安全连接

(1)修改docker.service

vi /lib/systemd/system/docker.service
找到ExecStart 开头的配置,注释原配置 进行备份
插入以下内容
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock -D --tlsverify --tlscert=/etc/docker/certs.d/server-cert-docker.pem --tlskey=/etc/docker/certs.d/server-key-docker.pem --tlscacert=/etc/docker/certs.d/ca-docker.pem
保存退出
systemctl daemon-reload
service docker restart

编写生成证书的脚本,这是在网上找的,可以自动生成脚本并且完成拷贝 (2)vim auto_gen_docker.sh

#!/bin/bash
#
# -------------------------------------------------------------
# 自动创建 Docker TLS 证书
# -------------------------------------------------------------
# 以下是配置信息
# --[BEGIN]------------------------------
CODE="docker"
IP="192.168.1.1"
PASSWORD="123456"
COUNTRY="CN"
STATE="HUNAN"
CITY="CHANGSHA"
ORGANIZATION="thyc"
ORGANIZATIONAL_UNIT="Dev"
COMMON_NAME="$IP"
EMAIL="an23gn@163.com"
# --[END]--
# Generate CA key
openssl genrsa -aes256 -passout "pass:$PASSWORD" -out "ca-key-$CODE.pem" 4096
# Generate CA
openssl req -new -x509 -days 365 -key "ca-key-$CODE.pem" -sha256 -out "ca-$CODE.pem" -passin "pass:$PASSWORD" -subj "/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORGANIZATION/OU=$ORGANIZATIONAL_UNIT/CN=$COMMON_NAME/emailAddress=$EMAIL"
# Generate Server key
openssl genrsa -out "server-key-$CODE.pem" 4096
# Generate Server Certs.
openssl req -subj "/CN=$COMMON_NAME" -sha256 -new -key "server-key-$CODE.pem" -out server.csr
echo "subjectAltName = IP:$IP,IP:127.0.0.1" >> extfile.cnf
echo "extendedKeyUsage = serverAuth" >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -passin "pass:$PASSWORD" -CA "ca-$CODE.pem" -CAkey "ca-key-$CODE.pem" -CAcreateserial -out "server-cert-$CODE.pem" -extfile extfile.cnf
# Generate Client Certs.
rm -f extfile.cnf
openssl genrsa -out "key-$CODE.pem" 4096
openssl req -subj '/CN=client' -new -key "key-$CODE.pem" -out client.csr
echo extendedKeyUsage = clientAuth >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in client.csr -passin "pass:$PASSWORD" -CA "ca-$CODE.pem" -CAkey "ca-key-$CODE.pem" -CAcreateserial -out "cert-$CODE.pem" -extfile extfile.cnf
rm -vf client.csr server.csr
chmod -v 0400 "ca-key-$CODE.pem" "key-$CODE.pem" "server-key-$CODE.pem"
chmod -v 0444 "ca-$CODE.pem" "server-cert-$CODE.pem" "cert-$CODE.pem"
# 打包客户端证书
mkdir -p "tls-client-certs-$CODE"
cp -f "ca-$CODE.pem" "cert-$CODE.pem" "key-$CODE.pem" "tls-client-certs-$CODE/"
cd "tls-client-certs-$CODE"
tar zcf "tls-client-certs-$CODE.tar.gz" *
mv "tls-client-certs-$CODE.tar.gz" ../
cd ..
rm -rf "tls-client-certs-$CODE"
# 拷贝服务端证书
mkdir -p /etc/docker/certs.d
cp "ca-$CODE.pem" "server-cert-$CODE.pem" "server-key-$CODE.pem" /etc/docker/certs.d/

(3)执行脚本

chmod a+x auto_gen_docker.sh
sh auto_gen.sh

拷贝当前目录下的tls-client-certs-docker.tar 文件到项目的resource下,并解压,如图所示:

(4)编写建立安全连接的方法

   public static DockerClient getDockerClient() 
        // 进行安全认证
        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
                // 服务器ip
                .withDockerHost("tcp://IP:PORT")
                .withDockerTlsVerify(true)
                // 压缩包解压的路径
                .withDockerCertPath("D:\\\\code\\\\my_code\\\\test-skill\\\\src\\\\main\\\\resources\\\\tls-client-certs-docker")
                // API版本 可通过在服务器 docker version 命令查看
                .withApiVersion("1.37")
                // 默认
                .withRegistryUrl("https://index.docker.io/v1/")
                // 默认 
                .withRegistryUsername("docker")
                // 默认
                .withRegistryPassword("123456")
                // 默认
                .withRegistryEmail("an23gn@163.com")
                .build();
        // docker命令执行工厂
        DockerCmdExecFactory dockerCmdExecFactory = new JerseyDockerCmdExecFactory()
                .withReadTimeout(60000)
                .withConnectTimeout(60000)
                .withMaxTotalConnections(100)
                .withMaxPerRouteConnections(10);
        dockerClient = DockerClientBuilder.getInstance(config).withDockerCmdExecFactory(dockerCmdExecFactory).build();
        return dockerClient;
    

使用这个docker-client代替上面普通连接生成的docker-client

    public DockerClient connectDocker()
        DockerClient dockerClient = getDockerClient();
        Info info = dockerClient.infoCmd().exec();
        String infoStr = JSONObject.toJSONString(info);
        System.out.println("docker的环境信息如下:=================");
        System.out.println(info);
        return dockerClient;
    

但是我们压缩包里面的认证文件是这样的:

所导致没有识别到认证文件,从而导致docker以为我们没有采用https协议,默认就是用http;

但是我们服务端是开启了https的,所以会出现上面那个错误。

如何解决呢?源码是修改不了的,我们只需要把我们生成的认证文件修改成他需要的名称格式就可以了,相信有不少网友也遇到过这样的坑吧!

重新运行 程序正常.

3.2 docker info 与 ping

3.2.1 docker info 

    /**
     * docker info
     * @throws URISyntaxException
     */
    private void dockerInfo() throws URISyntaxException 
        Info info = dockerClient.infoCmd().exec();
        System.out.println("docker info : " + info.toString());
    

3.2.2 ping 

    /**
     * ping
     * @throws URISyntaxException
     */
    private void dockerPing() throws URISyntaxException 
        dockerClient.pingCmd().exec();
        System.out.println("ping...");
    

3.3 镜像管理

3.3.1 搜索镜像

    /**
     * 搜索镜像
     * @param imageName
     * @return
     * @throws URISyntaxException
     */
    private List<String> searchImages(String imageName) throws URISyntaxException 
        List dockerSearch = dockerClient.searchImagesCmd("images").exec();
        List<String> list = new ArrayList<>();
        for (Object search : dockerSearch) 
            SearchItem search1 = (SearchItem) search;
            System.out.println(search1.getName());
            list.add(((SearchItem) search).getName());
        

        return dockerSearch;
    

3.3.2 从Dockerfile构建镜像

    /**
     * 从Dockerfile构建镜像
     * @return
     * @throws URISyntaxException
     */
    public String buildImage(String imageName, String imageTag, String dockerFile) throws URISyntaxException 
        ImmutableSet<String> tag = ImmutableSet.of(imageName + ":" + imageTag);
//        String imageId = dockerClient.buildImageCmd(new File("/opt/tmp/Dockerfile"))
        String imageId = dockerClient.buildImageCmd(new File(dockerFile))
                .withTags(tag)
                .start()
                .awaitImageId();
        return imageId;
    

3.3.3 获取服务器上所有的镜像

    /**
     * 获取服务器上所有的镜像
     * @return
     */
    public List<Image> getServerImageList()
        List<Image> imageList = dockerClient.listImagesCmd().exec();
//        System.out.println(imageList.size());
        for(Image image : imageList) 
            // 打印镜像名
            System.out.println(image.getRepoTags()[0]);
        
        return imageList;
    

3.3.4 判断服务器上是否存在某个镜像

    /**
     * 判断服务器上是否存在某个镜像
     * @return
     */
    public boolean existServerImage()
        boolean existFlag = false;
        List images = dockerClient.listImagesCmd().withImageNameFilter("busybox").exec();

        if (images.isEmpty()) 
            System.out.println("不存在 busybox 镜像。");
         else 
            existFlag = true;
            System.out.println("存在 busybox 镜像。");
        
        return existFlag;
    

3.3.5 加载镜像

    public LoadImageCmd loadImage(String filePath)
        LoadImageCmd loadImageCmd = null;
        try 
            loadImageCmd = client.loadImageCmd(new FileInputStream(filePath));
         catch (FileNotFoundException e) 
            e.printStackTrace();
        
        return loadImageCmd;
    

3.3.6 拉取镜像

    /**
     * 拉取镜像
     * @param repository 镜像名称:tag名称  "nginx:latest"
     * @return
     */
    public boolean pullImage(String repository)
        boolean isSuccess = false;
        try 
//            isSuccess = dockerClient.pullImageCmd(repository)
//                    .start()
//                    .awaitCompletion(30, TimeUnit.SECONDS);
//            isSuccess = dockerClient.pullImageCmd("nginx:latest").exec(new PullImageResultCallback()).awaitCompletion(30, TimeUnit.SECONDS);
            ResultCallback<PullResponseItem> exec = dockerClient.pullImageCmd(repository).exec(new ResultCallback<PullResponseItem>() 

                public void onStart(Closeable closeable) 
                    System.out.println("开始下载!");
                

                public void onNext(PullResponseItem object) 
                    // 实时显示出下载信息
                    System.out.println(object.getStatus());
                

                public void onError(Throwable throwable) 
                    throwable.printStackTrace();
                

                public void onComplete() 
                    System.out.println("下载完毕!");
                

                public void close() throws IOException 

                

            );

         finally 
            return isSuccess;
        
    

3.3.7 推送到Harbor仓库

    /**
     * 推送到Harbor仓库
     * @throws URISyntaxException
     * @throws InterruptedException
     */
    public void pushImage(String username, String password, String harborAddress, String imagesName) throws URISyntaxException, InterruptedException 
        AuthConfig authConfig = new AuthConfig()
                .withUsername(username) //"admin"
                .withPassword(password) // "Harbor12345"
                .withRegistryAddress(harborAddress); // "172.16.10.151:80"

        dockerClient.pushImageCmd(imagesName)
                .withAuthConfig(authConfig)
                .start()
                .awaitCompletion(30, TimeUnit.SECONDS);
    

3.3.8 打镜像tag

    /**
     * 打镜像tag
     * @throws URISyntaxException
     */
    public void tagImage(String imageName, String tagName, String version) throws URISyntaxException 
//        dockerClient.tagImageCmd("nginx:latest", "172.16.10.151:80/library/nginx", "v2").exec();
        dockerClient.tagImageCmd(imageName, tagName, version).exec();
    

3.3.9 删除镜像

    public void removeImage(DockerClient client,String imageId)
        client.removeImageCmd(imageId).exec();
    

3.3.10 查看镜像详细信息

    /**
     * 查看镜像详细信息
     * @throws URISyntaxException
     * @throws InterruptedException
     */
    public void inspectImage(String tagName) throws URISyntaxException, InterruptedException 
//        InspectImageResponse response = dockerClient.inspectImageCmd("172.16.10.151:80/library/nginx:v2").exec();
        InspectImageResponse response = dockerClient.inspectImageCmd(tagName).exec();
    

3.4 容器管理

3.4.1创建容器

     /**
     * 创建容器,映射单个端口
     * @return
     */
    public CreateContainerResponse createContainers(DockerEntity dockerEntity)
        CreateContainerCmd ccm = client.createContainerCmd(dockerEntity.getImageName()) // 镜像名
                .withName(dockerEntity.getContainerName()); // 容器名
        HostConfig hostConfig = new HostConfig();

        // 端口映射
        if (!CollectionUtils.isEmpty(dockerEntity.getBindingPorts()))
            // 封装端口映射
            List<PortBinding> list = new ArrayList<>();
            List<String> bindingPorts = dockerEntity.getBindingPorts();
            for (String bindingPort : bindingPorts) 
                String[] split = bindingPort.split(":");
                String serverPath = split[0];
                String volumePath = split[1];
                // 服务器暴露端口
                ccm = ccm.withExposedPorts(ExposedPort.parse(serverPath + "/tcp"));
                // 绑定主机端⼝ -> docker容器端⼝
                list.add(PortBinding.parse(serverPath + ":" + volumePath));
            
            hostConfig.withPortBindings(list);
        

        // 路径挂载
        if (!StringUtils.isEmpty(dockerEntity.getServerPath()) && !StringUtils.isEmpty(dockerEntity.getVolumePath()))
//        Bind bind = new Bind("服务器路径",new Volume("容器路径"));
            Bind bind = new Bind(dockerEntity.getServerPath(), new Volume(dockerEntity.getVolumePath()));
            hostConfig.setBinds(bind);
        
        CreateContainerResponse container = ccm
                .withHostConfig(hostConfig)
                .exec();
        return container;
    

3.4.2启动容器

    /**
     * 启动容器
     * @param containerId  容器ID
     */
    public void startContainer( String containerId)
        client.startContainerCmd(containerId).exec();
    

3.4.3 暂停容器

    /**
     * 暂停容器
     * @param containerId 容器ID
     */
    public void pauseCon( String containerId) 
        client.pauseContainerCmd(containerId).exec();
    

3.4.4 重启容器

    /**
     * 重启容器
     * @param containerId 容器ID
     */
    public void restartCon( String containerId) 
        client.restartContainerCmd(containerId).exec();
    

3.4.5停止容器

    public void stopContainer(String containerId)
        client.stopContainerCmd(containerId).exec();
    

3.4.6 删除容器

    public void removeContainer(String containerId)
        client.removeContainerCmd(containerId).exec();
    

 3.4.7 执行命令

    /**
     * 执行命令
     * @param containerName
     * @param imageName
     * @return
     */
    public CreateContainerResponse createContainers4(String containerName,String imageName)
        HostConfig hostConfig = new HostConfig();
        CreateContainerResponse container = client.createContainerCmd(imageName)
                .withName(containerName)
                .withHostConfig(hostConfig)
                .withCmd("python","/root/scripts/test.py")
                .exec();
        return container;
    

3.4.8 路径挂载

使用hostConfig方式,直接使用Volumes是行不通的

    public CreateContainerResponse createContainers(DockerClient client,String containerName,String imageName)
        HostConfig hostConfig = newHostConfig();
        Bind bind = new Bind("服务器路径",new Volume("容器路径"));
        hostConfig.setBinds(bind);
        CreateContainerResponse container = client.createContainerCmd(imageName)
                .withName(containerName)
                .withHostConfig(hostConfig)
                .exec();
        return container;
    

3.4.9 执行命令

    public CreateContainerResponse createContainers(DockerClient client,String containerName,String imageName)
        HostConfig hostConfig = newHostConfig();
        CreateContainerResponse container = client.createContainerCmd(imageName)
                .withName(containerName)
                .withHostConfig(hostConfig)
                .withCmd("python","/root/scripts/test.py")
                .exec();
        return container;
    

3.4.10利用 docker 执行脚本命令 传递参数

    public CreateContainerResponse createContainers(DockerClient client,String containerName,String imageName)
        HostConfig hostConfig = newHostConfig();
        CreateContainerResponse container = client.createContainerCmd(imageName)
                .withName(containerName)
                .withHostConfig(hostConfig)
                // 注意命令和参数不能进行组合,必须都用逗号隔开,也就是空格全部换成这里的,分割
                .withCmd("python","/root/scripts/test.py","-t","999")
                .exec();
        return container;
    

3.4.11 创建容器,先挂载路径,再执行脚本进行安装软件

    /**
     * 创建容器,先挂载路径,再执行脚本进行安装软件(比如:安装jdk等)
     * @param client
     * @param containerName
     * @param imageName
     * @return
     */
    public CreateContainerResponse createContainers(DockerClient client,String containerName,String imageName)
        HostConfig hostConfig = new HostConfig();
        Bind bind = new Bind("服务器路径",new Volume("容器路径"));
        hostConfig.setBinds(bind);
        CreateContainerResponse container = client.createContainerCmd(imageName)
                .withName(containerName)
                .withHostConfig(hostConfig)
                .withCmd("source","/opt/jdk/install.sh")
                .exec();
        return container;
    

以上是关于Docker-java实现应用和组件的远程发布的主要内容,如果未能解决你的问题,请参考以下文章

java操作docker示例(docker-java)

HaaS轻应用(JavaScript)OTA 远程升级组件

.net core 高吞吐远程方法调用组件XRPC

2003系统如何让同一用户在本地和远程同时登录

RPCORBMOM三类中间件比较

Jenkins构建推送拉取镜像和发布应用