Generating CI/CD pipelines for microservices with Yeoman
TL;DR: You can create a Yeoman generator for your CI/CD pipelines. It's good, I tried.
Once you start multiplying services, you hit a problem of increased effort spent on non-business logic. With every new application you need to setup all the boilerplate: initial project, dependencies, running tests, building and deploying everything. Most of this things are very similar from project to project but they differ in details so you can copypaste a lot but you need to do it carefully so you don't break a lot of stuff by forgetting to update one line in a copypasted file. This needs a better solution.
When we faced this problem in the team, the first thing we did it to find out constraints for the solution:
- It should work for projects with different programming languages because we have several of them.
- No big dependencies, every project should still be on it's own.
- Not a one-time fire-and-forget thing. We sometimes have tasks that require updating every projects (like migrating to a new Kubernetes cluster).
And the solution was to use a code generation tool. We decided not to generate the project boilerplate because it makes no sense. You don't want to spend more effort on it than the maintainers of create-react-app do. Instead we can generate all the CI pipeline-related code, the thing that's unique to our team and in the same time it's quite similar between projects.
We use GitLab CI and our CI/CD logic looks like this: jobs defined in
.gitlab-ci.yaml run commands defined in
Makefile. That's great for splitting generated and project-specific code because both
Makefile files support including external files.
Imagine this: generator puts all the boilerplate into
/cicd/ directory (we assume that all the code in that directory is generated). All the project-specific stuff is still stored in
Makefile files in the project's root directory but also it includes things from
cicd. This approach separates all the common CI pipeline parts from the project specific code bus still keeps them in project repository. And if our generator is good enough, we can make any project buildable and deployable with a single console command run.
Yeoman provides you some helpers for this:
- Composable subgenerators. Projects in different programming languages will have different CI jobs for testing but the parts like "publish a docker image" or "throw a docker image into Kubernetes" will be almost same. Yeoman allows you to split these things into multiple sub-generators and run only the ones you need in this specific project.
- Regeneration. Most project seed generators create only initial project state to be updated manually later. That's ok for application source code but for CI pipeline we have cases when we need to update it in every existing project. With yeoman you can regenerate code multiple times and let user decide how to handle conflicts.
- Interactive user prompts. You can ask user for some parameters using a rich library for that.
- Config storing. You can store user answers (or something else) so you don't ask it again on next generation.
- Template engine. The main part of every code generator is a template engine. Yeoman is using EJS for that. It's provides you a lot of useful stuff like conditions, loops, and includes.
- File utilities. Besides just copying files from template to destination of saving rendered files you can also edit existing project files with code if you need to.
And the thing that your generator is a code allows you to add logic of any complexity. For example: we want to ask user for desired node version for docker image. We can make an http call to download a json listing which node versions are there. Show input with choices options of currently supported versions + mark the latest "lts" version as a default option. Such stuff is super useful in practice but impossible when all you have is a template engine.
Thanks to that generator deploying a new "Hello World" project takes one console command run so there's no temptation to put completely new logic into existing project anymore. At the same time we can still add unique CI jobs to every project and there's no additional dependencies: if our generator is removed, no existing project will notice it.
How do you manage CI pipelines for microservices? Let me know in the comments.