Skaffold can build independent services from a multi-module monorepo, but it’s not always obvious how to configure it without building everything every time.
Let’s say you have a monorepo structured like this:
monorepo/
├── services/
│ ├── service-a/
│ │ ├── src/
│ │ ├── pom.xml
│ │ └── Dockerfile
│ ├── service-b/
│ │ ├── src/
│ │ ├── pom.xml
│ │ └── Dockerfile
│ └── service-c/
│ ├── src/
│ ├── pom.xml
│ └── Dockerfile
├── shared/
│ └── utils/
│ ├── src/
│ └── pom.xml
└── pom.xml (root)
You want Skaffold to only build and deploy service-a when you make changes to service-a/, even though service-b and service-c are in the same repository.
Here’s how you’d configure skaffold.yaml to achieve this, focusing on service-a as an example:
apiVersion: skaffold/v2beta25
kind: Config
deploy:
kubectl:
manifests:
- k8s/service-a.yaml # Assuming you have separate manifests per service
build:
local:
push: false
artifacts:
- image: my-registry/service-a
context: services/service-a
docker:
dockerfile: Dockerfile
sync:
manual: {} # Disable auto-sync for simplicity in this example, or configure as needed
# This is the key part: defining how Skaffold knows what to build
buildArgs:
# Example for Maven: If your Dockerfile uses ARG for version or other build info
# VERSION: "1.0.0"
The context field is crucial. It tells Skaffold the root directory from which to execute the build commands and find the Dockerfile. By setting context: services/service-a, you’re telling Skaffold that services/service-a is the working directory for building this specific artifact.
Now, imagine your services/service-a/Dockerfile looks like this:
FROM maven:3.8.5-openjdk-11 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/target/service-a-1.0.0.jar service-a.jar
EXPOSE 8080
CMD ["java", "-jar", "service-a.jar"]
When you run skaffold dev, Skaffold will:
- Detect changes within the
services/service-adirectory (or any files it’s configured to watch within that context). - Change its working directory to
services/service-a. - Execute the build using the
Dockerfilefound atservices/service-a/Dockerfile. - Tag the resulting image as
my-registry/service-a. - Deploy using the specified Kubernetes manifests, referencing the
my-registry/service-aimage.
To handle multiple services independently, you simply add more artifacts to the build section:
apiVersion: skaffold/v2beta25
kind: Config
deploy:
kubectl:
manifests:
- k8s/service-a.yaml
- k8s/service-b.yaml
build:
local:
push: false
artifacts:
- image: my-registry/service-a
context: services/service-a
docker:
dockerfile: Dockerfile
- image: my-registry/service-b
context: services/service-b
docker:
dockerfile: Dockerfile
Skaffold’s intelligent change detection will then monitor the files relevant to each artifact’s context. If you only change files in services/service-b/src, Skaffold will rebuild and redeploy only service-b.
What if you have shared modules?
This is where it gets interesting. If service-a depends on shared/utils, and your services/service-a/Dockerfile (or your build process within it, like mvn package) copies shared/utils, Skaffold needs to be aware of this dependency.
The simplest, though potentially less efficient, way is to ensure your context for service-a is broad enough to include any necessary shared code that the Docker build needs to COPY. If your Dockerfile uses a COPY ../../shared/utils ./shared-utils or similar, and services/service-a is your context, this will work.
However, Skaffold’s primary trigger for rebuilding an artifact is changes within its specified context directory. If shared/utils changes, and it’s outside the services/service-a context, Skaffold won’t automatically trigger a rebuild of service-a based on that change alone.
To handle shared module changes effectively, you have a few strategies:
-
Wider Context (Simple but potentially noisy): If
service-a’sDockerfiledirectly copies code fromshared/utils, you can set thecontextforservice-ato be the monorepo root:context: .. Then, within theDockerfile, you’d specify paths relative to the root, e.g.,COPY shared/utils ./shared-utils.- Pros: Simple to configure.
- Cons: Skaffold will now watch all files in the monorepo root for changes relevant to
service-a. A change inservice-bcould trigger a rebuild ofservice-aif theDockerfileis structured to copy it, which defeats the purpose of independent builds.
-
Multi-Stage Dockerfiles with Shared Build: A more robust approach is to have a multi-stage build where the shared dependencies are built first, and then copied into the service’s build stage. Skaffold’s
contextwould still point to the service, but theDockerfilewould handle the dependency.Example
services/service-a/Dockerfile:# Stage 1: Build shared utils FROM maven:3.8.5-openjdk-11 AS utils-builder WORKDIR /app COPY ../../shared/utils/pom.xml . COPY ../../shared/utils/src ./src RUN mvn clean package -DskipTests # Stage 2: Build service-a FROM maven:3.8.5-openjdk-11 AS app-builder WORKDIR /app COPY pom.xml . COPY src ./src # Copy the built JAR from shared utils COPY --from=utils-builder /app/target/utils-1.0.0.jar ../../shared/utils/target/utils-1.0.0.jar # Path might vary RUN mvn clean package -DskipTests # Stage 3: Final image FROM openjdk:11-jre-slim WORKDIR /app COPY --from=app-builder /app/target/service-a-1.0.0.jar service-a.jar EXPOSE 8080 CMD ["java", "-jar", "service-a.jar"]In this scenario, Skaffold’s
context: services/service-ais still used. Whenshared/utilschanges, the Docker build process itself will pick it up because theDockerfileexplicitly copies it. However, Skaffold’s detection mechanism still relies on changes within thecontext. To make Skaffold aware of the shared module change, you’d need to either:- Include
shared/utilsin Skaffold’swatchconfiguration forservice-a(if supported and desired). - Or, more commonly, if a change in
shared/utilsnecessitates a rebuild ofservice-a, you might structure your build system (e.g., Maven’s parent POM) such that a change inshared/utilsalso modifies a file withinservices/service-a(like a version number inservices/service-a/pom.xml), which Skaffold is watching.
- Include
-
Separate Skaffold Configurations: For truly independent development and CI/CD, you might have separate
skaffold.yamlfiles in each service’s directory, or a top-level one that only builds specific artifacts based on environment variables or command-line arguments.Example
services/service-a/skaffold.yaml:apiVersion: skaffold/v2beta25 kind: Config deploy: kubectl: manifests: - ../../k8s/service-a.yaml build: local: push: false artifacts: - image: my-registry/service-a context: . # Context is now the service directory itself docker: dockerfile: DockerfileYou would then run
skaffold dev -f services/service-a/skaffold.yaml. This is great for feature branches or focused development but can be cumbersome for managing the entire monorepo.
The key takeaway is that Skaffold’s context defines the root for build commands, and its change detection primarily operates on files within that context. To achieve independent service builds in a monorepo, you must carefully define these contexts and understand how your Dockerfile or build scripts interact with shared code.
If you’ve configured multiple artifacts, each with its own context, and you run skaffold dev, Skaffold will watch files in all specified contexts. When a change is detected in services/service-a/src, only the artifact with context: services/service-a will be rebuilt and redeployed.
If you find Skaffold is rebuilding everything when you change one service, double-check your context paths and ensure they are specific enough. Also, verify that your Dockerfile isn’t inadvertently copying files from other services into its build context that Skaffold might then consider part of its dependencies.
The next thing you’ll likely encounter is managing dependencies between these independently built services, especially when one service needs to talk to another that’s also being developed and deployed.