Rethinking serverless with FLAME
fly.io
fly.io
When starting out, these downsides are not really that visible. On the contrary, there is a very clear upside, which is that everything is free when you have low usage, and you have little to no maintenance.
It is only later, when you have built a hot mess of lambda workflows, which become more and more rigid due to interdependencies, that you wish you had just gone the monolith route and spent the few extra hundreds on something self-managed. (Or even less now, e.g. on fly.io)
A question for author: what if not using Elixir?
Can't wait for the deep dive on how that works
You can do it right now even without Flame, just by opening two Elixir nodes, then it's as simple as
```elixir
iex(first_node@localhost)> name = "Santa"
iex(first_node@localhost)> Node.spawn_link(:other_node@localhost, fn -> IO.puts "Hello #{name}" end)
Hello Santa
#PID<1337.42.0>
```
Note that while the string interpolation and `IO.puts` was run on `other_node@localhost`, it still did stdout from the first node- this is because it was the one that called `Node.spawn_link`, making it the 'group leader'. Outside of which stdout it went to, all the work was done in the other node.
From a quick look at the code, this looks the magic line: https://github.com/phoenixframework/flame/blob/main/lib/flam...
By the time things get bad, it's almost time to find a new job, especially if the process was something you introduced a year or more into your tenure and are now regretting. I've seen it with a handful of bad bosses, at least half a dozen times with (shitty) 'unit testing', scrum, you name it.
But what I don't know is how many people are mentally aware of the sources of discomfort they feel at work, instead of a more nebulous "it's time to move on". I certainly get a lot of pushback trying to name uncomfortable things (and have a lot less bad feelings about it now that I've read Good to Great). Nobody wants to say, "Oh look, the consequences of my actions."
The people materially responsible for the Rube Goldberg machine I help maintain were among the first to leave. The captain of that ship asked a coworker of mine if he thought it would be a good idea to open source our engine. He responded that nobody would want to use our system when the wheels it reinvented already exist (and are better). That guy was gone within three to four months, under his own steam.
There's valuable learning (and empathy too) in having to see your own decisions and creations through their whole lifecycle. Understanding how tech debt comes to be, what tradeoffs were involved and how they came to bite later. Which ideas turned out to be bad in hindsight through the lens of the people making them at the time.
Rather than just painting the previous crowd as incompetent while simultaneously making worse decisions you'll never experience the consequences of.
Moving on every 18-24 months leaves you with a potentially false impression of your own skills/wisdom.
This is so true. It's extremely enlightening to watch a design go from design docs to implementation and then finally the maintenance phase. A lot of problems that could happen never do and some unexpected ones pop up along the way.
Do have some empathy for when job markets or life make people move on. I'd like to stay at an employer for more than 1-2 years, but between the layoffs (avoiding them or getting laid off) or the need for a higher salary that often only comes from switching jobs, its not always possible to build tenure.
Frankly its a big issue in the industry at large. I hate interviewing etc. but I'm not going to get paid 20% less because of it. I had to ride the job hopping treadmill for awhile, and I'd like to get off it just as much as you'd like to see people have more tenure.
When I first started out though, I worked for 5 years at the same place until it was very clear I was going to cap out on being able to advance and merit increases of 3-5% a year aren't going to cut it.
I got way more by switching jobs (after 4 years) than I was ever going to get by staying.
It is possible to avoid these traps, but then there are a lot of traps that we collectively have the wisdom to avoid but individually do not.
I started taking things apart and putting them back together at a very young age. When I was a young man I was in a big hurry to get somewhere, and so I could walk into a brownfield project and slowly reconstruct and deconstruct how we got here, would I have made the same decisions with the same information, and how do I feel about this news. Not only was I not falling into the "1 year of experience 10 times" dilema, I got more like 3-4 years of experience 4 times in 10 years, by playing historian.
My first almost-mentor left me with a great parting gift at the start of the dot-com era. He essentially convinced me of the existence of the hype cycle (in '95!), that we were at the beginning of one/several, he had seen previous ones play out, and they would play out again. Not cycles for new things, mind you, but cycles trying to bring back old things that had been forgotten. Like fashion. If anything it made me more likely to want to excavate the site.
Going into the trap knowing it's a trap doesn't necessarily save you, but it does improve your odds. Of course it also makes you a curmudgeon at a tender age.
It can get hard to separate ideas from execution, to 'forget' about the pain of a horrible technical move that still managed to be a commercially success, or to forget the pain of a badly-timed big decision that exploded in your face.
There's only so much interesting code you can add to interesting code, before every single conversation becomes a proxy discussion of The Mythical Man Month - we can't teach anybody new how to use this code in anything like a reasonable time frame. The best we can do is create new experts as fast as the old ones disappear.
Really Interesting Code is more at home surrounded by Really Fucking Boring code. You get so blinded by the implementation details of the Interesting Code that you cannot see the forest for the trees. That is the secret wisdom of Relentless Refactoring. The more I rearrange the pieces the more things I can see that can be made out of them, some of which are better, and a few of which are much better or brilliant.
Last week I implemented something in 2 days that we wanted years ago and didn't do because it would have taken more than a month of 1.5 people working on it to fix. But after a conversation in which I enumerated all the little islands of sanity I had created between Here and There, it took a couple dozen lines of code to fix a very old and very sore problem.
Going from hundreds of lambdas to a monolith is overreacting to one extreme by going the other one. There's a whole spectrum of possible ways to split a project in useful ways, which simplify development and maintenance.
Once you have the flow for deploying always running hot application(s) with autoscaling the benefits of lambda are basically gone.
Low volume scale to zero is just another route. No 15 minute limit, no having to marshal data through other AWS services because that's all lambda talks to, no more eventbridge for cron, no more payload size limits and having to use S3 as buffer, no more network requests between different parts of the same logical app, code deploys are atomic you're either at v1.x or v1.x+1 but never some in-between state.
I really do like Lambda but once you're at the spend where it's the same as some dedicated always-on compute the value drops off.
I've been working on a blog post to show this off for the last couple of weeks:
https://restate.dev/blog/suspendable-functions-make-lambda-t...
Put another way, you can monolith and minimize spend on AWS; it's not either or.
I'm using asp.net these days and even a chunky app published ready to run with optimized ef models starts relatively quickly.
Don’t have strong feelings there. It worked. I did have some issues with upgrading the functions but found the work arounds.
Those "weird assumptions" are what makes the experience wonderful for the happy path. If you use the C#/v4 model, I can't imagine you'd have a hard time. Azure even sets up the CI/CD for you automatically if your functions are hosted in Github.
If your functions need to talk to SQL, you should be using Managed Identity authentication between these resources. We don't have any shared secrets in our connection strings today. We use Microsoft Auth to authenticate access to our HttpTrigger functions. We take a dep on IClaimsPrincipal right in the request and everything we need to know about the user's claims is trivially available.
I have zero experience using Azure Functions outside of the walled garden. If you are trying to deploy python or rust to Az Functions, I can imagine things wouldn't be as smooth. Especially, as you get into things like tracing, Application Insights, etc.
I feel like you should only use Microsoft tech if you intend to drink a large amount of their koolaid. The moment you start using their tooling with non C#/.NET stacks, things go a bit sideways. You might be better off in a different cloud if you want to use their FaaS runners in a more "open" way. If you can figure out how to dose yourself appropriately with M$ tech, I'd argue the dev experience is unbeatable.
Much of the Microsoft hate looks to me like a stick-in-bike-wheels meme. You can't dunk on the experience until you've tried the one the chef actually intended. Dissecting your burger and only eating a bit of the lettuce is not a thorough review of the cuisine on offer.
But Microsoft isn't selling burgers that people are taking a bit of lettuce from. They're selling lettuce, and if that lettuce sucks in any context that isn't the burger that they're also selling, then complaining about the quality of their lettuce is valid.
I think our “best” issue is how sometimes our functions won’t be capable of connecting to their container registry. For no apparent reason. We have an Entra ID group that has Arc Pull access and we add new function slot identities to it automatically, and for the most part it works fine, but then suddenly it won’t.
We currently has a pipeline slot, that is completely the same as the four other slots, which will only deploy a new function version if you deploy it twice through the Azure pipeline. We’ve had Microsoft look at it and their suggestion was to delete the pipeline and create it again.
And that’s just part of it. Then comes the VNETs, the subnets the private endpoints and how both subscriptions and resource groups make everything a tiny battle.
I don’t really mind that much, we’re abandoning it in favour of Azure Container Apps and a fully run Bicep + Dapr pipeline and we’re never looking back.
Though, to be fair to Microsoft, the way they designed Azure Functions makes the migration away from them really, really, easy. Which is frankly a brilliant design for the more managed side of “serverless” in my opinions. It’s just a shame that the managed part just doesn’t work very well. The functions themselves work fine, well maybe not if you don’t use .net in isolation, but I can’t speak about that as we weren’t going to trust Microsoft to update our dependencies (even if they are Microsoft SDKs).
And now I feel (a tiny bit) bad for sniping ffmpeg.fly.dev :)
It’s easy to dismiss these types of articles as straight sales pitches when the problem is presented as “…a fate worse than death.” and the solution so easy and painless, you’d be a fool not to dismiss any other approach! Thats the MO of snake oil sellers.
Instead, I think a more objective comparison with much less editorializing would be less of a turn off for folks curious about a better approach to solve a problem that you did well in outlining at the beginning of the article.
Hopefully you take this as constructive feedback! I think the substance of the article is interesting, I was just turned off by the presentation.
> function and have that block of code run in a temporary copy of your app.
That's interesting, sounds like what fork does but for serverless. Great work
There’s an example here. You’ll see it’s exactly the same model.
https://github.com/picloud/basic-examples/blob/master/exampl...
I’ve not worked with Elixer but I used Erlang a couple of decades back and it appears BEAM hasn’t changed much (fundamentally). My suspicion is that it’s much better suited for this work since it’s a core part of the design. Still, not a totally free lunch because presumably there a chance the primary process crashes while waiting?
We took an alternative approach with https://www.windmill.dev which is to consider the unit of abstraction to be at the source code level rather than the container level. We then parse the main function, and imports to extract the args and dependencies, and then run the code as is in the desired runtime (typescript, python, go, bash). Then all the secret sauce is to manage the cache efficiently so that the workers are always hot regardless of your imports
It's not as integrated in the codebase as this, but the audience is different, our users build complex workflows from scratch, cron jobs, or just one-off scripts with the auto-generated UI. Indeed the whole context in FLAME seems to be snapshotted and then rehydrated on the target VM. Another approach would be to introduce syntax to specify what is required context from what is not and only loading the minimally required. That's what we are currently exploring for integrating better Windmill with existing codebase instead of having to rely on http calls.
This isn't strictly what is happening. FLAME just uses the BEAM's built in clustering features to call a function on a remote node. That implicitly handles transferring only the context that is necessary. From the article:
> FLAME.call accepts the name of a runner pool, and a function. It then finds or boots a new copy of our entire application and runs the function there. Any variables the function closes over (like our %Video{} struct and interval) are passed along automatically.
One issue I see with this scheme still is that you have to be careful of what you do at initialization of the app since now all your background jobs are gonna run that. For instance, maybe your task doesn't need to be connected to the db and as per the article it will if your app does. They mention having hot-modules, but what if you want to run 1M of those jobs on 100 workers, you now have a 100 unnecessary apps. It's probably a non-issue, the number of things done at initialization could be kept minimal, and FLAME could just have some checks to skip initialization code when in a flame context.
> It's probably a non-issue, the number of things done at initialization could be kept minimal, and FLAME could just have some checks to skip initialization code when in a flame context.
Exactly :)
Acknowledging this is brand new; just curious what your thinking is.
EDIT: Would it go in the pool config, and a runner as a member of the pool has access to that?
For example, if I used the heroku api to do the equivalent of ps:scale to boot up more nodes - those new nodes (dynos in heroku parlance) could see what kind of pool members they are. I don't think there is a way to do dyno specific env vars - they apply at the app level.
If anyone tries to do a Heroku backend before I do, an alternative might be to use distinct process types in the Procfile for each named pool and ps:scale those to 0 or more.
Also, might need something like Supabase's libcluster_postgres[1] to fully pull it off.
EDIT2: So the heroku backend would be a challenge. You'd maybe have to use something like the formation api[2] to spawn the pool, but even then you can't idle them down because Heroku will try to start them back. I.e. there's no `restart: false` from what I can tell from the docs or you could use the dyno api[3] with a timeout set up front (no idle awareness)
[1] https://github.com/supabase/libcluster_postgres
[2] https://devcenter.heroku.com/articles/platform-api-reference...
[3] https://devcenter.heroku.com/articles/platform-api-reference...
I love the project's goals; I'm really hoping Windmill becomes a superior open-source Retool/Airtable alternative!
Serverless with a good local dev story. Nice!
So for each “Flame.call” it begins a whole new app process and copies the execution context in?
A very simple solution to scaling, but I’d imagine this would have some disadvantages…
Adding 10ms to the app startup time, adds 10ms to every “Flame.call” part of the application too… same with memory I suppose
I guess these concerns just need to be consider when using this system
For hot runners, the only overhead is the latency between the parent and child, which should be the same datacenter so 1ms or sub 1ms.
Your own mission statement states: "We want on-demand, granular elastic scale of specific parts of our app code." Doing that correctly is fundamentally a question of how long you need to wait for cold starts, because if you have a traffic spike, the spiked part of the traffic is simply not being served until the cold start period elapses. If you're running hot runners with no load, or if you have incoming load without runners (immediately) serving them, then you're not really delivering on your goal here. AWS EC2 has had autoscaling groups for more than a decade, and of course, a VM is essentially a more elaborate wrapper for any kind of application code you can write, and one with a longer cold-start time.
> Under load you are rarely paying the cold start time because the pool is already hot.
My spiky workloads beg to differ.
That is, we both make your existing code available to serverless functions by wrapping with something that, essentially, makes the code callable via remote-RPC .
Some things to consider, which are called out in the blog post:
Often code like this runs in a series of imperative steps. Each of these steps can run in series or parallel as additional lambdas. However, there's implicit state captured in variables between steps. This means that functions become workflows. In the Inngest model, Inngest captures this state and injects it back into the function so that things are durable.
On the note of durability, these processes should also be backed by a queue. The good thing about this model is that queues are cheap. When you make queues cheap (eg. one line of code) everything becomes easy: any developer can write reliable code without worrying about infra.
Monitoring and observability, as called out, is critical. Dead letter queues suck absolute major heaving amounts of nauseous air, and being able to manage and replay failing functions or steps is critical.
A couple differences wrt. FLAME and Inngest. Inngest is queue backed, event-driven, and servable via HTTP across any language. Because Inngest backs your state externally, you can write a workflow in Elixir, rewrite it in Typescript, redeploy, and running functions live migrate across backend languages, similar to CRIU.
Being event-driven allows you to manage flow control: everything from debounce to batching to throttling to fan-out, across any runtime or language (eg. one Elixir app on Fly can send an event over to run functions on TypeScript + Lambda).
I'm excited where FLAME goes. I think there are similar goals!
(Aside: I wish someone would rethink Serverless, heh.)
> (Aside: I wish someone would rethink Serverless, heh.)
Not sure if you've checked out https://sst.dev/ but I think they've done precisely that. For example, they have Live Lambda Development which makes local dev a real breeze by significantly shortening feedback loops (no need to push your code up to the cloud and wait for it to deploy)
> CPU bound work like video transcoding can quickly bring our entire service to a halt in production
Couldn't you just autoscale your app based on cpu though?
Or they require a GPU.
Or your core service only needs 1-2 servers, but you need to scale up to dozens/hundreds/thousands on demand, for work that only happens maybe once a day.
i think it's cool tech, but none of those things are "hair on fire" problems for me. i'm sure they are for some people.
We use it for our media supply chain which processes a few hundred videos daily using various systems.
Most other teams drank the AWS Step Koolaid and have thousands of lambas deployed, with insane development friction and surprisingly higher costs. I just found out today that we spend 6k a month on "Step Transitions", really?!
He mentioned this:
> With FLAME, your dev and test runners simply run on the local backend.
and this
> by default, FLAME ships with a LocalBackend
However, the overhead concerns me. This would only make sense in a situation where the function in question takes long enough that the startup overhead doesn't matter or where the main application is running on hardware that can't handle the resource load of many instances of the function in question.
I'm still, I think, in the camp of "monoliths are best in most cases." It's nice to have this in the toolbox, though, for those edge cases.
Resource-wise if you had a ton of unbounded concurrency then that would be a concern as you could quickly hit instance limits in the backend, but the pooling strategy discussed lower in the post addresses that pretty well, and gives you a good monitoring point as well.
Elixir looks ridiculously powerful. How's the job market for Elixir -- could one expect to have a chance at making money writing Elixir?
Its still a more exotic choice of a tech stack, and IMO its best suited for when you have fewer but more senior devs around, this is where it really shines. But I also found that phoenix codebase survived being "tortured" by a dozen juniors over years quite well.
I basically make my money solely with Elixir and have been for ~5 years now, interrupted only by gigs as a devops for the usual JS nightmares including serverless (where the cure always has been rewriting to Elixir/Phoenix at the end).
Is that a fair comparison?
A pattern here is to not split the monolith and use the same container for your main app and hot operations. The hot operations just need some different configuration eg. container args or env vars
That said, I don't understand this bit:
> Leaning on your worker queue purely for offloaded execution means writing all the glue code to get the data into and out of the job, and back to the caller or end-user’s device somehow
I assumed by "worker queue" they were talking about something akin to Celery in python land, but it actually does handle all this glue. As far as I can tell, Celery provides a very similar developer experience to FLAME but has the added benefit that if you do want durability those knobs are there. The only real downside seems you need redis or rabbit to facilitate it? I don't have any experience with them but I'd assume it's the same story with other languages/frameworks (eg ruby+sidekiq)?
Maybe I'm missing something.
He's made the distinction in the article that those tools are great when you need durability, but this gives you a lower ceremony way to make it Just Work™ when all you're after is passing off the work.
I believe the main point though is that background workers and FLAME are orthogonal concepts. You can use FLAME for autoscaling, you can use Celery for durability, and you could use Celery with FLAME to autoscale your background workers based on queue size. So being able to use these components individually will enable different patterns and use cases.
It's worth pointing out this transparent remote function call ability is unique to the BEAM. The FLAME pattern in other languages (as described in the article with Javascript) would also require serializable parameters.
> I believe the main point though is that background workers and FLAME are orthogonal concepts
Yeah I think this is what the author is driving at. I appreciate you helping me try and wrap my brain around it :)
It's been a while since I last read the multiprocessing docs but last time I did it did a pretty poor job of showing all the fancy tricks multiprocessing supports, like running a function in a different interpreter or on a different server altogether.
One could really up a whole Hetzner/OVH server and create a KVM for the workload on the fly!!
Reminds me of 12-factor app (https://12factor.net/) especially "VI. Processes" and "IX. Disposability"
If you're talking about a FLAME.call at app shutdown that hasn't yet reached the runner, it will follow the same app shutdown flows of the rest of your code and eventually drop into the ether like any other code path you have. If you want durability you'd reach for your job queue (like Oban in Elixir) under the same considerations as regular app code. Make sense?
https://en.m.wikipedia.org/wiki/OpenMosix
At least shipping processes should generalize to any FFI for most any run-time/language?
One potential gotcha I'm curious if there is a good story for is if it can guard against code that depends on other processes in the local supervision tree. I'm assuming since it's talking about Ecto inserts it brings over and starts the whole apps supervision tree on the function executor but that may or may not be desired for various reasons.
Similarly how does authorization/authentication/encryption work between the host and the forked of work? How is this all secured with minimal permissions?
On fly.io you get a private network between machines so comms are already secure. For machines outside of fly.io it’s technically possible to connect them using something like Tailscale, but that isn’t the happy path.
> how do I make sure that the unit of work has the right IAM
As shown in the demo, you can customise what gets loaded on boot - I can imagine that you’d use specific creds for services as part of that boot process based on the node’s role.
I still feel like Kubernetes might be the most profitable thing to ever happen to AWS for the same reason--and not because of EKS (their hosted Kubernetes control plane).
So: end-user -> app -> expensive_operation -> increase number of instances
rather than:
end-user -> app -> flame -> app_pool -> expensive operation -> scale pool
I guess this isn't 'specific parts' of my code by practically aren't you using the same app image in the pool? You'd have to have ffmpeg available, for example. I'm not sure I see the difference.
TL;DR: you get granular and programmatic scaling, including the ability to scale using specific resources (for example, machines with GPUs for certain workflows).
* OpenFaaS - https://www.openfaas.com/blog/porting-existing-containers-to...
* AWS Lambda - https://docs.aws.amazon.com/prescriptive-guidance/latest/pat...
* Scaleway Serverless Containers - https://www.scaleway.com/en/serverless-containers/
* Azure Container Instances - https://learn.microsoft.com/en-us/azure/container-instances/...
Probably others too, those are just the ones I know off the top of my head. I see very little reason to use traditional Function-based FaaS, which forces you into a special, locked-in framework, instead of using containers that work everywhere.
In that case, how should the Flame pools look like? Do they communicate in the same region and share the pools? Or are Flame pools strictly children of each individual parent? Does it make a difference in pricing or anything else to run on hot workers instead of starting up per parent?
What would you recommend the setup be in such a case?
Aside: I really liked the idea of Flame with Fly. It's a really neat implementation for a neat platform!
Confirm. Each parent node runs its own pool. There is no global coordination by design.
> Does it make a difference in pricing or anything else to run on hot workers instead of starting up per parent?
A lot would depend on what you are doing, the size of runner machines you decide to start in your pools (which can be different sizes from the app or other pools), etc. In general Elixir scales well enough that you aren't going to be running your app in every possible region. You'll be in a handful of regions servicing traffic in those regions and the load each region has. You could build in your own global coordination on top, ie try to find processes running on the cluster already (which could be running in a FLAME runner), but you're in distributed systems land and it All Depends™ what you're building the tradeoffs you want.
Can I suggest adding some docs to Fly to run Flame apps? To cover the more complex aspects of integrating with Fly, such as running Flame machines with a different size compared to the parent nodes, what kind of fly.toml config works and doesn't work with Flame, such as the auto_start and auto_stop configurations on the parent based on the number of requests, and anything else particularly important to remember with Fly.
- Trigger the lambda via HTTP endpoint, S3, or API gateway ($)
* Pretending that starting a fly machine doesn't cost the same as triggering via s3 seems disingenuous.
- Write the bespoke lambda to transcode the video ($) * In go this would be about as difficult as flame -- you'd have to build a different entrypoint that would be 1 line of code but it could be the same codebase. Node it would depend on bundling but in theory you could do the same -- it's just a promise that takes an S3 event, that doesn't seem much different.
- Place the thumbnail results into SQS ($) * I wouldn't do this at all. There's no reason the results need to be queued. Put them in a deterministically named s3 bucket where they'll live and be served from. Period.
- Write the SQS consumer in our app (dev $) * Again -- this is totally unnecessary. Your application *should forget* it dispatched work. That's the point of dispatching it. If you need subscribers to notice it or do some additional work I'd do it differently rather than chaining lambdas.
- Persist to DB and figure out how to get events back to active subscribers that may well be connected to other instances than the SQS consumer (dev $) * Your lambda really should be doing the DB work not your main application. If you've got subscribers waiting to be informed the lambda can fire an SNS notification and all subscribed applications will see "job 1234 complete"
So really the issue is:* s3 is our image database
* our app needs to deploy an s3 hook for lambda
* our codebase needs to deploy that lambda
* we might need to listen to SNS
which is still some complexity, but it's not the same and it's not using the wrong technology like some chain of SQS nonsense.
> * Pretending that starting a fly machine doesn't cost the same as triggering via s3 seems disingenuous.
You're going to be paying for resources wherever you decide to run your code. I don't think this needs to be spelled out. The point about costs is rather than paying to run "my app", I'm paying at multiple layers to run a full solution to my problem. Lambda gateway requests, S3 put, SQS insert, each have their own separate costs. You pay a toll at every step instead of a single step on Fly or wherever you host your app.
> * I wouldn't do this at all. There's no reason the results need to be queued. Put them in a deterministically named s3 bucket where they'll live and be served from. Period. This is totally unnecessary. Your application should forget it dispatched work. That's the point of dispatching it. If you need subscribers to notice it or do some additional work I'd do it differently rather than chaining lambdas.
You still need to tell your app about the generated thumbnails if you want to persist the fact they exist where you placed them in S3, how many exist, where you left off, etc.
> * Your lambda really should be doing the DB work not your main application. If you've got subscribers waiting to be informed the lambda can fire an SNS notification and all subscribed applications will see "job 1234 complete"
This is exactly my point. You bolt on ever more Serverless offerings to accomplish any actual goal of your application. SNS notifications is exactly the kind of thing I don't want to think about, code around, and pay for. I have Phoenix.PubSub.broadcast and I continue shipping features. It's already running on all my nodes and I pay nothing for it because it's already baked into the price of what I'm running – my app.
I think this is fine if and only if you have an application that can subscribe to PubSub.broadcast. The problem is that not everything is Elixir/Erlang or even the same language internally to the org that runs it. The solution (unfortunately) seems to be reinventing everything that made Erlang good but for many general purpose languages at once.
I see this more as a mechanism to signal the runtime (combination of fly machines and erlang nodes running on those machines) you'd like to scale out for some scoped duration, but I'm not convinced that this needs to be initiated from inside the runtime for erlang in most cases -- why couldn't something like this be achieved externally noticing the a high watermark of usage and adding nodes, much like a kubernetes horizontal pod autoscaler?
Is there something specific about CPU bound tasks that makes this hard for erlang that I'm missing?
Also, not trying to be combative -- I love Phoenix framework and the work y'all are doing at fly, especially you Chis, just wondering if/how this abstraction leaves the walls of Elixir/Erlang which already has it significantly better than the rest of us for distributed abstractions.
In this case, we give you global event streams with a durable workflow engine that any language (currently Typescript, Python, Go, Elixir) can hook into. Each step (or invocation) is backed by a lightweight queue, so queues are cheap and are basically a 1LOC wrapper around your existing code. Steps run as atomic "transactions" which must commit or be retried within a function, and are as close to exactly once as you could get.
My advice to anyone starting out on a purely web adventure: run your monolith on lambda. Just upload your whole app as your lambda. Use dynamodb for storage.
When your app gets popular, then optimize. Make a separate lambda for the popular part. Spin up a relational database or maybe an auto scaling group.
But start with a monolith on lambda. Get all the benefits without the downsides.
My advice would be to start with a plain old monolith. Even launch with it. Once, and if, you have workloads that a) make sense to run as an isolated function, and b) would help offload some processing from your main app, consider splitting it and using a Lambda.
Doing so prematurely, or worse, starting with it, opens the door to complexity you just don't need early on.
I’ve set up multiple businesses this way. It works great. There are no lambda details to worry about, the defaults are fine. Just upload code and run. No load balancers, no firewall rules. It couldn’t be simpler.
If it doesn’t work then worry about that other stuff.
But it’ll just work 9 times out of 10 in my experience.
Right, but how do you know that on a new project? What happens if you _do_ need more execution time, RAM, CPU, IOPS, etc., than what Lambda provides? Or need some specific integration or workflow that doesn't fit the Lambda model? It would be very expensive to backtrack and implement a traditional architecture at that point, instead of starting with the traditional architecture and using Lambda only when you have a good use case for it.
Not to mention that you're going all in with a single cloud provider. Vendor lock-in at an early stage is not a good idea.
> Just upload code and run. No load balancers, no firewall rules. It couldn’t be simpler.
It's probably simple for you if you've done it many times, but it would require a considerable amount of time and effort for someone unfamiliar with the stack or AWS. The simplest thing is often whatever we're used to, and if that's a traditional LAMP stack, then I'd suggest going with that first.
It’s dead simple for a beginner, far simpler than a LAMP stack.
I think you misunderstand how lambda works because your objections don’t make sense.
Lambda is a LAMP stack. They just take care of LA for you.
> Use dynamodb for storage.
So far I've done the opposite, i.e. use a relational database at the beginning, and, if the access pattern is clear and there are some parts of the application that would not scale well with SQL, move those parts to DynamoDB.
1. write a bog standard express.js backend (might or might not use a SPA/SSR frontend)
2. make the normal (dev mode) server start with an index.js (which boots the express app on a port) and ontop of that an additional lambda.js next to it, which wraps the app (not booting it) with `@vendia/serverless-express`.
3. use AWS CDK (which is typescript anyways!) and ship it as a "NodejsFunction", which also will use esbuild under the hood to bundle your app into one JS blob (no need or a ZIP container with node_modules!) on deploy. The entry file is the "lambda.js" from step 2.
4. in addition use the CDK to configure some traffic origin for the lambda, the cheapest one being a CloudFront function URL - which also acts as a quite good cache in general when you send the appropriate HTTP response headers from your express app!
5. Point a domain to the Cloudfront.
6. Profit!
... I've used this excessivbely in the past years for lots of microservices/microfrontends and it basically works as promised: scale to zero, scale horizontally on demand automatically, and being rather cheap (esp when in the free tier then its basically free to run small-mid loads).
Having said that, this WILL become a maintenance trainwreck when scaled up, telling from my devops experience. All the libraries/apis/infrastructure (especially in nodejs land) tend to have breaking updates all the time, so better have at least one FTE dedicated to maintenance of all the moving parts.
And tbh: all this pain goes away for like <100$/mo with Elixir/Phoenix/LiveView on fly.io. a single phoenix app on a single server can deal with _suprising_ amounts of traffic, and even if you get to serious load, scaling horizontally is outright trivial with Elixir+Fly. And there is no need for additional infra like Redis or Message Queues, since these things have natively built-in equivalents in Elixir (or: the BEAM itself). So only an appserver and a database you need here in terms of infrastructure/maintenance.
Oh man, this sounds like a dealbreaker, but thanks for sharing your approach nonetheless!
If people are churning after like 1-2 years, they never get to experience the long term pain themselves, and new hires will lobby for rewrites anyways. Since individual apps are rather small, rewrites service-by-service should be straightforward and manageable.
Ancient knowledge would be helpful here, but most teams simply don’t have „real“ seniors around. Therefore simple advice from my end:
1. keep the database stupid and sane. Postgres usually has everything needed for nearly every usecase in a good enough and battleground way. Don’t overdo complex join queries in hot code paths.
2. use a mature, slow-changing (as in: no breaking changes yearly) tech stack, that is powerful out of the box. When you need to use several infrastructure crudges, the tech isn’t powerful enough. I recommend Elixir, Go or even JVM-flavors, these will carry you far.
3. Monolith it is, fullstack. Every service/frontend you split apart might become easier in isolation, but you pile up significant overhead and complexity in terms of global monitoring/orchestration/dependencies/testability/debugging. Elixir/Phoenix, PHP/Laravel, … are fine choices that also have straightforward scaling options and cover everything you need probably.
4. minimize/eliminate usage of JS wherever possible. Aside from infrastructure providers/external APIs, this is the biggest source of maintenance issues. Also SPAs are rarely worth it, there still is a boring way of progressive enhancement that just works.
If customers think this is a feature and not a bug, then I have a very different understanding about what serverless/FaaS is meant to be used for. My division is pretty much only looking at edge networking scenarios. Can I redirect you to a CDN asset in Boston instead of going clear across the country to us-west-1? We would definitely NOT run Lamba out of us-west-1 for this work.
There are a number of common ways that people who don't understand concurrency think they can 'easily' or 'efficiently' solve a problem that provably do not work, and sometimes tragicomically so. This feels very similar and I worry that fly is Enabling people here.
Particularly in Elixir, where splitting off services is already partially handled for you.
I'm not sure how you're misunderstanding, but why would it go across the country when it's guaranteed to run from the same parent? Just deploy the app in the region you want, and now its Flame pools will be deployed in the same region.
If you want to switch the region it runs in, you can also easily just contact the other cluster to tell it to pick up the work.
Every solution is easy when you oversimplify the problem.
None of what you said is true if you care about persistent state in the app. Local reads and distant writes are how you avoid speed of light problems.