返回文章列表

私有容器映像倉庫完全實戰指南

深入剖析私有容器映像倉庫的完整技術棧,從架構設計到安全配置,從映像同步到存儲管理,從身份認證到 HTTPS 傳輸,涵蓋 Docker Registry、Harbor、Skopeo 與企業級映像倉庫完整解決方案

容器技術 DevOps 基礎設施

在容器化技術快速普及的今天,容器映像的管理與分發成為企業 IT 基礎設施的關鍵組成部分。Docker Hub、Google Container Registry、Amazon ECR 等公有雲服務提供了便捷的映像存儲與分發能力,但在許多場景下,企業需要建立私有的容器映像倉庫。安全合規要求、網絡隔離環境、訪問速度優化、成本控制等因素,都驅動著私有倉庫的部署需求。

私有容器映像倉庫不僅是映像的存儲中心,更是企業容器化基礎設施的核心樞紐。它承擔著映像的版本管理、訪問控制、安全掃描、分發加速等多重職責。在金融、政府、醫療等對數據安全與合規性有嚴格要求的行業,私有倉庫是必不可少的基礎設施。在網絡受限或完全隔離的環境中,私有倉庫提供了唯一可行的映像分發方案。即使在網絡條件良好的環境,本地倉庫也能顯著提升映像下載速度,降低外網帶寬消耗。

在台灣的企業環境中,從製造業的工廠自動化到金融業的核心系統,從政府機關的電子政務到科技公司的產品開發,私有容器映像倉庫正在成為標準配置。企業需要在安全性、可靠性、性能與成本之間找到平衡,選擇適合自身需求的倉庫方案。Docker Registry 提供了基礎的映像存儲能力,Harbor 則提供了企業級的完整功能,包含權限管理、映像複製、漏洞掃描等。

本文將從實踐的角度,系統性地探討私有容器映像倉庫的完整技術棧。我們將深入分析倉庫架構設計、安全配置方法、映像同步策略、存儲管理技巧,透過實戰案例展示從基礎部署到生產環境的完整流程,並提供企業級映像倉庫的最佳實務建議。

容器映像倉庫架構基礎

容器映像倉庫遵循 OCI Distribution Specification 標準,定義了映像的存儲格式與訪問接口。映像由多個層組成,每層代表文件系統的增量變更。倉庫不僅存儲映像層,還管理映像的元數據,包含標籤、摘要值、配置信息等。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "容器映像倉庫架構" {
  package "客戶端層" {
    component "Docker CLI" as docker_cli
    component "Podman CLI" as podman_cli
    component "Skopeo" as skopeo
    component "CI/CD 系統" as cicd
  }
  
  package "倉庫服務層" {
    component "API Gateway\n統一入口" as gateway
    component "認證服務\n(Auth Service)" as auth
    component "授權服務\n(Authorization)" as authz
    component "映像服務\n(Registry Core)" as registry
  }
  
  package "存儲層" {
    database "映像層存儲\n(Blob Storage)" as blob_storage
    database "元數據存儲\n(Metadata DB)" as metadata
    database "配置存儲\n(Configuration)" as config
  }
  
  component "反向代理\n(Nginx/Traefik)" as proxy
  component "TLS 終止" as tls
}

cloud "客戶端請求" as client

client --> proxy
proxy --> tls
tls --> gateway

docker_cli --> proxy
podman_cli --> proxy
skopeo --> proxy
cicd --> proxy

gateway --> auth : 身份驗證
gateway --> authz : 權限檢查
gateway --> registry : 映像操作

registry --> blob_storage : 讀寫映像層
registry --> metadata : 管理元數據
registry --> config : 讀取配置

note right of auth
  JWT Token
  OAuth 2.0
  Basic Auth
end note

note bottom of blob_storage
  本地文件系統
  對象存儲 (S3/GCS)
  分散式存儲
end note

@enduml

倉庫的核心功能包含映像的推送與拉取。推送操作將映像層上傳到存儲後端,並創建映像清單記錄層的組成與順序。拉取操作根據映像標籤查找對應的清單,依次下載所需的層。由於層的去重設計,不同映像可以共享相同的基礎層,大幅節省存儲空間。

認證與授權是倉庫安全的基礎。Docker Registry 支持多種認證機制,從簡單的 HTTP Basic Auth 到基於 Token 的 OAuth 2.0。生產環境通常採用集中式的身份管理系統,如 LDAP、Active Directory 或 OIDC 提供商,實現單點登錄與細粒度的權限控制。

Docker Registry 部署與配置

Docker Registry 是官方提供的開源容器映像倉庫實現,提供了映像存儲與分發的核心功能。它輕量級且易於部署,適合小型團隊或開發環境使用。

#!/usr/bin/env python3
"""
Docker Registry 部署管理工具

自動化 Docker Registry 的部署、配置與管理
"""

import subprocess
import yaml
import os
from typing import Dict, Optional
from pathlib import Path
import secrets

class RegistryDeployer:
    """Docker Registry 部署器"""
    
    def __init__(self, base_dir: str = "/opt/registry"):
        self.base_dir = Path(base_dir)
        self.config_dir = self.base_dir / "config"
        self.data_dir = self.base_dir / "data"
        self.certs_dir = self.base_dir / "certs"
        self.auth_dir = self.base_dir / "auth"
        
        self._ensure_directories()
    
    def _ensure_directories(self) -> None:
        """確保所需目錄存在"""
        for directory in [self.config_dir, self.data_dir, 
                         self.certs_dir, self.auth_dir]:
            directory.mkdir(parents=True, exist_ok=True)
        
        print(f"目錄結構已建立於: {self.base_dir}")
    
    def generate_config(
        self,
        enable_delete: bool = True,
        enable_auth: bool = True,
        enable_tls: bool = True,
        storage_backend: str = "filesystem"
    ) -> Dict:
        """
        生成 Registry 配置
        
        Args:
            enable_delete: 啟用映像刪除
            enable_auth: 啟用身份驗證
            enable_tls: 啟用 HTTPS
            storage_backend: 存儲後端類型
            
        Returns:
            配置字典
        """
        config = {
            'version': '0.1',
            'log': {
                'level': 'info',
                'formatter': 'text',
                'fields': {
                    'service': 'registry'
                }
            },
            'storage': {
                'cache': {
                    'blobdescriptor': 'inmemory'
                }
            },
            'http': {
                'addr': ':5000',
                'headers': {
                    'X-Content-Type-Options': ['nosniff'],
                    'Access-Control-Allow-Origin': ['*'],
                    'Access-Control-Allow-Methods': ['HEAD', 'GET', 'OPTIONS', 'DELETE'],
                    'Access-Control-Allow-Headers': ['Authorization', 'Accept', 'Cache-Control']
                }
            },
            'health': {
                'storagedriver': {
                    'enabled': True,
                    'interval': '10s',
                    'threshold': 3
                }
            }
        }
        
        # 配置存儲後端
        if storage_backend == 'filesystem':
            config['storage']['filesystem'] = {
                'rootdirectory': '/var/lib/registry'
            }
        
        # 啟用映像刪除
        if enable_delete:
            config['storage']['delete'] = {
                'enabled': True
            }
        
        # 配置身份驗證
        if enable_auth:
            config['auth'] = {
                'htpasswd': {
                    'realm': 'Registry Realm',
                    'path': '/auth/htpasswd'
                }
            }
        
        # 配置 TLS
        if enable_tls:
            config['http']['tls'] = {
                'certificate': '/certs/registry.crt',
                'key': '/certs/registry.key'
            }
        
        return config
    
    def save_config(self, config: Dict) -> Path:
        """
        保存配置到文件
        
        Args:
            config: 配置字典
            
        Returns:
            配置文件路徑
        """
        config_file = self.config_dir / "config.yml"
        
        with open(config_file, 'w') as f:
            yaml.dump(config, f, default_flow_style=False)
        
        print(f"配置已保存: {config_file}")
        return config_file
    
    def generate_htpasswd(
        self,
        username: str,
        password: Optional[str] = None
    ) -> Path:
        """
        生成 htpasswd 認證文件
        
        Args:
            username: 用戶名
            password: 密碼(若為 None 則自動生成)
            
        Returns:
            htpasswd 文件路徑
        """
        if password is None:
            password = secrets.token_urlsafe(16)
            print(f"自動生成密碼: {password}")
        
        htpasswd_file = self.auth_dir / "htpasswd"
        
        # 使用 htpasswd 工具生成認證文件
        cmd = [
            'htpasswd', '-cBb',
            str(htpasswd_file),
            username,
            password
        ]
        
        try:
            subprocess.run(cmd, check=True, capture_output=True)
            print(f"認證文件已生成: {htpasswd_file}")
            print(f"用戶名: {username}")
            return htpasswd_file
        
        except subprocess.CalledProcessError as e:
            print(f"生成認證文件失敗: {e.stderr.decode()}")
            raise
        except FileNotFoundError:
            print("錯誤: htpasswd 工具未安裝")
            print("請執行: apt-get install apache2-utils (Ubuntu/Debian)")
            print("或: yum install httpd-tools (CentOS/RHEL)")
            raise
    
    def generate_self_signed_cert(
        self,
        domain: str = "localhost",
        days: int = 365
    ) -> tuple[Path, Path]:
        """
        生成自簽名證書
        
        Args:
            domain: 域名
            days: 有效期(天)
            
        Returns:
            證書和私鑰路徑
        """
        cert_file = self.certs_dir / "registry.crt"
        key_file = self.certs_dir / "registry.key"
        
        cmd = [
            'openssl', 'req',
            '-newkey', 'rsa:4096',
            '-x509',
            '-sha256',
            '-nodes',
            '-days', str(days),
            '-out', str(cert_file),
            '-keyout', str(key_file),
            '-subj', f'/CN={domain}',
            '-addext', f'subjectAltName=DNS:{domain}'
        ]
        
        try:
            subprocess.run(cmd, check=True, capture_output=True)
            print(f"證書已生成:")
            print(f"  證書: {cert_file}")
            print(f"  私鑰: {key_file}")
            return cert_file, key_file
        
        except subprocess.CalledProcessError as e:
            print(f"生成證書失敗: {e.stderr.decode()}")
            raise
    
    def deploy_registry(
        self,
        container_name: str = "local-registry",
        port: int = 5000
    ) -> str:
        """
        部署 Registry 容器
        
        Args:
            container_name: 容器名稱
            port: 映射端口
            
        Returns:
            容器 ID
        """
        # 停止並刪除已存在的容器
        try:
            subprocess.run(
                ['docker', 'stop', container_name],
                capture_output=True
            )
            subprocess.run(
                ['docker', 'rm', container_name],
                capture_output=True
            )
        except:
            pass
        
        # 啟動新容器
        cmd = [
            'docker', 'run', '-d',
            '--name', container_name,
            '--restart', 'always',
            '-p', f'{port}:5000',
            '-v', f'{self.config_dir}/config.yml:/etc/docker/registry/config.yml',
            '-v', f'{self.data_dir}:/var/lib/registry',
            '-v', f'{self.certs_dir}:/certs',
            '-v', f'{self.auth_dir}:/auth',
            'registry:2'
        ]
        
        try:
            result = subprocess.run(
                cmd,
                check=True,
                capture_output=True,
                text=True
            )
            
            container_id = result.stdout.strip()
            print(f"Registry 已部署:")
            print(f"  容器 ID: {container_id[:12]}")
            print(f"  訪問地址: https://localhost:{port}")
            
            return container_id
        
        except subprocess.CalledProcessError as e:
            print(f"部署失敗: {e.stderr}")
            raise
    
    def test_registry(
        self,
        host: str = "localhost",
        port: int = 5000,
        username: Optional[str] = None,
        password: Optional[str] = None
    ) -> bool:
        """
        測試 Registry 連接
        
        Args:
            host: 主機名
            port: 端口
            username: 用戶名
            password: 密碼
            
        Returns:
            是否成功
        """
        registry_url = f"https://{host}:{port}/v2/"
        
        cmd = ['curl', '-s', '-k']
        
        if username and password:
            cmd.extend(['-u', f'{username}:{password}'])
        
        cmd.append(registry_url)
        
        try:
            result = subprocess.run(
                cmd,
                check=True,
                capture_output=True,
                text=True
            )
            
            if '{}' in result.stdout or 'repositories' in result.stdout:
                print("Registry 連接測試成功")
                return True
            else:
                print(f"Registry 響應異常: {result.stdout}")
                return False
        
        except subprocess.CalledProcessError as e:
            print(f"連接失敗: {e.stderr}")
            return False

# 使用範例
if __name__ == "__main__":
    print("=== Docker Registry 部署工具 ===\n")
    
    # 初始化部署器
    deployer = RegistryDeployer("/opt/my-registry")
    
    # 生成配置
    print("[生成配置]\n")
    config = deployer.generate_config(
        enable_delete=True,
        enable_auth=True,
        enable_tls=True
    )
    config_file = deployer.save_config(config)
    
    # 生成認證文件
    print("\n[生成認證文件]\n")
    username = "admin"
    password = "secure_password"
    deployer.generate_htpasswd(username, password)
    
    # 生成證書
    print("\n[生成自簽名證書]\n")
    deployer.generate_self_signed_cert(domain="localhost")
    
    # 部署 Registry
    print("\n[部署 Registry]\n")
    print("執行命令: docker run ...")
    print("\n完成!您可以使用以下命令測試:")
    print(f"docker login localhost:5000 -u {username} -p {password}")

這個部署工具自動化了 Docker Registry 的完整配置流程,包含配置文件生成、證書創建、認證設置與容器部署。在實際環境中,建議使用 Let’s Encrypt 等服務獲取正式的 TLS 證書。

映像同步與管理

企業環境中,常需要在不同倉庫間同步映像。Skopeo 是一個強大的映像操作工具,提供了映像複製、檢查、刪除等功能,無需在本地啟動 Docker 守護進程。

#!/bin/bash
# 映像批量同步腳本

# 配置變數
SOURCE_REGISTRY="docker.io"
TARGET_REGISTRY="localhost:5000"
TARGET_USER="admin"
TARGET_PASS="secure_password"

# 要同步的映像列表
IMAGES=(
    "library/nginx:alpine"
    "library/redis:7-alpine"
    "library/postgres:14-alpine"
    "library/node:18-alpine"
)

echo "=== 開始映像同步 ==="
echo "源倉庫: $SOURCE_REGISTRY"
echo "目標倉庫: $TARGET_REGISTRY"
echo ""

# 登錄目標倉庫
echo "登錄目標倉庫..."
skopeo login \
    --username "$TARGET_USER" \
    --password "$TARGET_PASS" \
    --tls-verify=false \
    "$TARGET_REGISTRY"

# 同步映像
for IMAGE in "${IMAGES[@]}"; do
    echo ""
    echo "同步: $IMAGE"
    
    SOURCE="docker://$SOURCE_REGISTRY/$IMAGE"
    TARGET="docker://$TARGET_REGISTRY/$IMAGE"
    
    skopeo copy \
        --src-tls-verify=true \
        --dest-tls-verify=false \
        "$SOURCE" \
        "$TARGET"
    
    if [ $? -eq 0 ]; then
        echo "✓ $IMAGE 同步成功"
    else
        echo "✗ $IMAGE 同步失敗"
    fi
done

echo ""
echo "=== 同步完成 ==="

這個腳本展示了批量映像同步的流程。在生產環境中,通常會配置定時任務定期同步公有倉庫的映像,確保本地倉庫的映像保持更新。

存儲管理與垃圾回收

容器映像倉庫的存儲管理是長期運營的重要議題。映像的持續推送會累積大量的層數據,即使刪除映像標籤,底層的 blob 數據仍然保留。垃圾回收機制負責清理不再被任何映像引用的 blob,釋放存儲空間。

垃圾回收操作需要在 Registry 停止服務時執行,因為它需要讀取倉庫的完整狀態。對於生產環境,建議在維護窗口期執行垃圾回收,或部署高可用的 Registry 集群,輪流對各節點執行垃圾回收。

總結

私有容器映像倉庫是企業容器化基礎設施的核心組件。從 Docker Registry 的基礎部署到 Harbor 的企業級功能,從映像同步到存儲優化,本文系統性地探討了私有倉庫的完整技術棧。在台灣的企業環境中,建立穩定、安全、高效的私有倉庫,是容器化戰略成功的關鍵。

然而,倉庫的管理不僅是技術問題,更是組織流程與安全策略的體現。訪問控制、映像掃描、審計日誌等安全機制需要與企業的整體安全架構整合。映像的生命週期管理、版本策略、清理政策等需要與開發流程協調。倉庫的容量規劃、性能優化、災難恢復等需要與基礎設施團隊合作。