Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

快速入门

SECoder 是软件工程课程所使用的开发平台, 它将服务同学们的小作业与大作业部署, 并且通过开源项目 GitLab 托管代码.

如需通过脚本或 API 工具调用 SECoder 后端接口, 可参考 SECoder API 使用说明.

注册平台并激活账号

从助教处获得平台的初始密码后, 请前往 t.secoder.net 进行注册以便激活账号.

如果无法打开网页, 请在校园网环境内访问.

注册步骤

  1. 打开注册页面

    访问 t.secoder.net

  2. 填写注册信息

    在注册页面中填写以下信息:

    • 姓名: 建议填写真实姓名, 便于课程管理
    • 账号: 填写你的学号
    • 电子邮箱: 填写最常用的邮箱, 与平时提交代码所使用的邮箱保持一致
    • 密码: 使用助教提供的初始密码
    注册页面 - 电脑端 注册页面 - 手机端
  3. 完成注册

    填写完所有信息后, 点击 注册 按钮提交注册信息.

  4. 激活账号

    注册成功后, 你的账号即被激活, 可以使用学号和密码登录 SECoder 平台.

激活 GitLab

SECoder 平台集成了 GitLab 代码仓库系统, 你可以通过 SECoder 账号直接登录 GitLab, 无需单独注册.

登录 GitLab

  1. 打开主页并找到 GitLab 入口

    登录 SECoder 平台后, 在主页可以看到 "GitLab 代码仓库和 CI/CD" 按钮.

    主页 GitLab 入口 - 电脑端 主页 GitLab 入口 - 手机端
  2. 进入 GitLab 登录页面

    点击 "GitLab 代码仓库和 CI/CD" 按钮, 会打开 GitLab 登录页面. 在页面下方可以看到 secoder 登录选项.

    GitLab 登录页面 - 电脑端 GitLab 登录页面 - 手机端
  3. 通过 SECoder 授权登录

    点击 secoder 按钮, 会跳转到 SECoder 授权页面. 页面会显示将要提供给 GitLab 的信息 (账号, 邮箱, 姓名), 点击 继续前往 GitLab 完成授权. 如果勾选 4 周内不再提示, 当前浏览器会在 4 周内自动批准后续 GitLab 登录; 这个选择不延长 SECoder 登录令牌的有效期.

    SECoder 授权页面 - 电脑端 SECoder 授权页面 - 手机端
  4. 登录成功

    授权成功后, 会自动跳转到 GitLab 主页, 页面顶部会显示你的用户名, 表示登录成功.

    GitLab 登录成功 - 电脑端 GitLab 登录成功 - 手机端

同步 GitLab 子组

每次通过 SECoder 登录 GitLab 时, GitLab 会收到当前账号, 邮箱和姓名. 如果你在 SECoder 中修改了这些信息, 需要重新执行一次 GitLab 登录以更新.

为了在 GitLab 中创建属于你的群组, 你需要手动触发同步:

  1. 进入个人资料页面

    在 SECoder 平台点击 个人资料 菜单.

  2. 同步 GitLab 子组

    在个人资料页面中, 找到 同步 GitLab 子组 按钮并点击.

    同步 GitLab 子组 - 电脑端 同步 GitLab 子组 - 手机端
  3. 完成同步

    点击按钮后, 系统会在 GitLab 中为你创建对应的子组. 如果你的组队情况发生变更, 例如加入了新的队伍, 需要再次点击同步按钮以更新群组信息.

用户登录

登录是使用 SECoder 平台的第一步, 本章将指导你完成登录流程.

访问登录页面

在浏览器中打开 https://t.secoder.net, 系统会自动跳转到登录页面.

登录页面 - 电脑端 登录页面 - 手机端

语言切换

登录页面支持多语言切换, 你可以点击页面右上角的地球图标, 在弹出的菜单中选择你需要的语言.

语言切换 - 电脑端 语言切换 - 手机端

系统支持三种语言:

  • English - 英文界面
  • 简体中文 - 简体中文界面(默认)
  • 繁體中文 - 繁体中文界面

选择语言后, 页面会立即切换到对应的语言显示.

主题切换

登录页面支持亮色和暗色模式切换, 你可以点击页面右上角的主题切换按钮来切换显示模式.

主题切换 - 电脑端 主题切换 - 手机端

点击该按钮后, 页面会在亮色模式和暗色模式之间切换. 浏览器会保存你的选择; 没有已保存选择时, 页面跟随操作系统的亮色或暗色偏好.

登录页面元素

登录页面包含以下元素:

  • 学号输入框: 输入你的学号
  • 密码输入框: 输入你的密码
  • 记住登录: 在关闭浏览器后继续保留登录状态
  • 登录按钮: 点击完成登录
  • 注册链接: 如果没有账号, 可以点击"在此注册"进行注册

填写登录信息

在对应的输入框中填写你的学号和密码.

填写登录信息 - 电脑端 填写登录信息 - 手机端
  1. 在 学号 输入框中填写你的学号
  2. 在 密码 输入框中填写你的密码
  3. 如果需要跨浏览器会话保留登录状态, 勾选 记住登录

未勾选 记住登录 时, 凭据保存在浏览器会话 Cookie 中, 关闭浏览器会结束该会话. 勾选后, 凭据会保存在当前浏览器的本地存储中. 两种方式都受服务端登录令牌有效期限制; 不要在公共或共享设备上勾选此项, 使用完毕后应点击 退出登录.

完成登录

点击 登录 按钮完成登录.

登录成功 - 电脑端 登录成功 - 手机端

登录成功后, 系统会跳转到概览页面, 你可以开始使用 SECoder 平台的各种功能.

常见问题

忘记密码

如果你忘记了密码, 请联系课程管理员重置密码.

账号被禁用

当前代码不会因为连续输错密码而自动临时锁定账号. 如果正确的账号和密码仍被拒绝, 账号可能尚未由课程管理员加入注册名单, 或已经被管理员禁用; 请联系课程管理员核对.

无法登录

如果遇到无法登录的问题, 请检查:

  • 学号和密码是否输入正确
  • 浏览器是否支持(推荐使用 Chrome, Firefox 或 Edge)
  • 网络连接是否正常

主界面

成功登录后, 你将进入 SECoder 的主界面 (概览页面). 这个页面是你使用平台各项功能的入口, 集成了多个开发运维服务.

界面概览

主界面采用简洁的卡片式布局, 主要包含两个部分:

  • SECoder 服务区: 展示可用的开发工具和服务
  • 用户信息区: 显示你的个人账户信息
主界面概览 - 电脑端 主界面概览 - 手机端

SECoder 服务

主界面提供了四个核心服务, 每个服务都通过按钮快速访问:

GitLab 代码仓库和 CI/CD

GitLab 是一个基于 Web 的 DevOps 生命周期工具, 提供 Git 代码仓库管理, 持续集成/持续部署 (CI/CD), 代码审查等功能.

点击 GitLab 按钮将跳转到 GitLab 服务页面.

GitLab 服务按钮 - 电脑端 GitLab 服务按钮 - 手机端

在 GitLab 中, 你可以:

  • 创建和管理代码仓库
  • 配置 CI/CD 流水线自动化构建和部署
  • 进行代码审查和合并请求
  • 管理项目成员和权限

相关文档: GitLab 官方文档 | Git 入手指南

SonarQube 代码质量和安全分析

SonarQube 是一个开源的代码质量和安全分析平台, 能够检测代码中的 bug, 漏洞和代码异味.

点击 SonarQube 按钮将跳转到 SonarQube 服务页面.

SonarQube 服务按钮 - 电脑端 SonarQube 服务按钮 - 手机端

通过 SonarQube, 你可以:

  • 查看代码质量评分
  • 识别代码中的潜在问题
  • 跟踪技术债务
  • 检查安全漏洞

相关文档: SonarQube 官方文档

Grafana 监控 集群指标告警

Grafana 是一个开源的分析和可视化平台, 用于监控和分析系统指标.

点击 Grafana 监控 按钮将跳转到 Grafana 服务页面.

Grafana 服务按钮 - 电脑端 Grafana 服务按钮 - 手机端

在 Grafana 中, 你可以:

  • 查看集群资源使用情况
  • 监控应用性能指标
  • 设置告警规则
  • 可视化时间序列数据

Kubernetes 仪表板 容器编排监控

SECoder 当前使用 Headlamp 提供 Kubernetes Web 界面, 前端按钮显示为 Kubernetes 仪表板. 普通用户主要在自己的命名空间中查看和管理容器化应用.

点击 Kubernetes 仪表板 按钮将跳转到 Headlamp 页面.

Kubernetes 服务按钮 - 电脑端 Kubernetes 服务按钮 - 手机端

通过该界面, 你可以:

  • 查看允许访问的集群状态和节点信息
  • 管理个人命名空间中的 Deployment, StatefulSet, Service 和 HTTPRoute 等资源
  • 查看容器日志
  • 查看个人命名空间的资源配额与使用情况

用户信息

主界面底部显示了你的个人账户信息, 包括:

  • 姓名: 你在注册时填写的姓名
  • 账号: 你的学号
  • 电子邮箱: 你在注册时填写的邮箱
用户信息区 - 电脑端 用户信息区 - 手机端

这些信息来自 SECoder 账户. 你可以在 个人资料 页面修改姓名和邮箱; 修改后需要重新登录 GitLab, 才会在下一次 SSO 时向 GitLab 提供最新信息.

导航栏

桌面端使用左侧边栏在 概览、用户、小组、邀请 和 个人资料 页面之间切换; 移动端先点击顶部菜单按钮展开侧边栏. 页面顶部还提供:

  • 当前页面标题
  • 语言按钮: 切换英文、简体中文或繁体中文
  • 主题切换: 切换深色/亮色模式
  • 退出登录: 退出当前账户

用户目录

登录后, 通过侧边栏点击 用户 可以打开只读的用户目录. 该页面用于查看当前平台 已经注册的用户及其组队状态, 不提供修改用户资料或管理账号的操作.

用户目录 - 电脑端 用户目录 - 手机端

目录按学号排序, 显示以下字段:

  • 姓名
  • 电子邮箱
  • 学号
  • sudo: 是否为平台管理员账号
  • 小组: 当前加入的小组标识符; 尚未组队时显示 无小组

当前界面显示首批最多 10 条记录. 点击页面右上角的刷新按钮可以重新读取列表. 如果姓名、邮箱或组队信息刚刚发生变化但尚未显示, 先刷新目录; 仍不一致时再联系课程管理员.

个人资料

个人资料页面允许你更新账户信息, 修改密码以及管理 Kubernetes 访问凭据.

访问个人资料页面

登录后, 通过侧边栏导航点击 个人资料 即可进入个人资料页面.

页面概览

个人资料页面包含以下主要功能区域:

  • 个人信息编辑: 更新姓名, 电子邮箱或密码
  • Kubernetes 凭据管理: 显示、复制或轮换 API 令牌, 并下载 kubeconfig
  • GitLab 同步: 同步 GitLab 子组信息
个人资料页面 - 电脑端 个人资料页面 - 手机端

编辑个人信息

更新姓名和邮箱

你可以随时更新你的姓名和电子邮箱信息. 保存成功约 2 秒后, SECoder 会清除当前登录 并返回登录页, 以免旧令牌继续携带修改前的信息. 重新登录 GitLab 后, GitLab 才会在下一次 SSO 中收到新姓名和邮箱.

  1. 在 姓名 输入框中修改你的姓名
  2. 在 电子邮箱 输入框中修改你的邮箱地址
  3. 点击 保存更改 按钮提交修改
保存更改按钮 - 电脑端 保存更改按钮 - 手机端

修改密码

为了账户安全, 建议定期修改密码.

  1. 在 新密码 输入框中输入新密码
  2. 在 确认新密码 输入框中再次输入相同的新密码
  3. 点击 保存更改 按钮完成密码修改

注意: 当前后端不会替你检查密码复杂度. 请主动使用足够长且不与其他服务复用的密码; 修改后页面会要求重新登录.

管理 Kubernetes API 令牌

页面上显示为 API 令牌 的凭据实际用于访问 SECoder Kubernetes API, 不是调用 SECoder 账户后端接口的登录令牌. GitLab CI 部署和下载的 kubeconfig 都使用这一 Kubernetes 凭据.

显示令牌

出于安全考虑, API 令牌默认是隐藏的. 点击 显示令牌 按钮可以查看你的令牌.

显示令牌按钮 - 电脑端 显示令牌按钮 - 手机端

复制令牌

令牌显示后, 复制令牌 按钮将变为可用状态. 点击该按钮可以将令牌复制到剪贴板, 方便你在代码或 API 工具中使用.

轮换令牌

如果令牌或 kubeconfig 可能泄露, 点击 轮换令牌, 阅读警告并确认. 页面会显示新令牌; 此前复制的令牌、下载的 kubeconfig, 以及使用旧令牌的 GitLab CI TOKEN 变量都会失效. 轮换后应立即更新仍需使用的 CI 变量并重新下载 kubeconfig.

轮换令牌确认 - 电脑端 轮换令牌确认 - 手机端

下载 kubectl 配置

点击 下载 kubectl 配置 按钮, 阅读并确认凭据安全提示后, 浏览器会下载一个 u-<你的用户 ID>.kubeconfig 文件. 该文件已经包含访问 SECoder Kubernetes 集群 所需的 API 地址、用户令牌、当前上下文和你的默认命名空间.

下载 kubectl 配置确认 - 电脑端 下载 kubectl 配置确认 - 手机端

下载后可以通过以下任一方式使用:

  1. 临时指定该配置文件执行 kubectl 命令:

    KUBECONFIG=/path/to/u-<你的用户 ID>.kubeconfig kubectl get pods
    
  2. 在当前 shell 会话中设置环境变量:

    export KUBECONFIG=/path/to/u-<你的用户 ID>.kubeconfig
    kubectl get pods
    kubectl get services
    
  3. 如果你希望长期使用, 可以将该配置合并到本机默认 kubeconfig 中:

    mkdir -p ~/.kube
    KUBECONFIG=~/.kube/config:/path/to/u-<你的用户 ID>.kubeconfig kubectl config view --flatten > /tmp/secoder-kubeconfig
    mv /tmp/secoder-kubeconfig ~/.kube/config
    chmod 600 ~/.kube/config
    

配置文件默认会切换到你的个人命名空间, 因此通常可以直接执行 kubectl get pods, kubectl apply -f deployment.yaml 等命令. 如果需要确认当前上下文和命名空间, 可以运行:

kubectl config current-context
kubectl config view --minify --output 'jsonpath={..namespace}'; echo

安全提示:

  • API 令牌和下载的 kubectl 配置等同于你的账户密码, 请妥善保管
  • 不要将令牌或 kubectl 配置分享给他人, 也不要公开发布在代码仓库中
  • 如果令牌或 kubectl 配置泄露, 立即在本页轮换令牌; 无法登录时再联系管理员

同步 GitLab 子组

当你加入新的团队或组队情况发生变化时, 需要同步 GitLab 子组信息.

点击 同步 GitLab 子组 按钮, 系统会将最新的组队信息同步到 GitLab, 确保你拥有正确的项目访问权限.

同步 GitLab 子组按钮 - 电脑端 同步 GitLab 子组按钮 - 手机端

建议在以下情况执行同步:

  • 首次使用平台
  • 加入新的课程小组
  • 组队成员发生变化
  • 发现 GitLab 中缺少某些项目权限

小组管理

小组是 SECoder 平台中团队协作的基本单位. 通过创建或加入小组, 你可以与同学共同完成课程项目.

创建小组

每个用户同时只能属于一个小组. 仅当你尚未加入小组时才能创建小组; 创建后你将成为组长, 负责小组信息和邀请.

进入小组管理页面

登录后, 通过侧边栏导航点击 小组 即可进入小组管理页面.

创建小组按钮 - 电脑端 创建小组按钮 - 手机端

开始创建小组

在 我的小组 标签页点击 创建小组 按钮开始创建流程.

填写小组信息

系统会弹出创建对话框, 需要填写以下信息:

小组名称

在 小组名称 输入框中填写小组的显示名称. 这个名称将展示给所有用户, 建议使用有意义的名称, 例如课程名称或项目名称.

填写小组名称 - 电脑端 填写小组名称 - 手机端

小组标识符

在 小组标识符 输入框中填写小组的唯一标识符.

标识符要求:

  • 建议直接使用符合 RFC 1035 的名称: 小写字母、数字和连字符 (-)
  • 建议以字母或数字开头和结尾, 最长 63 个字符
  • 服务端会把大写字母转为小写, 把其他字符转为连字符, 去掉首尾连字符并截断到 63 个字符; 创建完成后应以页面实际显示的标识符为准
  • 标识符创建后不能修改, 并且不能与现有小组重复
填写小组标识符 - 电脑端 填写小组标识符 - 手机端

建议示例:

  • 可直接使用: group1, team-alpha, cs101-2024
  • 会被规范化: Group-1 变为 group-1, -team 变为 team, team_1 变为 team-1

完成创建

确认信息无误后, 点击 创建 按钮完成小组创建.

创建按钮 - 电脑端 创建按钮 - 手机端

创建成功后, 你将成为该小组的组长. 在 我的小组 标签页中可以看到已创建的小组及其管理按钮.

创建成功 - 电脑端 创建成功 - 手机端

编辑小组

只有小组组长才能编辑小组信息. 你可以修改小组的显示名称.

进入编辑界面

在 我的小组 标签页中, 找到你创建的小组, 点击 编辑小组 按钮.

编辑小组按钮 - 电脑端 编辑小组按钮 - 手机端

修改小组名称

在弹出的编辑对话框中, 修改 小组名称. 注意: 小组标识符创建后无法修改.

修改小组名称 - 电脑端 修改小组名称 - 手机端

保存更改

点击 保存更改 按钮完成编辑.


删除小组

只有组长能删除小组. 在 我的小组 标签页点击 删除小组 并确认后, 系统会删除小组, 清除该小组的待处理邀请, 并让所有成员回到未分组状态.

删除小组确认 - 电脑端 删除小组确认 - 手机端

这个操作不能从学生界面撤销; 删除前请先确认小组标识符和成员列表.


邀请成员

只有小组组长才能邀请新成员加入小组.

发起邀请

在 我的小组 标签页中, 找到你创建的小组, 点击 邀请加入小组 按钮.

填写被邀请人学号

在弹出的对话框中, 输入要邀请的用户的学号.

填写被邀请人学号 - 电脑端 填写被邀请人学号 - 手机端

发送邀请

点击 发送邀请 按钮完成邀请发送.

被邀请人必须已经注册且当前未加入其他小组. 每位用户最多可以同时保留 5 个待处理邀请. 组长可以在仅对组长显示的 小组邀请 标签页中查看本组尚未处理的邀请.

发送邀请 - 电脑端 发送邀请 - 手机端
小组邀请标签页 - 电脑端 小组邀请标签页 - 手机端

接受邀请

收到小组邀请后, 你可以选择接受或拒绝. 已经加入小组的用户不能再接受其他邀请.

查看邀请

通过侧边栏导航点击 邀请 进入邀请管理页面.

接受邀请

在邀请列表中, 找到想要加入的小组, 点击 接受 按钮.

接受邀请 - 电脑端 接受邀请 - 手机端

拒绝邀请

如果不希望加入该小组, 可以点击 拒绝 按钮.

拒绝邀请 - 电脑端 拒绝邀请 - 手机端

接受邀请后, 该小组将出现在你的 我的小组 列表中.

部署应用

SECoder 平台使用 kustomization.yaml 管理部署配置, 并通过 GitLab CI/CD 自动部署.

准备工作

在 GitLab 项目里配置 2 个 CI/CD 变量:

  • TOKEN: 从 SECoder 的 个人资料 页面复制 API 令牌
  • NAMESPACE: 你的命名空间, 格式固定为 u-<学号>

示例: 学号是 2026000000, 则 NAMESPACE=u-2026000000.

这里的 TOKEN 是 Kubernetes API 令牌. 如果在个人资料页轮换令牌, 必须同步更新项目中的 TOKEN 变量, 否则后续部署会鉴权失败. 应把该变量设为 masked; 不要写进仓库或输出到 job 日志.

如果镜像保存在 Private GitLab 项目中, 还需要让 Kubernetes 在 CI 作业结束后继续拉取镜像: 到项目的 Settings → Repository → Deploy tokens 创建仅有 read_registry 权限的 Deploy Token, 将一次性显示的用户名和令牌分别存为 CI 变量 GITLAB_REGISTRY_USER 和 GITLAB_DEPLOY_TOKEN (后者设为 masked), 并在部署作业中用它们创建 Registry 拉取 Secret, 供 Deployment 的 imagePullSecrets 使用. 前后端项目各自创建, 完整示例见 课程部署文档.

先理解 kustomization.yaml

kustomization.yaml 是部署入口文件:

  • resources: 基础资源从哪里来
  • patches: 你要覆盖哪些字段

示例来自: secoder-tmpl/examples/minimal

resources:
  - git@github.com:THUSE-Course/secoder-tmpl.git/deploy/basic/
patches:
  - path: route.yaml
  - path: frontend.yaml
  - path: backend.yaml
  - path: pvc.yaml

这表示:

  1. 先读取 deploy/basic/ 的默认资源
  2. 再应用 Route、前端、后端和持久卷 4 个补丁

最小补丁示例

route.yaml: 绑定访问域名

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: route
spec:
  hostnames:
    - test-u-2026000000.t.secoder.net

frontend.yaml: 添加环境变量

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  template:
    spec:
      containers:
        - name: nginx
          env:
            - name: EXAMPLE_KEY
              value: hello,world

backend.yaml: 指定镜像版本

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: backend
spec:
  template:
    spec:
      containers:
        - name: backend
          image: nginx:1.29.5-alpine3.23

pvc.yaml: 挂载持久存储

当前最小模板还用 pvc.yaml 为后端 StatefulSet 增加 volumeMounts 和 volumeClaimTemplates. SECoder 用户命名空间只允许使用 nfs-tmp StorageClass; 模板示例申请 1Gi 的 ReadWriteOnce 存储并挂载到 /data.

请直接从当前 secoder-tmpl/examples/minimal 复制完整的 pvc.yaml, 再按应用实际的数据目录和容量修改. 不要只在 kustomization.yaml 中加入文件名而漏掉补丁文件.

在 GitLab CI/CD 中部署

SECoder 模板的 CI 文件位于: secoder-tmpl/gitlab-ci

由于 GitLab Runner 访问外部网络不稳定, 不要在 .gitlab-ci.yml 中使用 include: 远程引用这些模板. 请打开对应的模板 YAML 文件, 将内容直接复制到 项目 .gitlab-ci.yml 中, 再定义自己的作业.

通常做法是:

  1. 将需要的模板 YAML 内容内联复制到项目 .gitlab-ci.yml
  2. 新建一个作业 extends: .kustomize
  3. 设置 KUSTOMIZE_PATH 指向你的 kustomization.yaml 所在目录
  4. 提交代码后, GitLab Pipeline 自动执行部署

示例:

.kustomize:
  image:
    name: alpine/k8s:1.35.1
    entrypoint: [""]
  stage: deploy
  variables:
    KUSTOMIZE_PATH: .
  script:
    - |
      set -eu
      : "${TOKEN:?TOKEN is required}"
      : "${KUBERNETES_SERVICE_HOST:?not running in k8s?}"
      : "${KUBERNETES_SERVICE_PORT:?not running in k8s?}"
      export KUBECTL_APPLYSET=true
      kubectl \
        --token="${TOKEN}" \
        -n "${NAMESPACE}" \
        apply -k "${KUSTOMIZE_PATH:-.}" \
        --applyset="gitops-ci-${CI_PROJECT_PATH_SLUG:-unknown}" \
        --prune

deploy:
  extends: .kustomize

该模板使用 ApplySet 和 --prune: 同一项目后续部署时, 从 Kustomize 输出中移除的 旧资源会被删除. 如果某个资源校验失败, 之前的资源可能已经部分落地; 应先查看 job trace 和当前 namespace 中属于该 ApplySet 的对象, 再修正配置并重试, 不要直接批量删除 namespace 中的全部资源.

使用 BuildKit 缓存

如果项目用 BuildKit 构建镜像, 可以把缓存导出到同一个 GitLab Container Registry. 这样第一次 Pipeline 会创建缓存, 后续 Pipeline 会复用 Dockerfile 前面没有变化的层, 例如依赖安装步骤.

在 .gitlab-ci.yml 的 BuildKit 作业中增加一个缓存镜像引用:

variables:
  BUILDKIT_CACHE_REF: $CI_REGISTRY_IMAGE:buildcache

然后在 buildctl-daemonless.sh build 命令里同时导入和导出缓存:

buildctl-daemonless.sh build \
  --frontend "${BUILDKIT_FRONTEND}" \
  --local context="${BUILDKIT_CONTEXT}" \
  --local dockerfile="${BUILDKIT_DOCKERFILE_DIR}" \
  --import-cache type=registry,ref="${BUILDKIT_CACHE_REF}" \
  --export-cache type=registry,ref="${BUILDKIT_CACHE_REF}",mode=max \
  --output type="${BUILDKIT_OUTPUT_TYPE}",name="${BUILDKIT_IMAGE_NAME}:${BUILDKIT_IMAGE_TAG}",push="${BUILDKIT_PUSH}"

缓存和最终镜像使用同一个 Registry 登录信息, 因此通常不需要额外配置凭据. 如果多个分支都会构建, 可以把缓存引用改成 $CI_REGISTRY_IMAGE:buildcache-$CI_COMMIT_REF_SLUG, 避免不同分支互相覆盖缓存.

本地预览合成结果

在提交前, 建议先在本地预览:

kubectl kustomize path/to/your/kustomization-directory

这个命令只会输出最终 YAML, 不会实际修改集群.

系统设计

对于 SECoder 本身, secoder-tmpl 揭示了 SECoder 平台所包含的微服务.

部署 SECoder

SECoder 既可以指整个包含 Kubernetes 集群在内的平台, 也可以指集群中负责用户管理的前后端.

没有特别说明的话, 部署 SECoder 的含义是配置并调试集群在内的整个平台.

前置要求

  • 拥有校园网 IPv4 或 IPv6 地址, SECoder 至少要使用 22, 443 端口. 尽管可以在应用层进行负载均衡, 但更推荐的做法是进行端口绑定或裸机监听, 在这样的配置下 SECoder 才能够路由用户的部署业务并且对 GitLab SSH 协议的 TCP 流量进行转发.
  • NFS 存储
  • SECoder 域名的 DNS 编辑权限, 以及内外两层 TLS 证书的签发和续期能力. 只有实际部署并配置了 cert-manager 时, 才能认为集群内证书会自动续期.
  • 推荐至少两台虚拟机 (控制面和工作节点分离). 单节点部署也可运行, 但控制面、入口和所有有状态依赖会同时失效, 不属于高可用部署.

部署故障索引

故障排查

Kubernetes 集群

新版本 SECoder 的开发在 2026 年初完成, 使用 Debian 13 和 Kubernetes 1.36. 部署时应固定 Kubernetes minor 版本的软件源, 同时安装匹配版本的 kubeadm, kubelet 和 kubectl; 不要在未检查 CNI、CRI、Gateway API 和 Helm Chart 兼容性的情况下跨 minor 更新.

经过实验, 纯 IPv6 的集群也可以正常工作, 只需要提供合适的 DNS64 与 NAT64.

管理员应当首先准备 NFS 文件系统, 以便满足集群后续的存储需求. 其次准备 Debian 13 操作系统并安装 kubeadm.

kubeadm 可以通过配置文件首先启动一个控制面, 考虑课程的需要, 部署单个控制面节点 (分配 4c4g) 即可. 在这之外, 部署至少一个工作节点. 以下说明以 IPv4 单栈集群为准.

所有节点需要启用 Kubernetes 所需的内核转发参数:

cat >/etc/sysctl.d/k8s-net.conf <<'EOF'
net.ipv4.ip_forward = 1
EOF
sysctl --system

如果系统启用了 swap, 还需要关闭 swap 并从 /etc/fstab 中删除对应挂载. 确认 containerd 与 kubelet 已经安装后, 配置 containerd 使用 systemd cgroup, 并使 sandbox image 与当前 kubeadm 完全一致:

kubeadm config images list --kubernetes-version v1.36.4
containerd config dump | grep -E 'sandbox_image|SystemdCgroup'
crictl info

例如 kubeadm 输出 registry.k8s.io/pause:3.10.2 时, /etc/containerd/config.toml 中也应配置同一个 sandbox_image, 随后重启并验证:

systemctl restart containerd
systemctl is-active containerd
crictl info | grep sandboxImage

Debian 13 当前提供的 containerd 1.7 可以运行 Kubernetes 1.36, 但没有实现 CRI RuntimeConfig RPC. 在更新到 Kubernetes 1.37 或更高版本之前, 必须先执行 crictl runtime-config 并确认不再返回 Unimplemented; 否则先升级容器运行时.

确认运行时与 kubelet 已经启动后, 在控制面节点准备 kubeadm.conf. 其中 advertiseAddress 与 controlPlaneEndpoint 应当填写控制面节点的固定内网地址; podSubnet 与 serviceSubnet 必须互不重叠, 并且不能与节点所在网段冲突.

apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 10.128.1.111
  bindPort: 6443
skipPhases:
  - addon/kube-proxy
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
apiServer:
  certSANs:
    - c.t.secoder.net
networking:
  dnsDomain: cluster.local
  podSubnet: 100.64.0.0/17
  serviceSubnet: 100.64.128.0/17
controlPlaneEndpoint: 10.128.1.111:6443
clusterName: secoder
controllerManager:
  extraArgs:
    - name: "node-cidr-mask-size"
      value: "20"
    - name: "allocate-node-cidrs"
      value: "true"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
maxPods: 1024

maxPods 是 kubelet 对单节点 Pod 数量的上限, 不是按 CPU 或内存推导出的容量. 在这里的 /20 单节点 Pod CIDR 下有足够的地址容纳 1024 个 Pod, 因而单个工作节点 在配置层面最多承载 1024 个 Pod; 实际可承载数量还要受内存、CPU、PID、端口、 镜像磁盘和 CNI 状态容量限制. 多节点部署时, 必须同时核对 node-cidr-mask-size、每节点地址数与 maxPods.

这里跳过 addon/kube-proxy, 是因为 SECoder 默认使用 Cilium 的 kube-proxy replacement. 使用以下命令初始化控制面:

kubeadm init --config kubeadm.conf

初始化成功后, 为管理员配置 kubeconfig. 如果正在使用 root 用户, 可以直接执行:

export KUBECONFIG=/etc/kubernetes/admin.conf

如果使用普通用户, 则复制 kubeconfig:

mkdir -p "$HOME/.kube"
sudo cp -i /etc/kubernetes/admin.conf "$HOME/.kube/config"
sudo chown "$(id -u):$(id -g)" "$HOME/.kube/config"

此时控制面组件已经启动, 但网络插件尚未安装, 因而节点显示为 NotReady, CoreDNS 显示为 Pending 是正常现象. 接下来安装 Cilium. 首先添加 Helm 仓库:

helm repo add cilium https://helm.cilium.io/
helm repo update

随后创建 cilium.yaml. k8sServiceHost 应当与 kubeadm 配置中的 控制面地址一致, devices 应当填写节点承载集群流量的网卡名, ipv4NativeRoutingCIDR 应当与 podSubnet 一致.

autoDirectNodeRoutes: true
bpf:
  lbExternalClusterIP: true
devices: eth0
ipam:
  mode: kubernetes
ipv4:
  enabled: true
ipv4NativeRoutingCIDR: 100.64.0.0/17 # 要包含 Pod network CIDR
ipv6:
  enabled: false
k8sServiceHost: 10.128.1.111 # 填写 API Server 的 IP
k8sServicePort: 6443
kubeProxyReplacement: true
operator:
  replicas: 1
routingMode: native

安装 Cilium:

helm install cilium cilium/cilium \
  --namespace kube-system \
  --values cilium.yaml

等待 Cilium 与 CoreDNS 启动完成后, 控制面节点应当变为 Ready:

kubectl get pods -A
kubectl get nodes -o wide

故障排查

如果节点长期停留在 NotReady, 不要直接重复执行 kubeadm init 或重新安装所有组件. 先按下面的顺序缩小故障边界:

kubectl -n kube-system get pods -o wide
kubectl -n kube-system describe daemonset/cilium
kubectl -n kube-system logs daemonset/cilium --all-containers --tail=200
journalctl -u kubelet -u containerd --since=-15min --no-pager
crictl info
  • Cilium Pod 尚未创建时, 检查 Helm release、节点 taint 和镜像拉取事件.
  • Cilium 已运行而 CoreDNS 仍为 Pending 时, 检查 Pod CIDR、Service CIDR、 k8sServiceHost、网卡名以及是否确实跳过了 kube-proxy.
  • sandbox image 不一致时, 以 kubeadm config images list 为准修正 containerd, 重启 containerd 和 kubelet 后重新检查, 不要重新初始化控制面.
  • 日志显示 CRI 或 CNI 错误时, 先证明运行时和 Cilium 的实际故障已经消失, 再等待节点条件恢复. 一个成功的 Helm 命令不等于数据面已经可用.

最后, 在每个工作节点上执行控制面初始化时输出的加入命令:

kubeadm join 10.128.1.111:6443 \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

故障排查

如果初始化时输出的 token 已经过期, 可以在控制面节点重新生成:

kubeadm token create --print-join-command

所有工作节点加入后, 再次确认节点状态:

kubectl get nodes -o wide
kubectl get pods -A

所有节点都处于 Ready 状态, 且 kube-system 命名空间中的 Cilium, CoreDNS 和控制面组件都正常运行后, Kubernetes 集群的 bootstrap 就完成了.

控制节点默认是不调度工作负载的. 只有接受单节点非高可用风险时, 才让控制节点运行工作负载:

kubectl taint node <control-plane-node> node-role.kubernetes.io/control-plane:NoSchedule-

注意, 每个节点加入后, 使用 kubectl describe <nodename> 来查看集群给该节点分配了哪个 Pod Network CIDR. 随后在每个节点上配置对应路由, 例如在节点 .10 上:

[Match]
Name=eth0

[Network]
Address=172.30.20.10/24
Gateway=172.30.20.1
DNS=172.30.20.1

[Route]
Destination=100.64.32.0/20
Gateway=172.30.20.12
# 100.64.32.0/20 这个 Pod Network CIDR 在 172.30.20.12 机器上

[Route]
Destination=100.64.16.0/20
Gateway=172.30.20.11

在节点 .11 上:

[Match]
Name=eth0

[Network]
Address=172.30.20.11/24
Gateway=172.30.20.1

[Route]
Destination=100.64.32.0/20
Gateway=172.30.20.12

[Route]
Destination=100.64.0.0/20
Gateway=172.30.20.10

虽然 cilium 理论上能通过二层发现自动学到这个路由. 但如果未来要配置三层可达的路由, 就可以这么办.

IPv6

如果部署 IPv6 单栈集群, kubeadm 配置可参考:

apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: "2001:db8:0:0:2::2"
  bindPort: 6443
skipPhases:
  - addon/kube-proxy
---
apiVersion: kubeadm.k8s.io/v1beta4
clusterName: tunet
kind: ClusterConfiguration
controlPlaneEndpoint: "[2001:db8:0:0:2::2]:6443"
apiServer:
  certSANs:
    - c.t.secoder.net
networking:
  podSubnet: "2001:db8:0:0:3::/96"
  serviceSubnet: "2001:db8:0:0:3:1::/108"
controllerManager:
  extraArgs:
    - name: node-cidr-mask-size-ipv6
      value: "100"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
maxPods: 512

IPv6 单栈还需要启用 net.ipv6.conf.all.forwarding = 1, 并且仍然需要安装 CNI 后节点才会进入 Ready 状态.

FluxCD

为了保持不同学期之间的一致性, SECoder 采用 GitOps 的方式部署集群配置. FluxCD 是将配置文件同步为集群资源的有力工具. 管理员需要下载 flux 工具以便初始化 FluxCD.

开始前, 确认 Kubernetes 集群已经就绪, 当前 shell 的 kubeconfig 可以访问集群:

kubectl get nodes
flux check --pre

为了部署该实例版本, 记得查看 FluxCD 仓库下的 overlays 文件夹, 特别理解 overlays/secoder-infra*/, overlays/secoder-mon*/. 管理员应当至少查看 overlays 下的所有文件, 以了解自己部署的是什么东西.

例如, 查看 overlays/secoder-infra-pre/csi-driver-nfs/values.yaml, 可以看到:

apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: csi-driver-nfs
spec:
  values:
    storageClasses:
      - name: nfs-retain
        parameters:
          server: "10.128.1.111"
          share: /retain
          mountPermissions: "0777"
        reclaimPolicy: Retain
        volumeBindingMode: Immediate
        mountOptions:
          - nfsvers=4.2
          - noatime
      - name: nfs-tmp
        parameters:
          server: "10.128.1.111"
          share: /tmp
          mountPermissions: "0777"
        reclaimPolicy: Delete
        volumeBindingMode: Immediate
        mountOptions:
          - nfsvers=4.2
          - noatime

nfs-retain 用于需要保留的数据, 因而 PV 的 reclaim policy 必须是 Retain; nfs-tmp 用于可重建数据, reclaim policy 才是 Delete. 部署后要检查实际 PV, 不能只检查 StorageClass 名字:

kubectl get storageclass
kubectl get pv -o custom-columns=NAME:.metadata.name,CLASS:.spec.storageClassName,RECLAIM:.spec.persistentVolumeReclaimPolicy,PHASE:.status.phase

在部署时, 一定注意准备需要的 NFS server, 否则会遇到 PVC 无法绑定成功并不断重试.

推荐采用如下配置的 NFS server (async 是很重要的, 因为 GitLab CI 的任务也在 NFS 上跑, sync 会显著影响在 CI 上的编译的性能):

cat /etc/exports

/srv 172.30.20.0/24(rw,fsid=0,async,no_subtree_check,sec=sys,no_root_squash)

这里的 /srv 只是示例. 操作现有环境, 特别是清理或恢复数据之前, 必须先读取 /etc/exports, 再用 findmnt, exportfs -v 和 stat 确认实际 export root、 子目录和挂载目标; 不得从文档示例推断破坏性操作的路径.

GitOps 依赖顺序与发布验证

故障排查

执行恢复操作前应先确认故障对象、当前 Git revision 和实际工作负载状态. 不要因为上层 Kustomization 显示失败就删除整个 namespace; 底层 Pod 可能已经恢复, 只是控制器仍保留着之前耗尽的失败状态.

完整部署不是把所有 Kustomization 同时创建即可. 推荐的依赖顺序是:

  1. 安装 Gateway API CRD 和 Flux 控制器, 等待 CRD Established;
  2. 安装存储、Traefik 等 infra-pre 依赖;
  3. 安装 infra, 并等待 Kyverno 和其他 webhook Ready;
  4. 安装 mon-pre 与监控组件;
  5. 安装 PostgreSQL、Valkey、Garage 等 prod-pre 有状态依赖;
  6. 最后安装 SECoder、GitLab 和 SonarQube 等 prod 工作负载.

Flux dependsOn 之外, 每一步仍应设置明确的 health check, 不能只依赖对象创建顺序. Gateway API CRD 必须先于引用它们的 Gateway/Route; Kyverno 必须先于 SECoder 的生成策略. 当前仓库已经编码这些依赖, 修改 overlay 时应保持这一顺序.

大型 Chart 第一次冷启动会拉取数百 MB 到 1 GB 以上的镜像. GitLab 和 SonarQube 的 HelmRelease 应为 install/upgrade 设置足够的超时 (例如 60m) 和明确 remediation:

spec:
  timeout: 60m
  install:
    remediation:
      retries: 3
  upgrade:
    remediation:
      retries: 3
      remediateLastFailure: true

故障排查

普通的 reconcile 请求不会清除已经耗尽的 Helm remediation 次数. 当镜像拉取完成、 Pod 和依赖已经健康, 但 HelmRelease 仍报告 retries exhausted 时, 使用同一个新 token 同时请求 reconcile 和 reset:

token="$(date -Iseconds)"
namespace=NAMESPACE
release=RELEASE
kubectl -n "$namespace" annotate "helmrelease/$release" \
  reconcile.fluxcd.io/requestedAt="$token" \
  reconcile.fluxcd.io/resetAt="$token" --overwrite
kubectl -n "$namespace" get "helmrelease/$release" \
  -o jsonpath='{.status.lastHandledResetAt}{"\n"}'
kubectl -n "$namespace" wait --for=condition=Ready \
  "helmrelease/$release" --timeout=30m

只有 .status.lastHandledResetAt 已处理新的 token, 且 live workload 同时健康, 才能认为恢复成功. 随后还要显式 reconcile 并等待上层 Kustomization Ready:

stage=KUSTOMIZATION_NAME
kubectl -n flux-system annotate "kustomization/$stage" \
  reconcile.fluxcd.io/requestedAt="$token" --overwrite
kubectl -n flux-system wait --for=condition=Ready \
  "kustomization/$stage" --timeout=30m

发布前不仅要检查 HelmRelease values, 还要渲染实际 Chart 资源并断言关键字段. 至少执行:

kubectl kustomize overlays/<target> >/tmp/rendered.yaml
helm template <release> <repo>/<chart> --version <version> \
  --namespace <namespace> -f /tmp/effective-values.yaml >/tmp/chart.yaml
kubeconform -strict -summary -ignore-missing-schemas /tmp/chart.yaml
kubectl apply --server-side --dry-run=server -f /tmp/rendered.yaml

本地 render 与通用 JSON schema 不能覆盖 admission webhook 的全部规则. 只有 live webhook 接受 server-side dry-run 后才能提交并触发有状态资源创建.

Chart 大版本会移动 values 路径而不一定报错. 例如 Traefik Chart 41 的 Kubernetes Service 原生字段位于 service.spec; external IP 应写成:

service:
  spec:
    externalIPs:
      - 10.128.1.111

因此还应解析 /tmp/chart.yaml, 确认最终 Service.spec.externalIPs 等关键字段 确实存在, 而不是仅确认 HelmRelease 保存了输入值.

故障排查

如果 HelmRelease Ready 但 live Service 缺少该字段, 应修正权威 values、重新 render 和 server-side dry-run, 再通过 Flux 发布并读回 Service; 不要只 patch live Service, 否则下次 reconcile 会覆盖修复.

首次创建新的 GitOps 仓库时, 可以使用与托管平台匹配的 flux bootstrap gitlab、flux bootstrap github 或 flux bootstrap gitea 生成组件和 sync 清单. 生成后先审查并签名提交. 已经准备好的 SECoder GitOps 仓库则不应再次运行会写仓库的 bootstrap 命令; 新集群只需要安装已提交的组件, 并使用只读 deploy key 创建 source Secret. 管理员的 Git 写凭据与集群内只读凭据必须分开保存和轮换.

故障排查

如果 Flux controller 的出站配置依赖同一个 GitOps 仓库中的网络前置工作负载, 直接先安装 Flux 会形成“Flux 必须先拉到仓库才能创建自己的网络依赖”的启动环. 此时应在安装 Flux 前, 从已审核的本地 revision 手工应用该前置工作负载, 等待 Deployment Available, 验证其实际 listener, 再用一次真实的依赖源请求证明路径可用. 没有这类依赖的部署跳过该步骤. 这里的前置网络能力必须是平台配置的一部分, 不得引入未纳入平台配置和恢复演练的临时外部依赖.

对已准备好的仓库, bootstrap 顺序应明确分成两个 API discovery 周期:

kubectl create namespace flux-system
kubectl apply -f /secure/path/flux-read-secret.yaml
# 如有仓库内网络前置依赖, 在这里应用并完成 listener/依赖请求验收.
kubectl apply -f clusters/secoder/flux-system/gotk-components.yaml
kubectl wait --for=condition=Established \
  crd/gitrepositories.source.toolkit.fluxcd.io \
  crd/kustomizations.kustomize.toolkit.fluxcd.io --timeout=5m
kubectl apply -f clusters/secoder/flux-system/gotk-sync.yaml

故障排查

如果把 CRD、controller 和 GitRepository/Kustomization CR 放在同一次 kubectl apply 中, 客户端可能因为 REST mapping 尚未刷新而拒绝后两种 CR. 这不表示 CRD 安装失败; 等 CRD Established 后单独重试 sync 清单即可.

验收不能只等待一个可能早已为 true 的 Ready 条件. 应等待这次发布对应的精确 artifact 和 applied revision:

revision='master@sha1:SIGNED_COMMIT_SHA'
kubectl -n flux-system wait \
  --for=jsonpath='{.status.artifact.revision}'="$revision" \
  gitrepository/flux-system --timeout=10m
kubectl -n flux-system wait \
  --for=jsonpath='{.status.lastAppliedRevision}'="$revision" \
  kustomization/flux-system --timeout=20m
flux get sources git -A
flux get kustomizations -A
flux get helmreleases -A

故障排查

若 source Ready 但子 Kustomization 停滞, 依次查看它的 dependsOn、health check、 事件和 controller 日志.

只有精确 revision 已应用、所有依赖层 Ready、所有 HelmRelease Ready, 且不存在异常 Pod 时, bootstrap 才完成.

反代

故障排查

反代通过 Traefik 的 LoadBalancer/External IP 暴露. 如果外层再部署 Nginx 并在 Nginx 终止 TLS, 需要单独维护外层证书、到 Traefik 的 upstream, 以及足够大的 client_max_body_size. 收到外层 502 报告后, 先从同一公网路径复测并记录当前响应; 不要基于已经恢复的旧故障直接修改入口. 若仍失败, 应同时检查:

kubectl -n infra get service traefik -o wide
kubectl -n infra get endpointslice -l kubernetes.io/service-name=traefik
kubectl -n infra get gateway traefik-gateway -o yaml
kubectl get httproute,tcproute -A -o wide

每条 Route 都应在 .status.parents[].conditions 中出现 Accepted=True 和 ResolvedRefs=True. Traefik Chart 创建的 Gateway 通常名为 <release>-gateway; 本部署为 traefik-gateway, 不能把 TCPRoute 的 parentRef 写成不存在的 traefik. Gateway listener 显示 attachedRoutes: 0 而后端 Service/Endpoint 正常时, 优先检查 parent 名称和 sectionName.

外层 Nginx 与集群入口是两个独立故障边界. 公网 HTTPS 正常不代表公网 GitLab SSH 正常; 外层主机还必须把 gitlab.<domain>:22 的 TCP 流量转发到 Gateway 的 SSH listener. 应分别验证 HTTP、Gateway TCPRoute/Endpoint 和公网 22 端口. 如果没有外层主机的授权访问, 不要用修改集群内 TCPRoute 来掩盖公网 connection refused.

外层和集群内 Traefik 也可能使用不同证书. 即使外层 Let's Encrypt 证书有效, 提交在 GitOps 仓库中的 secoder-tls 仍可能过期, 影响节点 hosts shortcut 或 直接访问 Traefik 的客户端. 部署和日常监控都要分别检查两层证书的 SAN 与 notAfter; 若未部署 cert-manager, 必须建立人工续期和滚动验证流程.

镜像拉取与冷启动

故障排查

部署前应逐一验证 Kubernetes、Cilium、Flux controller、Helm Chart 和业务镜像所用 registry 的实际可达性, 并预拉取 Cilium、pause、Flux controller 及其他启动链关键 镜像. registry 首页返回 401 可能只是正常的认证 challenge; 应结合 containerd 事件、 active transfer 和目标镜像是否最终出现来判断, 不能把一次 HTTP 状态码当成拉取成功.

containerd 的启动路径不能只依赖由同一个 containerd 启动的集群内网络工作负载, 否则会形成 runtime -> CNI/Service -> 网络 Pod -> runtime 的冷启动环. 如果采用镜像 中转站或内部 registry, 应保证它独立于待恢复集群, 记录原始与中转 digest, 并继续在 GitOps 中固定不可变 digest.

镜像大且节点缓存为空时, 先观察拉取是否持续推进. Helm 超时但 active transfer 仍增长通常是 action timeout, 不是 Chart 不兼容. 等待镜像完成后, 按上文的 requestedAt/resetAt 路径恢复; 如果拉取没有进展, 再检查 registry 认证、DNS、 节点磁盘、containerd 日志和 Pod event. 不要在镜像仍正常下载时删除 release 或 PVC.

GitLab

因为 GitLab 的 Helm Chart 所能覆盖的配置有限, 在确认 GitLab 正确启动后, 管理员需要登录 root 用户并配置下列设置:

通过以下命令获取 root 用户的密码:

kubectl get secret -n prod gitlab-gitlab-initial-root-password \
  -o jsonpath="{.data.password}" | base64 -d

命名空间和 Secret 名称以当前 Helm release 为准. 使用外部 CloudNativePG、Valkey 和 Garage 时, 必须先验证数据库 migration、Gitaly、Sidekiq、Webservice、Registry、 Runner 注册和 Toolbox, 不能用单个 Web 页面 200 代替完整 install 验收.

课程当前不使用 GitLab incoming email. Helm values 应显式禁用 incoming email 和 Mailroom, 同时可以保留 outbound SMTP. 验收时确认没有 Mailroom Deployment/Pod; 不要因为看到 outbound email 配置而启用收件链路.

管理员初始化

GitLab 19 的设置页会把折叠 accordion 中的控件保留在页面树中. 每次修改前先展开 对应 section, 保存后 hard reload 并逐项读回; 自动化工具报告点击成功并不能证明 设置值已经改变.

  1. 修改 root 初始密码

    使用上面取得的初始密码登录 GitLab, 立即设置符合当前密码策略的新密码. 在完成 SECoder SSO 锁定验收前保留这一管理员密码入口, 但不得继续使用初始密码.

  2. 抄写 root 用户的 email

    把 GitLab root 用户的 email 抄写下来, 然后将其设置为 SECoder 的 root 用户的 email. 目的是让 SECoder 的 root 用户同样登录 gitlab 的 root 用户.

    做完这一步之后必须用 SECoder 的 root 登录一次 GitLab, 不然禁止 GitLab 密码登录后就无法再登录 GitLab 了.

    故障排查

    如果在 email 对齐前试登录并自动创建了重复用户, 恢复顺序必须是: 登录重复 用户并解除 JWT identity, 将 GitLab root 的主 email 写入 SECoder root, 再把 JWT identity 链接到 GitLab root, 最后删除重复用户. 完成后仍须用全新浏览器 会话重新证明 SECoder SSO 落到 GitLab 管理员 root, 才能禁用密码登录.

    如果真实 SSO redirect 返回 JWT signature verification failed, 立即停止密码登录 禁用操作. 从 SECoder 实际签名私钥派生公钥, 将其 fingerprint 与 GitLab verifier 当前读取的公钥 fingerprint 比较; 不要直接比较私钥和公钥文件的 SHA-256. 若 GitOps source 已正确而 live Secret/ConfigMap 仍旧, 应先恢复 reconciliation, 而不是改写 正确 source. 修正后重启实际读取该配置的 workload, 再用全新浏览器会话执行完整 redirect, 必须落到预期管理员账号. 仅看到 provider 按钮或 email 相同都不算通过.

  3. 允许创建不过期的 PAT

    进入 Admin > Settings > General > Account and limit,

    • 禁用 Access token expiration
    • 禁用 Allow new users to create top-level groups

    点击 Save changes 保存选择.

  4. 禁止注册

    进入 Admin > Settings > General > Sign-up restrictions,

    • 禁用 Sign-up enabled

    点击 Save changes 保存选择.

  5. 禁止密码登录

    进入 Admin > Settings > General > Sign-in restrictions

    • 禁用 Allow password authentication for the web interface
    • 禁用 Allow password authentication for Git over HTTP(S)
    • 启用 Disable password authentication for users with an SSO identity

    在执行这一步之前, 必须先把 GitLab root email 设置为 SECoder root email, 并用 SECoder root 完成一次真实 SSO 登录. 在独立浏览器会话验证 SSO 成功之前, 不得禁用密码登录, 否则可能锁死管理员入口. 同样注意保存设置.

  6. 设置仓库

    进入 Admin > Settings > Repository > Default branch

    • 设置初始分支为 master
    • 允许 Maintainers push 到受保护分支
    • 允许 Developers + Maintainers merge
    • 启用 Allow developers to push to the initial commit

    同样注意保存设置

  7. 设置项目默认可见性

    进入 Admin > Settings > General > Visibility and access controls

    • 设置 Default project visibility 为 Private

    同样注意保存设置

  8. 配置 CI/CD

    进入 Admin > Settings > CI/CD > Continuous Integration and Deployment

    • 禁用 Default to Auto DevOps pipeline for all projects

    同样注意保存设置

  9. 创建 Access token

    进入 User settings > Personal access tokens 创建名为 rw 的 token, 设置 scope 为:

    • api

    这里的 token 随后应当提供给 SECoder 的 reconciler. 格式为

    secretGenerator:
      - name: reconciler
      literals:
        - gitlab-token=<your-token>
    

    故障排查

    如果 Secret 使用固定名称并通过环境变量注入, 更新 Secret 不会自动重启 reconciler/exporter. 应显式 rollout restart 这些消费者, 或把 Secret checksum 放入 Pod template annotation 以触发滚动更新. Grafana OAuth Secret 同理.

  10. 创建 System hook

    确认新的 API token 已被消费者读取且 exporter Ready 后, 进入 Admin > System hooks, 配置 URL 为 http://exporter:8000. 是否配置 Secret token 必须与 exporter 的 GITLAB_WEBHOOK_SECRET 一致. 最终验收必须创建真实项目并 push 一次, 确认 exporter 成功处理该 Push event.

    故障排查

    如果测试返回 502, 检查 exporter Service、Endpoint、Pod readiness 和 webhook Secret 是否一致; 不要把 502 当作正常状态.

  11. 创建顶级分组

    以 root 身份创建 g2026, u2026, pub 顶级组. 前两个在后续配置中会用到.

  12. 为 Grafana 配置 oauth

    Configure GitLab OAuth authentication | Grafana documentation

    GitLab application 的 callback URL 是 https://grafana.t.secoder.net/login/gitlab. 完成后, 记录下凭据, 填写到集群配置中; Secret 生效并重启 Grafana 后, 用全新浏览器会话验证一次 GitLab OAuth 登录.

出站邮件验收与恢复

出站 SMTP 应在 GitLab 管理员初始化和集成配置完成后验收, 不能只以 HelmRelease Ready 作为结论. 按以下顺序核对, 全程不得打印密码:

  1. 比较 GitOps source 中的 SMTP 非敏感字段与 live Helm values;
  2. 用 secret-safe helper 计算 source generator 中密码的 SHA-256, 再与 live Secret 和 Rails runtime 的密码 SHA-256 比较;
  3. 从 Rails runtime 读回 delivery method、服务器、端口、TLS、发件人和 reply-to;
  4. 同步发送一封测试邮件, 证明 SMTP provider 接受 transaction;
  5. 由收件人确认收到预期发件人、主题和正文;
  6. 再次确认 incoming email 仍为 false, 且没有 Mailroom Deployment/Pod.
kubectl -n prod get helmrelease/gitlab
kubectl -n prod get deployment,pod -o name
kubectl -n prod get secret gitlab-smtp \
  -o jsonpath='{.data.password}' | base64 -d | sha256sum
kubectl -n prod exec deployment/gitlab-toolbox -c toolbox -- \
  gitlab-rails runner 'require "json"; require "digest"; s=ActionMailer::Base.smtp_settings; puts({delivery_method: ActionMailer::Base.delivery_method, address: s[:address], port: s[:port], authentication: s[:authentication], starttls_auto: s[:enable_starttls_auto], verify_mode: s[:openssl_verify_mode], from: Gitlab.config.gitlab.email_from, reply_to: Gitlab.config.gitlab.email_reply_to, password_sha256: Digest::SHA256.hexdigest(s[:password].to_s)}.to_json)'
kubectl -n prod exec deployment/gitlab-toolbox -c toolbox -- \
  gitlab-rails runner 'Notify.test_email("operator@example.edu", "GitLab SMTP test", "delivery validation").deliver_now'

故障排查

失败时按不一致边界恢复:

  • source 与 live Secret 不一致: 先确认权威 source 正确, 再恢复 Flux/Helm reconciliation; 不要把错误的 live 值回填到 source.
  • live Secret 与 Rails runtime 不一致: 检查 GitLab Chart 生成的配置和 Secret 引用, 等 Helm reconciliation 完成后, 只滚动重启实际消费 SMTP 配置的 workload.
  • runtime 一致但同步发送失败: 根据原始错误检查 DNS、端口、STARTTLS、证书校验、 发件人 allowlist 和 provider credential; 不要关闭 TLS 校验来掩盖问题.
  • transaction accepted 但未收到: 检查退信、垃圾邮件和 provider delivery log, 直到邮箱收件读回完成.
  • 任何恢复都不得顺带启用 incoming email、复制 IMAP credential 或部署 Mailroom.

CI 发布与 ApplySet 恢复

故障排查

平台服务使用管理员 namespace 发布; 普通用户 namespace 的 HTTPRoute hostname 必须以 namespace 开头. admission 因 hostname/namespace 不匹配而拒绝 Route 时, 不能只修改变量后重试, 因为 kubectl apply --applyset 不是原子的: 排在 Route 前面的 Deployment、Service 和 ApplySet parent Secret 可能已经创建.

恢复时先从 job trace 取得 applyset 名称和失败对象, 再在原 namespace 中盘点带有该 ApplySet 标识的资源. 只删除本次失败创建的命名对象和 parent Secret, 确认没有旧版本 业务资源被误包含后, 修正 namespace/hostname 并重试原发布任务. 最终验收必须同时满足:

namespace=NAMESPACE
name=APPLICATION_NAME
kubectl -n "$namespace" get secret \
  -l applyset.kubernetes.io/id -o name
kubectl -n "$namespace" get deployment,service,httproute,secret \
  -l applyset.kubernetes.io/part-of -o name
kubectl -n "$namespace" get "deployment/$name" "service/$name"
kubectl -n "$namespace" get "httproute/$name" -o yaml
kubectl -n "$namespace" get pods -l "app.kubernetes.io/name=$name" -o wide

不要对 applyset.kubernetes.io/part-of 的全部结果直接执行批量删除; 先把 label 值、 parent Secret 和 job 中的 applyset 名称对应起来, 再逐个删除确认属于失败发布的对象.

  • Deployment 使用目标 commit 对应的不可变镜像且 Ready;
  • HTTPRoute 的 parent 同时为 Accepted=True 和 ResolvedRefs=True;
  • 公网 HTTPS 返回预期内容;
  • 失败 namespace 中不再残留同一发布的 workload 或 ApplySet parent.

BuildKit 成功 push 镜像不证明目标 Pod 有 pull 权限. 私有项目应配置最小权限的 Registry credential 和 imagePullSecret, 并从 Pod event 验证认证结果. 把项目改为 Public 是独立的安全决策, 只能在源码和镜像本来就允许公开且得到明确批准时采用.

GitLab CI/CD 变量需要隐藏时, 创建阶段使用当前 API 支持的 masked_and_hidden=true; masked=true 只保证 job log 脱敏, hidden=true 在部分 版本中不会得到相同的 API metadata. 创建后只读回 key、protected、masked、hidden 等 metadata, 不读取 value.

SECoder 使用

空数据库第一次启动时会创建 SECoder root 用户, 初始密码是 root. 平台对外开放前必须立即登录并修改密码, 然后确认初始密码已经返回 401. 如果保留或恢复了原来的 PVC, 则现有密码不会被 bootstrap 覆盖; 不要删除数据库来 “重置”密码.

故障排查

集群重建后, 旧的用户 kubeconfig 即使尚未到期也不能继续使用, 因为 token 的 签名密钥和绑定对象属于旧集群. 以 root 登录 SECoder 后, 从个人页面重新获取 RBAC token/kubeconfig, 并通过公网 Kubernetes API 验证:

chmod 600 u-root.kubeconfig
kubectl --kubeconfig ./u-root.kubeconfig get namespace u-root
kubectl --kubeconfig ./u-root.kubeconfig auth can-i '*' '*' --all-namespaces
kubectl --kubeconfig ./u-root.kubeconfig get nodes

u-root 应带有正确的 SECoder tenant label, root 的绑定应指向预期的 cluster-admin role. 普通用户还要验证只能访问自己的 namespace 和所在小组 namespace.

用户注册名单和小组名单的日常维护, 见独立章节 《批量添加用户和小组名单导入》. 该章节包含批量用户 文件格式、全量 CSV 约束、预览确认流程以及常见验证错误的处理方式.

SonarQube

同样, 登录 admin 用户, 密码 admin. 第一次登录后会要求改密码. 新密码不仅有长度要求, 还必须同时满足 SonarQube 当前版本显示的大小写字母、数字、 特殊字符等复杂度规则; 纯十六进制随机串可能被拒绝.

如果 Community Edition 使用持久化内嵌 H2, 这只是单实例、非高可用方案. 必须持久化 SonarQube 数据目录并记录备份/恢复边界; 生产规模增长后应迁移到 受支持的外部数据库. Pod Ready 不能替代重启后的项目、分析历史和权限验证.

打开 https://sonar.t.secoder.net/admin/settings, 设置 sonar.core.serverBaseURL 为 https://sonar.t.secoder.net.

打开 https://sonar.t.secoder.net/admin/projects_management,

修改 Default visibility of new projects 为 Private.

打开 https://sonar.t.secoder.net/admin/permission_templates,

修改 Default Permission Template, 将它改成如下的设置:

GroupBrowseSee Source CodeAdminister IssuesAdminister Security HotspotsAdminister ArchitectureAdministerExecute Analysis
sonar-administratorsYesYesYesYesYesYesYes
sonar-usersYesNoNoNoNoNoNo
CreatorsYesYesYesYesYesYesYes

然后为 GitLab 配置登录:

打开 https://sonar.t.secoder.net/admin/settings?category=authentication&tab=gitlab, 参照 SonarQube 线上文档配置 GitLab 作为鉴权提供者. GitLab 那里依次填写 https://sonar.t.secoder.net/oauth2/callback/gitlab (redirect URL), Confidential, api. 然后在 SonarQube 那里配置 Application ID, GitLab URL 为 https://gitlab.t.secoder.net, 启用 Synchronize user groups. 填完这些后, 记得启用 Allow users to sign up, Allowed groups 留空. 这表示任何能够通过该 GitLab 鉴权的用户都可进入 SonarQube, 当前版本会显示高风险确认对话框; 只有明确接受该暴露边界时才确认, 并在保存后执行一次全新会话 GitLab OAuth 登录.

接下来打开 https://sonar.t.secoder.net/admin/settings?category=almintegration&alm=gitlab, 允许登录到 SonarQube 的用户从 GitLab 中导入项目. GitLab API URL 填写 https://gitlab.t.secoder.net/api/v4, token 使用具有 api 权限的 Personal Access Token (可以复用之前的). 保存后必须看到 Configuration valid, 届时使用 SECoder 的学生将独立导入他们的项目.

SonarQube 故障恢复

故障排查

  • Pod Ready 但重启后项目或分析历史消失时, 立即停止新的初始化操作, 核对 PVC、挂载 目录和 H2 文件是否来自预期持久卷. 不要创建新 PVC 来掩盖旧数据未挂载.
  • GitLab OAuth redirect 失败时, 先保留 SonarQube 本地 admin 登录, 对照 callback URL、 Application ID/Secret、GitLab base URL 与实际 Route; Secret 修正后重启读取它的 SonarQube workload, 再用全新浏览器会话验收.
  • GitLab DevOps integration 不是 OAuth 登录配置. Configuration valid 失败时, 单独检查 API URL 是否以 /api/v4 结尾、PAT 是否具有 api scope、TLS/DNS 可达性和 token 是否有效. OAuth 能登录不代表项目导入 API 已配置成功.

批量添加用户和小组名单导入

本章面向课程助教和系统管理员, 说明如何批量开放学生注册, 以及按课程名单 一次性维护小组、组长和成员关系. 这些操作需要使用带有 sudo: true 的管理员 账号, 在 SECoder 的 管理员 页面完成.

操作前检查

  1. 使用管理员账号登录 SECoder, 通过侧边栏进入 管理员 页面.
  2. 确认课程名单中的学生学号准确无误, 并准备好每位学生的初始密码.
  3. 如果平台启用了 只读模式, 批量添加用户和应用小组名单都会被禁止. 小组 名单仍可以上传并预览, 但必须先关闭只读模式才能应用.

建议先批量添加用户, 等学生完成注册并在用户访问列表中显示为 已注册 后, 再导入小组名单. 小组名单导入只接受已经注册、未封禁且非 sudo 的学生.

批量添加用户

批量添加用户只会把学号加入注册访问名单, 不会直接创建学生账号. 学生仍需使用 该学号和初始密码自行注册.

准备用户文件

创建 UTF-8 编码的纯文本文件, 每行一名学生, 格式为:

学号:初始密码

例如:

20260001:initial-password-1
20260002:initial-password-2
20260003:initial-password-3

文件处理规则如下:

  • 空行会被忽略.
  • 没有冒号, 或冒号前后任一部分为空的行会失败.
  • 文件中的每条记录会独立提交; 某条记录失败不会撤销其他已经成功的记录.
  • 已在注册访问名单中但尚未注册的学号会更新初始密码, 同时清除封禁状态.
  • 已经注册的用户不能通过此操作重复添加; 如果只是要恢复其访问权限, 使用用户 列表中的 解封用户 操作.

上传文件

在 管理员 页面的 用户访问控制 区域:

  1. 点击 批量添加用户.
  2. 选择准备好的纯文本文件.
  3. 等待页面显示处理结果.
  4. 检查 success 和 failed 数量. 对失败的学号修正文件或核对用户状态后, 单独重新处理.

处理结束后, 用户访问列表会刷新. 列表中的 允许 表示该学号可以注册; 已注册 表示学生已经完成账号注册. 只有允许注册不代表学生已经加入小组.

导入小组名单

小组名单导入是全量同步操作, 不是只追加某几个成员的增量操作. 文件必须同时 包含现有小组和所有需要纳入名单的学生; 系统会根据文件计算创建小组、修改名称、 转移组长以及调整学生成员关系.

CSV 格式

创建 UTF-8 编码的 CSV 文件. 表头必须精确使用 CodeName、DisplayName、 Leader 和连续的 Member1、Member2 等列, 至少包含 Member1:

CodeName,DisplayName,Leader,Member1,Member2,Member3
team-a,第一组,20260001,20260002,20260003
team-b,第二组,20260004,20260005,
,,,20260006,

每列含义和填写规则:

  • CodeName 是小组不可变且唯一的标识符. 必须是规范的 RFC 1035 名称: 使用小写 英文字符、数字和连字符, 长度不超过 63, 且不能以连字符开头或结尾.
  • DisplayName 是显示给用户的小组名称. 小组行必须填写; 已存在的小组可以通过 修改该列来重命名.
  • Leader 是组长学号. 组长会自动计入该小组成员, 不要在 Member 列中重复填写.
  • Member1、Member2 等列填写其他成员学号. 列名必须连续, 不能跳过编号.
  • CodeName、DisplayName 和 Leader 同时为空的行表示未分组学生. 这类行最多 一行, 学生学号填写在 Member 列中.

导入前必须满足以下完整性要求:

  • 每个现有小组都必须在 CSV 中出现; 不存在的 CodeName 会创建为新小组.
  • 每个已注册、未封禁、非 sudo 学生都必须出现且只能出现一次, 可以出现在某个小组 或未分组行中.
  • 不能填写未注册、已封禁或 sudo 账号.
  • 同一个 CodeName、学生学号或组长不能在名单中重复出现.
  • 小组行必须同时有 DisplayName 和 Leader.

上传、预览和应用

在 管理员 页面的 小组名单导入 区域:

  1. 点击 上传并预览 CSV, 选择名单文件.
  2. 先处理页面显示的验证错误. 页面会指出错误所在的行、列和原因; 文件验证 不通过时不能应用任何变更.
  3. 验证通过后, 检查 已验证的变更 区域, 重点核对新建小组、重命名、组长变更、 学生变更和取消分组数量, 以及下方的具体变更列表.
  4. 确认预览内容与课程名单一致后, 点击 应用名单.
  5. 在确认对话框中再次确认. 系统会在一个数据库事务中应用小组名称、组长、学生 成员关系和相关邀请.

应用成功后, 系统会刷新用户访问列表, 并同步小组 Kubernetes namespace 的 tenant label. 如果 label 同步失败, 页面会列出受影响的小组; 重新预览并应用同一份 CSV 即可重试. 如果预览后数据库中的名单发生变化, 应重新上传 CSV 获取新的预览后再应用.

小组名单导入不会自动修改 GitLab 子组成员. 学生仍需在个人资料页面点击 同步 GitLab 子组 来触发个人 GitLab 权限同步.

推荐工作顺序

  1. 用 批量添加用户 上传课程学生的学号和初始密码.
  2. 通知学生完成注册, 在 用户访问控制 列表核对注册状态.
  3. 根据已注册学生生成包含全部小组和未分组学生的完整 CSV.
  4. 上传并预览 CSV, 逐项检查变更摘要和明细.
  5. 确认无误后应用名单, 再抽查用户访问列表和小组成员关系.

名单导入是全量操作. 后续调整名单时, 仍应上传包含全部现有小组和全部在用学生 的最新完整文件, 不要只上传新增成员的片段.