分类: 信息技术

  • About SCP on Synology DSM

    Scp to Synology DSM file system will use SFTP, not through SSH(default port: 8022), so you should use port 9022 (default port for SFTP) instead. And, you should use user space file system root as the target root. For example, you have a shared directory on /volume1/books/, you can use /books/ as the path, /volume1/books/ not works.

    scp -P 9022 convert_to_utf8 username@synology_dsm_hostname:/books/collection/
  • EdgeRouter UPnP2 Configuration with ACL Control

    Configuration Example:

    service {
        upnp2 {
            acl {
                rule 1 {
                    action allow
                    external-port 1024-65535
                    local-port 0-65535
                    subnet 192.168.199.0/24
                }
                rule 2 {
                    action allow
                    external-port 1024-65535
                    local-port 0-65535
                    subnet 192.168.5.121/32
                }
                rule 3 {
                    action deny
                    external-port 1024-65535
                    local-port 0-65535
                    subnet 192.168.0.0/16
                }
            }
            listen-on eth1
            nat-pmp enable
            secure-mode enable
            wan eth0
        }
    }

    Looks like it is compatible with Synology DSM 7.

  • DBeaver Clickhouse session problem

    When using user settings (e.g. SET max_parser_depth = 2000) in DBeaver’s Clickhouse query window, an error will occur.

    A typical error message is:

    SQL Error [113] [07000]: Code: 113. DB::Exception: There is no session or session context has expired. (THERE_IS_NO_SESSION) (version 22.x.x.x (official build)), server ClickHouseNode [uri=http://xxx.xxx.xxx.xxx:8123/system]

    How to resolve it?

    By default, DBeaver does not connect using a session (the CLI for example does). If you require session support (for example to set settings for your session), edit the driver connection properties and set session_id to a random string (it uses the http connection under the hood). Then you can use any setting from the query window.

    From Clickhouse Documentation:

    https://clickhouse.com/docs/en/interfaces/third-party/gui#dbeaver

  • OCFS2 手册

    OCFS2 – Linux共享磁盘集群文件系统

    引言

    OCFS2是一个文件系统。它允许用户存储和检索数据。数据存储在以分层目录树组织的文件中。它是POSIX兼容的文件系统,支持该规范说明的标准接口和行为语义。

    它也是一个共享磁盘集群文件系统,该文件系统允许多个节点同时访问同一磁盘。这就是乐趣的开始,因为允许在多个节点上访问文件系统会打开蠕虫罐。(This is where the fun begins as allowing a file system to be accessible on multiple nodes opens a can of worms.) 如果节点具有不同的架构怎么办?如果节点在写入文件系统时挂掉,该怎么办?如果两个节点上的进程同时进行读写操作,可以期待什么样的数据一致性?如果一个节点在文件仍在另一节点上使用的同时删除了该文件怎么办?

    (更多…)

  • Oracle 集群文件系统 (Cluster File System – OCFS2) 用户指南

    1. 引言

    集群文件系统允许集群中的所有节点通过标准文件系统接口并发访问设备。如此可以轻松管理需在集群中运行的应用程序。

    OCFS(版本1)于2002年12月发布,这使 Oracle Real Application Cluster(RAC)用户无需处理 RAW 设备即可运行集群数据库。该文件系统旨在存储与数据库相关的文件,例如数据文件,控制文件,重做日志,存档日志,等等。

    OCFS2 是”下一代” Oracle 集群文件系统。它被设计为通用集群文件系统。使用它,不仅可以将数据库相关文件存储在共享磁盘上,还可以存储 Oracle 二进制文件和配置文件(共享的 Oracle 主目录),从而使 RAC 的管理更加容易。

    (更多…)

  • Free SSL Cert from Let’s Encrypt! It’s REALLY FRAGRANT!!

    Traefik Docker Compose Example:

    version: '3.7'
    networks:
      livedignet:
        external: true
    services:
      traefik:
        image: "traefik:2.1"
        container_name: "traefik"
        networks:
          - livedignet
        command:
        # - "--log.level=DEBUG"
          - "--api.insecure=true"
          - "--providers.docker=true"
          - "--providers.docker.exposedbydefault=false"
          - "--entrypoints.web.address=:80"
          - "--entrypoints.websecure.address=:443"
          - "--certificatesresolvers.ldhttpchallenge.acme.httpchallenge=true"
          - "--certificatesresolvers.ldhttpchallenge.acme.httpchallenge.entrypoint=web"
        # When u are on test stage. UnComment the line below.
        # - "--certificatesresolvers.ldhttpchallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
          - "--certificatesresolvers.ldhttpchallenge.acme.email=your@email.com"
          - "--certificatesresolvers.ldhttpchallenge.acme.storage=/letsencrypt/acme.json"
        ports:
          - "80:80/tcp"
          - "443:443/tcp"
          - "8080:8080/tcp"
        volumes:
          - type: bind
            source: "./letsencrypt"
            target: "/letsencrypt"
          - type: bind
            source: "/var/run/docker.sock"
            target: "/var/run/docker.sock"

    Web Application Example:

    version: '3.7'
    networks:
      livedignet:
        external: true
    services:
      livedig:
        image: 'wordpress:latest'
        container_name: "livedig"
        networks:
          - livedignet
        external_links:
          - mysql
        environment:
          WORDPRESS_DB_HOST:      'mysql:3306'
          WORDPRESS_DB_USER:      'mysql_usrname'
          WORDPRESS_DB_PASSWORD:  'mysql_password'
          WORDPRESS_DB_NAME:      'mysql_dbname'
          WORDPRESS_TABLE_PREFIX: 'wp_'
        working_dir: '/var/www/html'
        labels:
          - "traefik.enable=true"
    
          - "traefik.http.routers.livedig_http.rule=Host(`livedig.com`)"
          - "traefik.http.routers.livedig_http.entrypoints=web"
          - "traefik.http.routers.livedig_http.middlewares=redirect-to-https"
          - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
    
          - "traefik.http.routers.livedig.rule=Host(`livedig.com`)"
          - "traefik.http.routers.livedig.entrypoints=websecure"
          - "traefik.http.routers.livedig.tls.certresolver=ldhttpchallenge"
        ports:
          - '80'
        volumes:
          - type: bind
            source: ./wp-content
            target: /var/www/html/wp-content
            read_only: false
  • ClickHouse Mutation

    在 ClickHouse 中,ALTER UPDATE/DELETE 等,被称为 Mutation。

    一、Mutation 异步执行注意事项

    Mutation 是异步执行的,所以如果有后续任何类型的 Query,无论是INSERTSELECTALTER等,如果这些 Query 的条件对 Mutation 的结果有依赖,那么都应该等待 Mutation 完全结束之后再操作。

    确认 Mutation 是否完成,可以通过查询system.mutations表中是否有相关的”is_done=0″记录来完成。

    检测是否有未完成的 Mutations:

    SELECT COUNT() FROM `system`.mutations
    WHERE `database`='${db_name}' AND `table`='${tbl_name}' AND is_done=0;
    

    二、删除挂起的 Mutation

    某同学曾经在ALTER UPDATE中错误地赋值,将NULL赋给不可为NULL的字段,从而使 Mutation 无法正确执行,然后就一直挂在系统里,而且不断报错。

    2019.09.05 10:46:11.450867 [ 9 ] {}  db_name.tbl_name: DB::StorageReplicatedMergeTree::queueTask()::: Code: 349, e.displayText() = DB::Exception: Cannot convert NULL value to non-Nullable type, Stack trace:
    

    此时便需要将挂起的 Mutation 清理掉。

    首先,通过

    SELECT * FROM `system`.mutations
    WHERE `database`='${db_name}' AND `table`='${tbl_name}' AND is_done=0;
    

    查得与挂起 Mutation 相关记录的mutation_id字段值等相关信息。然后进行清理。

    清理有两种手法:

    1. 系统自带清理

    语法:

    KILL MUTATION [ON CLUSTER cluster]
      WHERE 
      [TEST]
      [FORMAT format]
    

    例子:

    -- 取消并移除某单表的所有 Mutations:
    KILL MUTATION WHERE database = '${db_name}' AND table = '${tbl_name}'
    
    -- 取消某特定的 Mutation:
    KILL MUTATION WHERE database = '${db_name}' AND table = '${tbl_name}' AND mutation_id = '${mutation_id}'
    

    官方文档参考:KILL MUTATION

    2. 手工清理

    有两种 Case,一种是复制表,一种是非复制表。

    2.1 对于复制表,处理 ZooKeeper 中相应的 ZNode 即可。

    在 ZooKeeper 中找到znode /${path_to_table}/mutations/${mutation_id},将其删除。

    2.2 对于非复制表

    先将表卸载:

    DETACH TABLE `${tbl_name}`;
    

    ${clickhouse_data_dir}/${db_name}/${tblname}/ 目录中,删除 mutation${mutation_id}.txt 文件。

    重新装载表:

    ATTACH TABLE `${tbl_name}`;
    
  • Grafana Active Directory LDAP configuration

    Grafana Active Directory LDAP configuration examples.

    Configration example below allows your active directory member user use their sAMAccountName login into your Grafana service.

    U need manage the Admin/Editor/Viewer roles in AD through add the user to the specialfied AD group.

    Remember, DN is case sensitive, this is very important.

    # Set to true to log user information returned from LDAP
    verbose_logging = false
    
    [[servers]]
    # Ldap server host (specify multiple hosts space separated)
    host = "${livedig.yourServersIPorFQDN}"
    # Default port is 389 or 636 if use_ssl = true
    port = 389
    # Set to true if ldap server supports TLS
    use_ssl = false
    # Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
    start_tls = false
    # set to true if you want to skip ssl cert validation
    ssl_skip_verify = false
    # set to the path to your root CA certificate or leave unset to use system defaults
    # root_ca_cert = "/path/to/certificate.crt"
    
    # Search user bind dn
    bind_dn = "CN=robot,CN=IT System,CN=Users,DC=example,DC=io"
    # Search user bind password
    # If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
    bind_password = '${livedig.urUserBaseDNPassword}'
    
    # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
    search_filter = "(&(objectCategory=Person)(sAMAccountName=%s)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))"
    
    # An array of base dns to search through
    search_base_dns = ["CN=Users,DC=example,DC=io"]
    
    # In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
    # This is done by enabling group_search_filter below. You must also set member_of= "cn"
    # in [servers.attributes] below.
    
    ## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
    #group_search_filter = ""
    ## An array of the base DNs to search through for groups. Typically uses ou=groups
    #group_search_base_dns = [""]
    
    # Specify names of the ldap attributes your ldap uses
    [servers.attributes]
    name = "givenName"
    surname = "sn"
    username = "sAMAccountName"
    member_of = "memberOf"
    email =  "mail"
    
    # Map ldap groups to grafana org roles
    [[servers.group_mappings]]
    group_dn = "CN=Grafana Admin,CN=IT System,CN=Users,DC=example,DC=io"
    org_role = "Admin"
    # The Grafana organization database id, optional, if left out the default org (id 1) will be used.  Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
    # org_id = 1
    
    [[servers.group_mappings]]
    group_dn = "CN=Grafana Editor,CN=IT System,CN=Users,DC=example,DC=io"
    org_role = "Editor"
    
    [[servers.group_mappings]]
    # If you want to match all (or no ldap groups) then you can use wildcard
    group_dn = "CN=Grafana Viewer,CN=IT System,CN=Users,DC=example,DC=io"
    org_role = "Viewer"
    
  • Python中”if __name__ == ‘__main__’:”的解析

    Python中if __name__ == '__main__':的解析

    Python 源代码常会在代码最下方看到形如if name == 'main':的语句,下面介绍其作用。

    所有Python模块都有一个内置属性name,其值取决于如何使用模块。

    1. 如果被import,则模块__name__值通常为模块文件名,且不带路径及文件扩展名。
    2. 直接运行,则__name__值为__main__

    所以,if name == 'main'用来判断该模块的使用方式。

    如:

    class UsageTest:
        def __init(self):
            pass
    
        def f(self):
            print('Hello, World!')
    
    if __name__ == '__main__':
        UsageTest().f()
    

    在终端直接运行:

    $ python UsageTest.py
    Hello, World!
    

    import 方式:

    $ python
      >>>import UsageTest
      >>>UsageTest.__name__ # Test模块的__name__
      'UsageTest'
      >>>__name__ # 当前程序的__name__
      '__main__'
    
  • 使用 Docker 构建

    使用 Docker 构建

    原文:https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/ci/docker/using_docker_build.md

    GitLab CI 允许你使用 Docker Engine 构建和测试基于 Docker 的项目。

    这也允许您使用docker-compose和其他 docker-enabled 的工具。

    持续集成/持续部署的新趋势之一是:

    1. 创建应用程序镜像,
    2. 针对创建的镜像运行测试,
    3. 将镜像推送到远程 Registry,并
    4. 从推送的镜像部署到服务器。

    当您的应用程序已经具有可用于创建和测试镜像的Dockerfile时,它也很有用:

    $ docker build -t my-image dockerfiles/
    $ docker run my-docker-image /script/to/run/tests
    $ docker tag my-image my-registry:5000/my-image
    $ docker push my-registry:5000/my-image
    

    这需要 GitLab Runner 的特殊配置,以在作业期间启用docker支持。

    Runner 配置

    在作业中有三种方法可以使用docker builddocker run,每个都有自己的考虑。

    使用 Shell 执行器

    最简单的方法是在shell执行模式下安装 GitLab Runner。然后 GitLab Runner 作为gitlab-runner用户执行作业脚本。

    1. 安装 GitLab Runner
    2. 在 GitLab Runner 安装期间,选择shell作为执行作业脚本或使用命令的方法:

      sudo gitlab-ci-multi-runner register -n \
        --url https://gitlab.com/ \
        --registration-token REGISTRATION_TOKEN \
        --executor shell \
        --description "My Runner"
      
    3. 在服务器上安装 Docker Engine。 有关如何在不同系统上安装 Docker Engine 的更多信息,请参阅 Supported installations
    4. 新增 gitlab-runner 用户到 docker 组:

      sudo usermod -aG docker gitlab-runner
      
    5. 验证gitlab-runner是否可以访问Docker:
      sudo -u gitlab-runner -H docker info
      
      现在你可以通过将docker info添加到.gitlab-ci.yml中来验证一切是否正常:
      before_script:
        - docker info
      
      build_image:
        script:
          - docker build -t my-docker-image .
          - docker run my-docker-image /script/to/run/tests
      
    6. 现在可以使用docker命令,如果需要可以安装docker-compose

    注: * 通过在docker组中添加gitlab-runner,你可以有效地授予gitlab-runner的完整的 root 权限。有关更多信息,请阅读 On Docker security: docker group considered harmful

    使用 docker-in-docker 执行器

    第二种方法是使用专门的 Docker 镜像 docker-in-docker(dind),它安装了所有工具(dockerdocker-compose),并以特权模式在该镜像的上下文中运行作业脚本。

    为了做到这一点,请按以下步骤操作:

    1. 安装 GitLab Runner
    2. 从命令行注册 GitLab Runner 以使用dockerprivileged模式:

      sudo gitlab-ci-multi-runner register -n \
        --url https://gitlab.com/ \
        --registration-token REGISTRATION_TOKEN \
        --executor docker \
        --description "My Docker Runner" \
        --docker-image "docker:latest" \
        --docker-privileged
      
      上面的命令将注册一个新的 Runner 来使用 Docker 所提供的特殊docker:latest镜像。请注意,它使用privileged模式启动构建和服务容器。如果要使用docker-in-docker模式,您始终必须在 Docker 容器中使用privileged = true。 上面的命令将创建一个类似于这个的config.toml条目:
      [[runners]]
        url = "https://gitlab.com/"
        token = TOKEN
        executor = "docker"
        [runners.docker]
          tls_verify = false
          image = "docker:latest"
          privileged = true
          disable_cache = false
          volumes = ["/cache"]
        [runners.cache]
          Insecure = false
      
    3. 您现在可以在构建脚本中使用docker(请注意包含docker:dind服务)
      image: docker:latest
      
      # When using dind, it's wise to use the overlayfs driver for
      # improved performance.
      variables:
        DOCKER_DRIVER: overlay
      
      services:
        - docker:dind
      
      before_script:
        - docker info
      
      build:
        stage: build
        script:
          - docker build -t my-docker-image .
          - docker run my-docker-image /script/to/run/tests
      

    Docker-in-Docker 运行良好,是推荐的配置,但并不是没有挑战:

    • 启用--docker-privileged禁用了容器的所有安全机制,并使你的主机由于特权升级而暴露,从而导致容器突破(主机-容器屏障)。有关更多信息,请查看官方 Docker 文档 Runtime privilege and Linux capabilities
    • 当使用 docker-in-docker 时,每个作业都处于一个干净的环境中,没有过去的历史。并发作业工作正常,因为每个构建都获得自己的 Docker Engine 实例,因此不会相互冲突。但这也意味着作业可能会更慢,因为没有缓存层。
    • 默认情况下,docker:dind使用--storage-driver vfs,这是最慢的形式。要使用其他驱动程序,请参阅使用 overlayfs 驱动程序

    使用这种方法的示例项目可以在这里找到:https://gitlab.com/gitlab-examples/docker.

    Use Docker socket binding

    第三种方法是将/var/run/docker.sock绑定装载到容器中,以便 docker 在该镜像的上下文中可用。

    为了做到这点,遵循以下步骤:

    1. 安装 GitLab Runner.
    2. 从命令行注册 GitLab Runner 以使用docker并共享/var/run/docker.sock

      sudo gitlab-ci-multi-runner register -n \
        --url https://gitlab.com/ \
        --registration-token REGISTRATION_TOKEN \
        --executor docker \
        --description "My Docker Runner" \
        --docker-image "docker:latest" \
        --docker-volumes /var/run/docker.sock:/var/run/docker.sock
      
      上面的命令将注册一个新的 Runner 来使用 Docker 提供的特殊docker:latest镜像。请注意,它正在使用 Runner 本身的 Docker 守护进程,docker 命令产生的任何容器都将是 Runner 的兄弟,而不是所运行程序的子进程。这可能会有不适合您的工作流程的复杂性和局限性。 上面的命令将创建一个类似于这个的config.toml条目:
      [[runners]]
        url = "https://gitlab.com/"
        token = REGISTRATION_TOKEN
        executor = "docker"
        [runners.docker]
          tls_verify = false
          image = "docker:latest"
          privileged = false
          disable_cache = false
          volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
        [runners.cache]
          Insecure = false
      
    3. 您现在可以在构建脚本中使用docker(请注意,在 Docker 执行器中使用 Docker 时,不需要包含docker:dind服务):
      image: docker:latest
      
      before_script:
      - docker info
      
      build:
        stage: build
        script:
        - docker build -t my-docker-image .
        - docker run my-docker-image /script/to/run/tests
      

    虽然上述方法避免在特权模式下使用 Docker,但您应该了解以下影响:

    • 共享 docker 守护进程,禁用了容器的所有安全机制,并将主机暴露给特权提升,从而导致容器突破屏障。例如,如果一个项目运行docker rm -f $(docker ps -a -q),它将删除 GitLab Runner 容器。
    • 并发作业可能无效;如果你的测试创建了具有特定名称的容器,它们可能会相互冲突。
    • 将文件和目录从源代码库共享到容器中可能无法正常工作,因为卷装载是在主机上下文中完成的,而不是在构建容器中,例如:
      docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests
      

    使用 OverlayFS 驱动程序

    默认情况下,使用docker:dind时,Docker 使用vfs存储驱动程序,每次运行时都会拷贝文件系统。磁盘操作非常密集,如果使用不同的驱动程序(例如overlay),则可以避免这种情况。

    1. 确保使用最近的内核,最好是>= 4.2
    2. 检查overlay模块是否加载:
      sudo lsmod | grep overlay
      
      如果没有结果,那就没有加载。加载之:
      sudo modprobe overlay
      
      如果一切顺利,您需要确保在系统重启时也加载该模块。Ubuntu 系统上是通过编辑/etc/modules完成的。添加以下行:
      overlay
      
    3. .gitlab-ci.yml顶部定义一个变量以使用该驱动:
      variables:
        DOCKER_DRIVER: overlay
      

    使用 GitLab 容器 Registry

    注: – 此功能需要 GitLab 8.8 和 GitLab Runner 1.2。 – 从 GitLab 8.12 开始,如果你的帐户启用了两步认证,则需要传递个人访问令牌而不是密码,才能登录到 GitLab 的 Container Registry。

    一旦构建了 Docker 镜像,就可以将其推送到内置的 GitLab 容器 Registry 中。例如,如果你在 runner 上使用 docker-in-docker,那.gitlab-ci.yml可能如下:

     build:
       image: docker:latest
       services:
       - docker:dind
       stage: build
       script:
         - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
         - docker build -t registry.example.com/group/project/image:latest .
         - docker push registry.example.com/group/project/image:latest
    

    必须使用为你创建的特殊gitlab-ci-token用户,才能推送到连接到项目的 Registry。它的密码由$CI_JOB_TOKEN变量提供。这允许您自动构建和部署 Docker 镜像。

    您也可以利用其他变量来避免硬编码:

    services:
      - docker:dind
    
    variables:
      IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
    
    before_script:
      - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
    
    build:
      stage: build
      script:
        - docker build -t $IMAGE_TAG .
        - docker push $IMAGE_TAG
    

    在这里,$CI_REGISTRY_IMAGE将解析为与该项目相关联的 Registry 地址,$CI_COMMIT_REF_NAME将解析为该作业所在分支或标签的名称。还声明了我们自己的变量$IMAGE_TAG,将两者结合起来,以节省我们在script部分中的输入。

    这是一个更详细的例子,将任务分解为 4 个流水线(pipeline)阶段,包括并行运行的两个测试。build存储在容器 Registry 中,并在后续阶段使用,需要时则下载该镜像。对master的更改也被标记为latest,并使用特定于应用程序的部署脚本进行部署:

    image: docker:latest
    services:
      - docker:dind
    
    stages:
      - build
      - test
      - release
      - deploy
    
    variables:
      CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-
    
    image:$CI_COMMIT_REF_NAME
      CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest
    
    before_script:
      - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
    
    build:
      stage: build
      script:
        - docker build --pull -t $CONTAINER_TEST_IMAGE .
        - docker push $CONTAINER_TEST_IMAGE
    
    test1:
      stage: test
      script:
        - docker pull $CONTAINER_TEST_IMAGE
        - docker run $CONTAINER_TEST_IMAGE /script/to/run/tests
    
    test2:
      stage: test
      script:
        - docker pull $CONTAINER_TEST_IMAGE
        - docker run $CONTAINER_TEST_IMAGE /script/to/run/another/test
    
    release-image:
      stage: release
      script:
        - docker pull $CONTAINER_TEST_IMAGE
        - docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
        - docker push $CONTAINER_RELEASE_IMAGE
      only:
        - master
    
    deploy:
      stage: deploy
      script:
        - ./deploy.sh
      only:
        - master
    

    使用容器 Registry 的注意事项:

    • 在运行命令之前必须先登录到容器 Registry。把它放在before_script里,会在每个作业之前运行它。
    • 使用docker build --pull可以确保 Docker 在构建前获取 base 镜像的任何更改,以防您的缓存失效。这需要运行更长时间,但意味着不会遇到未打安全补丁的 base 镜像。
    • 在每个docker run之前做一个明确的docker pull,确保获取刚构建的最新镜像。如果您正在使用多个会在本地缓存镜像的 Runner,这一点尤为重要。在镜像标签中使用 git SHA 又使得这不太必要,因为每个作业都将是唯一的,并且您不应该有一个过时的镜像,但是如在依赖更改后,重新构建给定的 Commit,这仍然可能(有过时的镜像)。
    • 同时发生多个作业的情况下,你不会想直接构建为latest