Now Reading
From Go on EC2 to Fly.io: +enjoyable, −$9/mo

From Go on EC2 to Fly.io: +enjoyable, −$9/mo

2023-02-26 23:13:09

February 2023

Go to: Old to new | To-dos | Weddings | Config | Statics | Cron | Load testing | Conclusion

I not too long ago switched two facet initiatives from being hosted on an Amazon EC2 occasion to utilizing Fly.io. It was a extremely good expertise: Fly.io simply labored. It allowed me to delete about 500 strains of Ansible scripts and config information, and saved me $9 a month.

For the bigger of the 2 initiatives, I additionally made a number of simplifications whereas I used to be at it: I switched from utilizing a CDN for internet hosting static information to utilizing go:embed with ETag caching, from utilizing cron jobs to easy background goroutines, and from utilizing config information to surroundings variables.

I left the structure of each apps the identical: every makes use of a Go net/http server, an SQLite database, and a few HTML templates and static information.

It took me about an hour to determine the fundamentals of Fly.io and transfer the less complicated venture, and a few evenings to maneuver the extra complicated one. Fly.io handles the annoying reverse proxy and SSL stuff, deployment is so simple as fly deploy, and there’s a pleasant dashboard on Fly.io to indicate me what’s occurring.

Outdated to new

For a very long time, I’ve been utilizing a single EC2 occasion operating Amazon Linux to host these two purposes (occasion sort t2.micro). They’re low-traffic websites, and this labored positive. However even with good instruments, it required extra setup and babysitting than I cared for.

These are small Go internet purposes, and as somebody pointed out on Hacker Information, deploying a Go app is so simple as “Scp the executable and run it. It will work on any Linux vm with none setup.”

As I replied (this was earlier than really switching to Fly.io):

That’s what I do now. However there’s a bunch extra setup:

  • Set up and configure Caddy to terminate the SSL. Caddy is nice, however nonetheless stuff to consider and 20 strains of config to determine.
  • Configure systemd to run Caddy and my Go server. Not rocket science, however required me determining systemd for the primary time and the suitable 25-line config file for every server.
  • Scripts to improve Caddy when a brand new model comes out (it wasn’t within the apt repos after I did this).
  • Ansible setup scripts to clone the repo, create customers and teams for Caddy and my Go server, copy config information, add cron jobs for backups (150 strains of Ansible YAML).

It appears such as you don’t want most of this, or get it with out further configuration with Fly.io and Render.

As I famous after that, the distinction between Fly.io and doing it your self utilizing an EC2 occasion is form of just like the distinction between Dropbox and what was instructed in that famous Hacker News comment when Dropbox first got here out:

For a Linux person, you may already construct such a system your self fairly trivially by getting an FTP account, mounting it regionally with curlftpfs, after which utilizing SVN or CVS on the mounted filesystem. From Home windows or Mac, this FTP account may very well be accessed by built-in software program.

It may be “fairly trivial”, but it surely’s nonetheless an excessive amount of work for lazy builders, not to mention non-technical individuals.

So I’d been wanting round for a easy internet hosting service that will handle internet hosting, SSL certificates, and deployment. A few years in the past I performed with Heroku, however they have been costlier, and so they pushed you towards utilizing their comparatively expensive hosted databases (as a substitute of a easy disk quantity for SQLite). It appears like that is still the case.

Then extra not too long ago I bumped into Fly.io and Render. Render really appears a bit extra full-fledged (for instance, they help cron jobs), but it surely wasn’t going to save lots of me any cash in comparison with EC2, so I saved wanting.

Fly.io appeared extra geeky and command line-oriented, which suited me, and their costs are additionally ridiculously low: free for as much as three small digital machines (I solely want two), and $2/month for small VMs after that. It seems that 1 shared CPU and 256MB of RAM is loads for a Go app, even with a modest quantity of site visitors (see load testing).

I take advantage of SQLite for each my apps, and Fly.io is all-in on SQLite, so it appeared like a superb slot in that respect. In addition they have a wonderful tech blog.

Easy Lists

Screenshot of Simple Lists

I wished to check out Fly.io on the tiny to-do record app that I host for my household (see my article about Simple Lists). It’s written in Go, and is constructed within the old-school approach: HTML rendered by the server, plain previous GET and POST with HTML kinds, and no JavaScript.

So I put in flyctl (the Fly.io CLI), and first impressions have been good! With out altering my supply code in any respect, I typed flyctl launch to see what would occur. A few minutes later, my app was up and operating on a fly.dev subdomain. Certainly it will possibly’t be this straightforward…

But it surely form of was. The software had auto-generated a fly.toml config file, routinely found out construct my Go app, found out that the app appeared for the PORT surroundings variable and added an entry for PORT=8080. It appears like they use Paketo “construct packs” to do that – although I wasn’t aware of this venture earlier than.

The one situation was that it was referencing an SQLite database on the digital machine’s ephemeral disk, so each time I deployed, Fly.io would blow away the database.

To repair this, Fly.io has the idea of persistent volumes, so I used the CLI to create one:

$ flyctl volumes create simplelists_data --size=1

The --size=1 means a 1GB quantity. That’s various to-do record entries … however apparently that’s the smallest measurement they permit. Fly.io offers you 3GB without cost, and fees $0.15/GB monthly after that. Storage is reasonable!

Then I added the next three strains to fly.toml (the final version is principally what flyctl generated with this added):

[mounts]
  supply = "simplelists_data"
  vacation spot = "/knowledge"

Then every little thing simply labored. Fly.io does each day snapshots of your volumes, which is sufficient “backup” for this use case. For some cause the snapshots are about 60MB, after I’m solely utilizing about 100KB of disk area, however oh properly – that’s Fly.io’s drawback!

In order for you your individual occasion of Easy Lists, you may clone the repo and sort flyctl launch to run it your self. You’ll have to generate a password hash with simplelists -genpass and set the SIMPLELISTS_PASSHASH secret first.

Gifty Weddings

Gifty Weddings is a marriage present registry web site that helps {couples} make their very own present registry that’s not tied to a selected retailer. It’s a medium-sized internet software with a Go and SQLite backend and an Elm frontend. Right here’s what the house web page appears like:

Screenshot of the Gifty Weddings website

To get Gifty engaged on Fly.io, I needed to make a number of adjustments:

  • Make the server take config choices as surroundings variables as a substitute of in a file.
  • Embed HTML templates and static information within the Go binary utilizing go:embed and fs.FS.
  • Add goroutines for my two background duties as a substitute of utilizing cron jobs.

Config in surroundings variables

The primary change was a really minor one. I had a Config struct that I loaded utilizing json.Decoder, and I modified that to make use of os.Getenv. This can be a comparatively small venture, so there’s no want for a elaborate library like Viper – the Go customary library works positive.

Right here’s roughly what this appears like:

func fundamental() {
    cfg := Config{
        AWSKey:        os.Getenv("GIFTY_AWS_KEY"),
        AWSSecret:     os.Getenv("GIFTY_AWS_SECRET"),
        ListenAddress: getEnvOrDefault("GIFTY_LISTEN_ADDRESS", ":8080"),
        DatabasePath:  getEnvOrDefault("GIFTY_DATABASE_PATH", "/knowledge/gifty.sqlite"),
        ...
    }
    // ... use cfg ...
}

func getEnvOrDefault(title, defaultValue string) string {
    worth, okay := os.LookupEnv(title)
    if !okay {
        worth = defaultValue
    }
    return worth
}

Internet hosting static information

Right here’s the place I had a call level. I’ve beforehand advocated utilizing a CDN like Amazon Cloudfront (backed by S3) for internet hosting static information. I even wrote a Python software referred to as cdnupload that uploads an internet site’s static information to S3 with a content-based hash within the filenames to present nice caching whereas avoiding versioning points. As of a few weeks in the past, that’s additionally what I used for Gifty.

That setup remains to be good for bigger, distributed purposes – and I like what Fly.io is doing with distributed apps – however for this small web site it appeared like overkill. Go’s internet server is ok at serving static information, and I knew I might use Final-Modified or ETag headers to unravel the caching situation.

So I stated goodbye to cdnupload and went all in on go:embed. This landed in Go 1.16: it’s a built-in option to inform the Go compiler to embed your information into the binary and make them accessible as an fs.FS filesystem interface at runtime.

Right here’s what that appears like:

// This "go:embed" directive tells Go to embed static/* (recursively),
// and make it accessible because the staticFS variable.

//go:embed static/*
var staticFS embed.FS

func fundamental() {
    // Inform the HTTP server to serve staticFS at /static/*
    hashFS := hashfs.NewFS(staticFS)
    http.Deal with("/static/", hashfs.FileServer(hashFS))

    // This perform is handed to the HTML templating engine,
    // permitting templates to generate paths to static information.
    // In templates, it is used like this:
    //
    //  <hyperlink rel="stylesheet" href="https://benhoyt.com/writings/flyio/{{static"kinds/fundamental.css"}}">
    funcMap := template.FuncMap{
        "static": func(path string) string {
            return "https://benhoyt.com/" + hashFS.HashName("static/"+path)
        },
    }
    // ...
}

Though Go’s http.FileServer helps Final-Modified headers, sadly go:embed doesn’t provide file modification time. Nor does it support ETag.

That’s a bit annoying, and I used to be nearly to write down an ETag wrapper myself, however then I discovered a 200-line library by Ben Johnson (who works at Fly.io!) referred to as hashfs. The library wraps an fs.FS filesystem and provides you an http.Handler that generates ETag headers, permitting browsers to cache successfully.

Background jobs

Earlier than switching to Fly.io, I had two cron jobs:

  • A job that sends “post-wedding” emails to clients a number of days after their marriage ceremony date passes.
  • A job that backs up the database each day utilizing the SQLite consumer’s .backup command, and uploads the consequence to an S3 bucket.

Fly.io doesn’t help cron jobs as a built-in idea, so I had a number of choices to select from:

  1. Use Fly.io to run a service supervisor that will begin Gifty in addition to the cron jobs. [Update: as Ben Johnson pointed out, if I was using a Dockerfile, I could apt install cron and then just use cron normally.]
  2. Fireplace up a separate cron software in Fly.io or use Fly Machines.
  3. Use easy goroutines within the Go server to carry out background duties.

Possibility 1 would defeat a number of the simplicity of utilizing Fly.io within the first place: I’d must create a Dockerfile and configure varied issues, which I wished to keep away from.

See Also

Possibility 2 is cleaner, but it surely may be annoying to attach the cron app to the primary app to entry the database (are volumes cross-application? I’m unsure). And Fly Machines is one other factor to be taught (and what would begin them on a time interval?).

Possibility 3 at first appears soiled, however I really like the simplicity of it! I wouldn’t must be taught something new, and I knew I wasn’t going to run a couple of occasion of my app, so I ended up going with that.

In Go, you can begin a timed background activity in a number of strains of code utilizing a goroutine and a time.Ticker. That is roughly what I’m doing (in fundamental):

// Begin goroutine to ship post-wedding emails from time to time.
go func() {
    ticker := time.NewTicker(time.Hour)
    for {
        <-ticker.C
        err := sendPostWeddingEmails(config, emailRenderer, dbModel)
        if err != nil {
            emailAdmin("error sending post-wedding e mail: %v", err)
        }
    }
}()

// Begin goroutine to verify if database wants backing up from time to time.
// Ticks each 6 hours, however backUpDatabase skips if there's already one right this moment.
go func() {
    ticker := time.NewTicker(6 * time.Hour)
    for {
        <-ticker.C
        err := backUpDatabase(config, s3Client)
        if err != nil {
            emailAdmin("error backing up database: %v", err)
        }
    }
}()

It’s simplistic, but it surely works properly. Retries should not dealt with instantly – I simply use the truth that it’ll strive once more subsequent tick (not that I get many errors!).

And sure, I do know – the goroutines aren’t gracefully shut down when the server stops. However within the unlikely occasion the server exits when a activity is operating, it gained’t damage something. The one further factor I do in the actual code is catch panics and e mail these to me too.

The backUpDatabase perform (once more, protecting it stupid-simple) makes use of os/exec to run the sqlite3 consumer with a script of .backup <filename> after which uploads the consequence to a non-public S3 bucket. It additionally deletes any backups older than the most recent 10.

Load testing

I already took down the previous EC2 server, so sadly I can’t evaluate earlier than and after occasions. Nevertheless, I primarily wished to check that the brand new server was quick sufficient.

The location on Fly.io feels sooner from right here (New Zealand), however I feel that’s primarily as a result of I’m internet hosting it in Fly.io’s syd area (in Sydney, simply throughout the ditch), whereas beforehand it was hosted in AWS’s us-west-2 area (in Oregon), which is considerably farther from me and most of my clients.

As well as, my static information are actually hosted on the identical area and server, which implies they’re additionally coming from Sydney as a substitute of the U.S., and means the browser might be able to reuse open connections, and doesn’t must do TLS setup for an additional host.

Right here’s a screenshot of the community timeline for the preliminary HTML and subsequent static information – uncached. That is from the homepage, which is the heaviest web page because it consists of a variety of photographs, however I’m happy with the truth that it totals lower than 900KB and is absolutely loaded in underneath a second.

GiftyWeddings.com network timeline

I ran a small check utilizing the HTTP load testing software Vegeta hitting 4 URLs: three pages that render HTML templates (the house web page, a registry web page which does two SQL queries, and the contact web page), and a medium-sized picture.

I had the “assault” run for 10 seconds. The default charge is 50 requests per second, however I additionally tried 500 and 1000. Under are the outcomes:

$ cat urls.txt | vegeta assault -duration=10s | vegeta report
Requests      [total, rate, throughput]         500, 50.10, 49.84
Period      [total, attack, wait]             10.032s, 9.98s, 51.434ms
Latencies     [min, mean, 50, 90, 95, 99, max]  42.805ms, 50.004ms, 45.411ms, 53.643ms, 58.801ms, 146.6ms, 216.559ms
Bytes In      [total, mean]                     10757125, 21514.25
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Standing Codes  [code:count]                      200:500
Error Set:

$ cat urls.txt | vegeta assault -duration=10s -rate=500/s | vegeta report
Requests      [total, rate, throughput]         5000, 500.08, 497.69
Period      [total, attack, wait]             10.046s, 9.998s, 47.869ms
Latencies     [min, mean, 50, 90, 95, 99, max]  42.615ms, 61.354ms, 49.472ms, 72.032ms, 117.76ms, 304.653ms, 1.177s
Bytes In      [total, mean]                     107571250, 21514.25
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Standing Codes  [code:count]                      200:5000  
Error Set:

$ cat urls.txt | vegeta assault -duration=10s -rate=1000/s | vegeta report
Requests      [total, rate, throughput]         10000, 1000.11, 994.86
Period      [total, attack, wait]             10.05s, 9.999s, 50.801ms
Latencies     [min, mean, 50, 90, 95, 99, max]  42.876ms, 126.907ms, 60.591ms, 254.24ms, 419.47ms, 1.294s, 3.508s
Bytes In      [total, mean]                     215062995, 21506.30
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           99.98%
Standing Codes  [code:count]                      0:2  200:9998  
Error Set:
Get "https://giftyweddings.com/": ... connection reset by peer
Get "https://giftyweddings.com/static/photographs/gifts-b80...38f.jpg": ... connection reset by peer

Even after I went as much as the speed of 500 requests per second it dealt with positive, and looking the location was nonetheless quick, although the imply went up from 50ms to 61ms and the 99th percentile from 147ms to 305ms.

It was solely went I cranked the speed as much as 1000 requests per second that the Fly.io VM began to battle: the imply went as much as 127ms and the p99 to 1.3s, there have been two errors out of 10,000 requests, and looking the location throughout the check felt sluggish.

So the smallest shared 1 CPU Fly.io VM can deal with 500 requests per second with none issues. I’m pleased with that! I understand this isn’t precisely a scientific check, but it surely’s ok for my functions right here.

Conclusion

I’m just a few weeks into utilizing Fly.io to host my facet initiatives, however I’m very pleased with their product to date. I used to be fairly completely satisfied to delete the five hundred strains of Ansible scripts, systemd unit information, and Caddy config information.

It additionally made me smile to lastly cease the EC2 occasion and bump my AWS invoice down from $9 monthly to about 10 cents monthly (I nonetheless use S3 for user-uploaded photographs and for backups). I’ve nothing towards EC2 and would use it once more for sure issues, however for small internet purposes, Fly.io looks as if an ideal match.

It most likely seems like Fly.io is paying me to hold on like this, however belief me, they’re not. I’m simply an enthusiastic geek who likes their product, their help for very small VMs, and their love of SQLite. To not point out their pricing!

I’d find it irresistible for those who sponsored me on GitHub – it can inspire me to work on my open supply initiatives and write extra good content material. Thanks!

Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top