3 ms·
The key to doing migrations on the request is having the migrations live in single classes/files. It makes it very clean to have the app migrate from version 3
by tomschlick 5y ago
The key to doing migrations on the request is having the migrations live in single classes/files. It makes it very clean to have the app migrate from version 3 to version 12 with the 9 or so classes handing the response from one to the other.
I made a package a few years ago in PHP/Laravel that focuses on this: https://github.com/tomschlick/request-migrations https://github.com/tomschlick/request-migrations
- derefr 5y agoOr doing the migrations as load-balancer-side request rewrites. (Nginx is particularly good for this use-case; it’s what sets it apart from simpler LBs like HAProxy.) Then the cruft of old versions doesn’t have to live in your codebase at all, but can live in the same infrastructure-config repo that holds your backend-service-unifying route-map; your SSL config; your rate-limiting setup; etc.
- tomschlick 5y agoMy main concern there would be the management and orchestration of the releases for that since its detached from the codebase that is changing. It sounds very performant though. Have any examples of this setup?
- derefr 5y agoI don’t, but that’s because I keep my own infrastructure config living inside the same repo the code lives in. We do k8s GitOps with [a simulacra of] Google Kubernetes Engine’s “Application Delivery” (https://cloud.google.com/kubernetes-engine/docs/concepts/add-on/application-delivery https://cloud.google.com/kubernetes-engine/docs/concepts/add...). In this approach, you keep all your k8s manifests relating to the app in the app’s repo, and then the app is “released” by running a command that does the following: 1. tags a particular commit, which locks down the release as being of a particular codebase + a particular target converged k8s state; 2. generates a Docker image, tags it with the commit tag, and pushes it; 3. makes a copy of the manifests; 4. burns the build-image’s SHA into the copy of the manifests; 5. compiles the source manifests (which are using Kustomize) into a single static manifest, which is a complete definition of the new target converged cluster state for the app's k8s namespace; 6. commits that static manifest to a separate tagged “deployment” repo. A separate "deploy" command is then used to reach out to a cluster-side converger component and tells it to pull a particular commit from the "deployment" repo and converge to it. ----- We do have subcomponents that live outside this repo, though; we manage them by: 1. running a “release” within the subcomponent repo (which does create a git tag + push a build-image tagged with that tag; but doesn’t generate/commit any k8s manifests, since the subcomponent repos don’t have any k8s config of their own); and then 2. manually (for now) taking the git tag of the built image, and updating the main app repo’s k8s per-env Kustomization.yaml file with it, i.e. updating a stanza like this with a new value for "newTag": images: - name: gcr.io/our-project/subcomponent newTag: v20210409094011 In theory, our main component could also be tracked as a subcomponent in this manner, such that all our infra config would live in its own basically-empty "app" repo; but that would lose one of the main advantages of GitOps, which is being able to see from the git log exactly what went into a deployed release, both in terms of build-image and infra-config. Our subcomponent services don't evolve nearly as quickly as our main app does, so we don't lose much in the way of release comprehensibility by having them "symbolically linked" to the release like this. As it is, though, our app's LB config for api.example.com lives as a k8s Ingress manifest at /.appctl/config/base/unified-api/ingress.yaml within our app repo. (We're using ingress-nginx, so there's a pretty direct 1:1 mapping between this manifest and an Nginx server{} block.)
- gregmac 5y agoI've done this in a codebase as well, and it worked quite well. The "main" code was always updated to be the latest stuff, and the old methods were moved to a class named like `WhateverApi_v1_0` (based on the last version it was available in). A routing engine picked up the proper controller based on the requested API version. The other thing we had to help make this easier was automated API "shape" tests. These are per-version tests that basically just call every API method and check that the response matches a specific schema. For every release, we made a new directory containing a copy of all the tests from the last version, and then never touched any of the old directories. If an API ever changed in a backwards-incompatible way, one of these tests would catch it. All this was relatively painless to maintain, and we also never had any API regression issues over dozens of releases spanning years. I'll note we did sometimes "cheat" and add new properties to an existing model (without making the backwards-compatible controller stuff), but this doesn't break any consumers because of the nature of JSON (at least we never had anyone complain about it, and I'm not aware of any language where that would happen).
- tomschlick 5y agoYup thats exactly what the package I linked does as well and it has worked without a hitch on the project I have implemented it on. All you have to do to test a specific version is send that header in the integration test and its good to go.