写 Scala 服务的人大多用 sbt 构建。sbt 的配置本身就是 Scala 代码,依赖解析和增量编译也比较稳定。真正麻烦的通常是把项目搬到线上的那几步。把 sbt 项目放到阿里云上跑,要先解决三件事:依赖下载够不够快,镜像怎么打、怎么发,以及线上和本地的运行环境能不能对齐。下面以一个订单服务为例,按顺序讲一遍。

案例背景
某电商团队用 Scala 和 http4s 写了一个订单服务,代码放在公司内网的 Git 仓库里。以前的发布方式很原始:开发人员在本地执行 sbt assembly 生成 fat jar,再手动传到服务器上运行。时间一长,问题就堆出来了。
第一个是 JDK 版本对不上。本地是一个版本,服务器是另一个,出过几次“本地能跑,线上报错”的情况。第二个是依赖经常下载超时,新人搭环境要折腾半天。第三个最麻烦:没有发布记录,线上出了问题,没人能确定当前跑的是哪一版代码。于是团队决定把整条链路迁到阿里云上。
第一步:给依赖下载加速
sbt 默认从 Maven Central 拉依赖。国内访问这些地址时速度时快时慢,首次构建尤其明显。阿里云提供了 Maven 公共仓库的镜像,最直接的做法是在 build.sbt 里加一条 resolver:
resolvers += “aliyun” at “https://maven.aliyun.com/repository/public”
sbt 会依次查询这些仓库来找依赖。如果希望本机所有项目都用上镜像,可以研究一下 ~/.sbt/repositories 文件。需要注意,这个文件主要影响 sbt 启动器和插件的解析,具体行为和是否需要额外参数,以 sbt 官方文档为准。镜像地址也可能变动,上线前先在本机完整跑一次依赖解析,确认没问题再推给团队。团队在测试环境里做过对比,首次依赖解析的耗时有明显下降。[待补充实测数据]
第二步:把构建环境固定下来
构建结果不一致,多数时候是工具版本不同造成的。团队做了三处固定,每处都尽量只需要改一个地方:
- sbt 版本:在 project/build.properties 中写入 sbt.version,填具体版本号,不要用范围写法。这样所有人拿到的 sbt 都是同一个。
- JDK 版本:在项目根目录的 .tool-versions 里声明 JDK 17。这个文件是 asdf 的约定,本地装了 asdf 才会自动读取。CI 镜像里如果没有 asdf,就要在流水线配置里写同样的版本,两边保持一致。
- 插件版本:所有 addSbtPlugin 都集中写在 project/plugins.sbt 中,同样使用固定版本。升级时只改这一个文件,diff 也一目了然。
第三步:用 sbt 直接生成容器镜像
团队没有继续用 fat jar,改用 sbt-native-packager 直接生成 Docker 镜像。好处是打包和镜像定义都在 sbt 里完成,不用再单独维护一份 Dockerfile。先在 project/plugins.sbt 里加入插件,版本号同样要填固定值:
addSbtPlugin(“com.github.sbt” % “sbt-native-packager” % “1.10.x”)
然后在 build.sbt 里配置镜像信息。镜像仓库指向阿里云容器镜像服务 ACR 的命名空间,版本号从环境变量读取,由 CI 注入 Git 提交的短哈希。这样每个镜像都能对应到一次提交:
enablePlugins(JavaAppPackaging, DockerPlugin)
dockerBaseImage := “eclipse-temurin:17-jre”
Docker / packageName := “order-service”
dockerRepository := Some(“registry.cn-hangzhou.aliyuncs.com/your-namespace”)
version := sys.env.getOrElse(“IMAGE_TAG”, “dev”)
执行 sbt Docker/publish 就能构建并推送镜像。推送前要先登录 ACR,登录凭证必须由 CI 的加密变量注入,不能写进代码或 build.sbt,也别让它出现在构建日志里。CI 执行环境还需要能访问 Docker 守护进程,否则这一步会卡在打包阶段。
第四步:CI 流水线与 ACK 部署
流水线分三个阶段,部署目标是阿里云容器服务 ACK 集群。顺序不能乱,前一步失败就停下来:
- 运行 sbt test。测试失败就终止,不生成镜像,也不发布。
- 运行 sbt Docker/publish,把带提交哈希标签的镜像推送到 ACR。
- 用 kubectl set image 更新 ACK 中 order-service 的 Deployment,镜像指向上一步生成的标签。更新后用 kubectl rollout status 确认新版本真的起来了。
每次发布的镜像标签都对应一次提交。出问题时,用 kubectl rollout undo 就能回到上一个版本,不用临时找人重新打包。发布记录也因此能直接追溯到代码,线上跑的是哪个版本,查一下标签就清楚了。还有一点要提前注意:如果 ACR 命名空间是私有的,ACK 需要配置拉取镜像的凭证(imagePullSecrets),否则 Pod 会一直卡在拉取镜像的状态。
常见问题与处理
- 依赖冲突:先用 sbt evicted 看哪些版本被覆盖了,再在 dependencyOverrides 里明确指定需要的版本。不要只靠它“碰巧能跑”。
- 镜像体积过大:基础镜像用 JRE,不用完整 JDK,并去掉运行时用不到的依赖。团队的镜像体积确实小了不少,具体数字待补充。
- CI 构建变慢:缓存 ~/.cache/coursier 和 ~/.sbt 目录,避免每次都重新下载依赖。缓存 key 最好绑定 build.sbt 和 project/ 下文件的哈希,依赖变了,缓存才会失效。
- 凭证泄露:ACR 访问凭证和 ACK 的 kubeconfig 都放在 CI 的密钥管理里,并定期轮换。如果日志里出现过这些值,按泄露处理,直接换掉。
这条路径从依赖下载一直走到 ACK 发布。换到别的 Scala 服务上,大体可以照搬,主要改的是镜像命名空间、集群信息和环境变量名。哪一步卡住,先看它属于哪个阶段,通常能很快定位。