在没有Docker的情况下构建和推送Docker镜像

本文描述了一种方法,利用Go语言的库,无需Docker二进制文件,构建并直接推送到AWSECRDocker仓库,详细阐述了构建镜像层、配置、上传和清单生成的过程。
摘要由CSDN通过智能技术生成

曾几何时,我遇到了一个特殊的用例:我想在不使用Docker二进制文件或任何类似工具的情况下构建和推送Docker映像。

下面记录的方法仅依赖于Go标准库。

我做了一些假设,所以这个工作流不包括任何任意的场景。下面的工作流程依赖于这些假设和简化:

  • 程序处理单个静态构建的linux/amd64 Go二进制文件的映像,该二进制文件没有外部依赖,您可以使用以下Dockerfile获得映像:
FROM scratch
COPY main /main
CMD ["/main"]
  • 该程序应与AWS ECR存储库一起工作;

  • 为了简单起见,程序从本地docker配置中读取身份验证凭据。

步骤

Docker提供了一个HTTP API;它的文档中方便地有“放置图像”一节。

简而言之,构建和发布映像的整个过程有以下几个步骤:

  1. 构造一个Docker映像层。我的docker映像将有一个保存单个文件的层。
  2. 构造一个图像配置——一个JSON文件,通过摘要(layer.tar.gz的sha256和)引用图像层,加上一些元数据,比如ARCH/OS,要运行什么命令,要使用什么环境变量。
  3. 上传一个docker层对象。
  4. 上传一个图像配置对象。
  5. 构造一个图像清单——一个JSON文件,通过它们的摘要引用层和配置。
  6. 发布映像清单。

认证

此工作流使用的所有API端点都需要授权。

AWS ECR文档说明了如何做到这一点:每个请求必须有一个Authorization: Basic $TOKEN标头。

如果你检查一个~/.docker/config. conf文件。运行docker login命令后,您将看到该文件包含注册表域和授权令牌之间的映射。

为简单起见,我的代码将从该文件获取匹配域的授权令牌。

解析全图名称


在我的原型代码将不得不处理一个docker镜像的全名,由3部分组成:docker注册域名,镜像名称,和一个标签。例如,对于public.ecr标识的映像。Aws /amazonlinux/amazonlinux:最新全名,代码需要区分域名(public.ecr.aws)、短名(amazonlinux/amazonlinux)和标签(最新)。

解析它很简单。代码有一个专门的类型来保存所有独立的部分:

type imageSpec struct {
        Domain string
        Name   string
        Tag    string
}

 

构建Docker镜像层


Docker映像层只是一个gzip压缩的tar归档文件。Go标准库包archive/tar和compress/gzip涵盖了这种情况。

一个值得注意的警告是,在层构建期间,我需要计算未压缩和压缩内容(包括layer.tar和layer.tar.gz)的sha256校验和。有一种方便的方法可以做到这一点,因为程序构建了一个依赖于哈希的图像。哈希实现io。写入器接口,io。MultiWriter允许同时向多个写入器写入数据。

相关的代码是这样的:

outerHash, innerHash := sha256.New(), sha256.New()
buf := new(bytes.Buffer)
gw := gzip.NewWriter(io.MultiWriter(buf, outerHash)) // compressed stream (layer.tar.gz)
tw := tar.NewWriter(io.MultiWriter(gw, innerHash))   // uncompressed stream (layer.tar)
if err := tw.WriteHeader(&tar.Header{
        Name:    "main",
        Mode:    0755,
        ModTime: fi.ModTime(),
        Size:    fi.Size(),
}); err != nil {
        return nil, nil, err
}

...

outerDigest := fmt.Sprintf("sha256:%x", outerHash.Sum(nil))
innerDigest := fmt.Sprintf("sha256:%x", innerHash.Sum(nil))

镜像配置

一个最小的镜像配置可能是这样的:

{
    "os": "linux",
    "architecture": "amd64",
    "created": "2021-03-13T16:39:51.535472845Z",
    "config": {
        "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
        "Cmd": ["/main"],
        "WorkingDir": "/",
        "ArgsEscaped": true
    },
    "rootfs": {
        "type": "layers",
        "diff_ids": [
            "sha256:0a7631da79a7cf9bfbe5c09457481b869b45095dfd309681f7ed465e711815ed"
        ]
    }
}

rootfs→diff_ids下的数组通过未压缩的内容摘要引用层。它对应于上一节代码片段中的innerDigest变量。

对象上传


图像层和配置以相同的方式上传到注册表:向注册表执行POST /v2/<name>/blobs/uploads/请求,从响应Location头获取一个新的唯一URL,然后使用PUT请求将有效载荷上传到此URL。(这个配置应该以与图层相同的方式上传,这对我来说是一个棘手的问题。起初,我以为它一定是清单的一部分。)

从回复中获取上传位置的代码:

resp, err := http.DefaultClient.Do(req)
if err != nil {
        return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
        return "", fmt.Errorf("unexpected status on %v: %v", req.URL.Path, resp.Status)
}
uploadLocation := resp.Header.Get("Location")
if uploadLocation == "" {
        return "", errors.New("response has no valid location")
}
return uploadLocation, nil

然后上传一个对象(层或配置),我还需要它的字节大小和摘要-一个十六进制编码的sha256校验和对象内容的sha256:前缀:

// payload has a []byte type, it's either a layer in tar.gz format, or a json-encoded image config
digest := fmt.Sprintf("sha256:%x", sha256.Sum256(payload))
uploadLocation = uploadLocation + "?digest=" + digest
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadLocation, bytes.NewReader(payload))
if err != nil {
        return err
}
req.Header.Set("Authorization", "Basic "+auth)
req.Header.Set("Content-Type", "application/octet-stream")
req.ContentLength = int64(len(payload))
resp, err := http.DefaultClient.Do(req)
if err != nil {
        return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
        return fmt.Errorf("unexpected status on object upload %q: %v", req.URL, resp.Status)
}

图像清单


一旦层和配置都上传,我需要构建一个图像清单。现在,我掌握了所有的细节。

一个最小的清单看起来像这样:

{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 616,
    "digest": "sha256:82a2678c5bdcdf82a0fe0d54b0a58f5604182d4ffb7b9e5ca6835e5c207c720c"
},
"layers": [
    {
        "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
        "size": 360666,
        "digest": "sha256:81c1eb1aeb9f8cabc9eca332be5c331a1bd687c453eec180c1f447ee720827eb"
    }
]
}

在config键下,该对象引用前一节中的映像配置。Size以字节为单位描述配置大小,digest是有效负载摘要—与配置上传步骤中使用的相同。

图层数组描述所有图像图层。在我的例子中,图像只有一个图层。Size是层对象的大小,以字节为单位(layer.tar.gz文件大小)。摘要是层对象sha256摘要,对应于outerDigest变量。

Manifest通过一个专用的API端点发布,使用PUT /v2/<name>/ Manifest /<tag>请求。API支持不同的清单版本,上面的清单需要Content-Type: application/vnd.docker.distribution.manifest。v2 + json头。

如果成功,API将返回201个已创建的代码。

此时,注册表中应该出现一个新映像。

创建原型


您可以在https://github.com/artyom/push-to-docker-repo找到完整的原型代码。

它是一个独立的Go程序,只使用Go标准库。该程序可以采用静态构建的linux/amd64二进制文件,将其打包到docker容器中,并将其发布到docker注册表中。

请注意,它只在AWS ECR存储库中进行了测试。

更多资料

原文 https://artyom.dev/push-docker-image-without-docker.md

有道翻译_文本、文档、网页、在线即时翻译

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值