Testcontainers is awesome and all the hate it gets here is undeserved.
Custom shell scripts definitely can't compete.
For example one feature those don't have is "Ryuk": A container that testcontainers starts which monitors the lifetime of the parent application and stops all containers when the parent process exits.
It allows the application to define dependencies for development, testing, CI itself without needing to run some command to bring up docker compose beforehand manually.
One cool usecase for us is also having a ephemeral database container that is started in a Gradle build to generate jOOQ code from tables defined in a Liquibase schema. suspend fun myFunction(param: String) {
val asyncVal = someSuspendingFunction()
val asyncVal2 = anotherSuspendingFunctio(asyncVal)
return someCalculation(asyncVal2)
}
Into a modified method and *class* like that: // The different parts of the function get put into different ifs
// The state that is normally in the JVM stack frame is holded in a custom class which is a subclass of https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-continuation/
// Some integer tracks where in the function we are
fun myFunction(param: String, continuation: Something<MyFunctionClass>?) {
continuation = continuation ?: MyFunctionContinuation(param)
if (continuation.state == 0) {
contination.state = 1
someSuspendingFunction(continuation)
return SUSPEND
}
if (continuation.state == 1) {
// The result mechanism is in the Contiuation interface/abstract class. It can be used to later receive the async result
// of callig annother suspending function
continuation.asyncVal = contiuation.getResult()
anotherSuspendingFunction(continuation.asyncVal, continuation)
return SUSPEND
}
if (continuation.state == 2) {
continuation.asyncVal2 = contiuation.getResult()
return FINISHED(someCalculation(continuation.asyncVal2))
}
}
class MyFunctionContinuation: Continuation {
// Function arguments
val param: String
// Every suspending function needs this. It tracks where in the function we currently are
val state: Int = 0
// Local variables inside the function. Those would normally live in the JVM function stack
// But the Coroutine runtime needs to be able to "rehydrate" the JVM function stack once a non-blocking call finishes
var asyncVal: <Don't remember>
var asyncVal2: <Don't remember>
}
I hope the basic idea comes across.
It makes absolutely no sense to base this decision on the number of users. We have some applications that don't even have 10 users but still use k8s.
Try to understand the point that was made in the original comment: Kubernetes is a way to actually make infrastructure simpler to understand for a team which maintains lots of different applications, as it can scale from a deployment with just one pod to hundreds of nodes and a silly microservices architecture.
The point is not that every application might need this scalability, no the point is that for a team needing to maintain lots of different applications, some internal, some for customers, some in private datacenters, some in the cloud, Kubernetes can be the single common denominator.
Hell, I'm a hardcode NixOS fan but for most services, I still prefer to run them in k8s just as it is more portable. Today I might be okay having some service sitting on some box, running via systemd. But tomorrow I might want to run this service highly available on some cluster. Using k8s that is simple, so why not do it from the start by just treating k8s as a slighly more powerful docker-compose.