You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I work on something similar to a "platform" team for 130 Ruby on Rails developers. We run and provide CI (via self-hosted Github Actions) for the applications that other developers build.
27
+
I work on something similar to a "platform" team for 130 Ruby on Rails developers. We run and provide continuous integration (CI) via self-hosted Github Actions for the applications that other developers build.
28
28
29
29
{: .box-note .ignore-blockquote }
30
30
31
31
<!-- prettier-ignore -->
32
32
>**A bit more background**\\
33
-
> To go a bit more in depth all the compute is handled by several Kubernetes clusters and the actual code lives in multiple repositories all under the same organization. Thus, a lot of the challenges we face are around providing flexibility for our developers without implementation details leaking out to them.
33
+
> All the compute is handled by several Kubernetes clusters while the actual code lives in multiple repositories all under the same organization. Thus, a lot of the challenges we face are around providing flexibility for our developers without implementation details leaking out to them.
34
34
35
35
Our apps all follow a fairly similar pattern:
36
36
37
+
- they have their own github repository under our organization
37
38
- they have a `Dockerfile`
38
-
- get built as an image
39
-
- get served via Kubernetes.
39
+
- get built as an docker container image (my terminology might be wrong here)
40
+
- get served via Kubernetes
40
41
41
42
However, despite serving everything as containers our developers don't actually develop in containers. So to avoid the issue where production is the first place that our apps see containerized use we run all our tests in containers.
42
43
43
44
## How Do We Run CI?
44
45
45
-
Basically all our apps have a structure like so:
46
+
All our apps have this general structure:
46
47
47
48
```plaintext
48
49
.
50
+
├── app_code
51
+
├── ...
49
52
├── .github
50
53
│ └── workflows
51
54
│ └── main.yaml
@@ -56,9 +59,9 @@ Basically all our apps have a structure like so:
56
59
└── docker-compose.test.yaml
57
60
```
58
61
59
-
The`Dockerfile`is nothing worth mentioning, it just sets up our application with whatever dependencies it needs (gems, node modules, etc).
62
+
Our`Dockerfile`largely follows [the default generated by Rails](https://github.com/dylhack/rails/blob/main/railties/lib/rails/generators/rails/app/templates/Dockerfile.tt). The difference are mainly in the certs that we add to the image.
60
63
61
-
The scripts in `bin` as just basic bash scripts that represent the testing process, linting process or whatever. For example:
64
+
The scripts in `/bin` as just basic bash scripts that represent the testing process, linting process or whatever. For example:
62
65
63
66
```bash
64
67
# bin/ci-lint
@@ -67,7 +70,7 @@ bundle exec brakeman
67
70
bundle exec rubocop
68
71
```
69
72
70
-
The idea is that these are the same scripts that a developer could run locally on their machine as in CI.
73
+
The idea is that these are the same scripts that a developer could run locally on their machine. Reusing the scripts ensure they are the source of truth for both local and CI processes.
71
74
72
75
The `docker-compose` looks something like this:
73
76
@@ -93,13 +96,14 @@ services:
93
96
retries: 5
94
97
```
95
98
96
-
Running `docker compose up` locally builds the image, starts Postgres, then runs the `command` in a container.
99
+
Running `docker compose up` locally builds the image, starts Postgres, then runs `bin/ci-some-command` in against the container with the app. We specify a default command but docker compose allows us to override the command when called via command line.
97
100
98
-
This translates nicely to CI because the developers are able create arbitrary scripts and those scripts will run in CI the same way as locally. For example if a particular app wants to use Cypress they can add that to the existing script or create a new script for it.
101
+
CI runs tests via the same docker compose and an commands that a developer would run locally. Lets take a look at that.
99
102
100
103
## Getting It All Set Up in Github Actions
101
104
102
105
So how does this all look as an actions workflow?
106
+
103
107
{% raw %}
104
108
105
109
```yaml
@@ -139,10 +143,10 @@ jobs:
139
143
uses: our_org/actions/ruby-rspec-report@v4
140
144
```
141
145
142
-
Largely it looks something like:
146
+
To sum it up:
143
147
144
-
1. we pull down the repo
145
-
2. set up docker
148
+
1. we pull down the repo (checkout)
149
+
2. set up docker (login, setup buildx, bake)
146
150
3. run the test in docker compose
147
151
4. print a report.
148
152
@@ -159,33 +163,29 @@ Largely it looks something like:
159
163
160
164
{% endraw %}
161
165
162
-
The `DOCKER_RUN_CMD` here is simply to all passing in commands to run other `bin` scripts (ex: `bin/ci-lint`and `bin/ci-rspec`). I omitted showing the potential inputs that this action might receive.
166
+
As mentioned before, the command in the `docker-compose` file can be override. The `DOCKER_RUN_CMD` here is passing in commands to run other `bin` scripts (ex: `bin/ci-lint`and `bin/ci-rspec`). I omitted showing the potential inputs that this action might receive.
163
167
164
168
## Benefits and Drawbacks
165
169
166
170
### Benefits
167
171
168
-
The biggest benefit of this approach (and the one that makes it pretty much mandatory) is that our applications see containerized usage before landing in some higher environment (staging, qa, etc). The majority of our developers simply don't develop in docker, this, given that the final product is served in a container, testing in docker is the next best thing.
172
+
**Shifting Docker Left**: The majority of our developers don't develop in docker. Thus, for our apps to see any containerized usage before the apps hit staging or production we need to run tests in docker. This makes docker in CI a necessity.
169
173
170
-
Since our testing happens by starting docker compose and running a bash script with test commands the entire process is very platform agnostic. We don't have to worry about how do you set up ruby on GitHub Actions vs Jenkins vs CircleCI, then repeat for node, etc. We set up docker and that's it.
174
+
**Platform Agnosticism**: Since our testing happens by starting `docker compose` and running a script with test commands the entire process is platform agnostic. We don't have to worry about differences in ruby / node / etc on GitHub Actions vs Jenkins vs CircleCI. We set up docker and that's it. And if we are protected if we ever need to switch to another provider.
171
175
172
176
### Drawbacks
173
177
174
-
The main drawback (and I mean pure drawback not tradeoff) is performance overhead. This comes in two forms: container overhead and time to build the container.
175
-
176
-
In terms of the container overhead, running software directly in the runner will always be faster than running it with docker compose on the runner. Does it really matter? Not really.
177
-
178
-
The real problem we have encountered is the slow speed in building the images. The P90 of the setup step is 15 minutes on our biggest app (when all the layers miss the cache). Fixing this is still a work in progress.
178
+
**Performance Overhead**: In terms of the container overhead, running software directly in the runner will always be faster than running it with `docker compose` on the runner. Does it really matter? Not really.
179
179
180
-
### Shifted Complexity
181
-
182
-
We have moved the complexity around within the actions jobs. Without docker there might be complexity in ensuring all required packages are present to test the app. With docker you get that for free but instead you have to worry about setting up and building the image. This made sense for us.
180
+
**Container Build Overhead**: Building images is slow. The P90 of the setup step is 15 minutes on our biggest app (when all the layers miss the cache). Fixing this is still a work in progress.
183
181
184
182
## Conclusion
185
183
184
+
We have moved the complexity around within the actions jobs. Without docker there might be complexity in ensuring all required packages are present to test the app. With docker you get that for free but instead you have to worry about setting up and building the image. This made sense for us, it might not for you.
185
+
186
186
I left a lot of the detail out of my summary but we have about 25 web apps under us of varying sizes that we support this way. Beyond the initial setup the files in each app repository stay fairly static and developers largely get to ignore them. CI usually just works.
187
187
188
-
That was a super brief overview of our CI and in a future post I will go a bit more in depth on how we provide a few cool features namely:
188
+
That was a super brief overview of our CI and in future posts I will go a bit more in depth on how we provide a few cool features namely:
189
189
190
190
- variably size parallelization of runs via input: `runner-count: 8`
191
191
- providing arbitrary commands to get run via CI: `[{"cmd": "some-command"}]`
0 commit comments