this is a continuation of the dev diaries at How far can i get with Swift WebAssembly in 30 minutes? , but i’m posting it as a separate thread for ease of referring to it.
my goal today is to take the app i wrote last week and deploy it live over the internet, in an at least somewhat maintainable way.
choosing an architecture
to deploy a Traditional Server Application you need to stand up a machine like an EC2 instance that has an IP address and runs some kind of SwiftNIO-based process that responds to incoming requests. so the question i ask myself is: do i want to do that here?
and in general there would be two big reasons why i would want to do this:
- the HTML is Dynamic, even just a little bit Dynamic
- i can get live statistics about how often the app is being accessed, including how many requests are coming from Real Human Users as opposed to AI Bots, and geofence requests from High Risk Jurisdictions, without paying Cloudflare hundreds (thousands?) of dollars a month to do this for me.
but a WebAssembly app doesn’t really need much, or any Dynamic HTML at all. in fact, if we didn’t care about SEO, the HTML could just be a blank canvas that loads the WebAssembly and JavaScript resources. (like Vue.js?)
and, i can get anonymized usage metrics by adding some callback URL to the app later. so my instinct is i do not need to rent an EC2 instance at all, this whole thing could be an Embarrassingly Static, S3-only application.
generating static HTML
after i decided i would be serving static HTML, i realized that simplifies a ton of things for me. i do not need to write a server! all i really need is a Swift script that prints the HTML, and some Bash that packages the thing into a bundle.
the HTML looks like this:
and it’s called like this:
while i was doing this, i came to the conclusion that sourcekit-lsp just wasn’t well equipped to handle a project with modules that target both WebAssembly and the host platform, so i deleted the .sourcekit-lsp
configuration i had added earlier, and now i am just building everything for the host.
when i build for WebAssembly, i pass --scratch-path .build.wasm
to help it coexist with the host build.
deploying from GitHub
i am Very Lazy and it would be nice if i just had a GitHub Action that builds and deploys this application every time i tag a release in the Git repo.
here is the Deploy.yml i am using right now, which begins with
and ends with
the Deploy
script takes the index.html
and overwrites the unversioned location s3://tayloraswift-apps/bson-inspect/index.html
with its contents.
it also takes the two main.js
, main.wasm
resources, compresses them with gzip
, and uploads them to a versioned URL. this means when the CDN cache turns over for the index.html
, it will naturally fetch a fresh version of the app and its runtime.
HTTP models data compression at the protocol level, so the WebAssembly is named main.wasm
, not main.wasm.gz
.
creating an S3 Bucket
now i need to actually set up this S3 Bucket, which is Not Complicated because all of the default settings are pretty good for this use case.
you really shouldn’t make S3 Buckets publicly accessible, even for read-only access, so to allow GitHub Actions to access a Bucket, you need to create an IAM key in AWS.
apply some Common Sense when picking credential settings.
to give the key access to the S3 Bucket, you need to explicitly give it the appropriate permissions using a Customer inline policy.
for some reason, the AWS Visual Editor used to populate the Resource
field without the trailing /*
, and that will not work, so make sure the Bucket path ends in /*
.
once you get the key, you can add it to the GitHub repo.
i used a regular Repo Variable for the Key ID, and a Repo Secret for the key itself.
after a little trial and error, i got the GitHub Actions workflow to populate the S3 bucket with the expected resources.
one thing i want to point out — i nested all these resources under a directory called bson-inspect
instead of using a Unique Name for the index.html
. this will allow me to reuse this S3 bucket for other applications, without making every app’s resources accessible from every other app’s CDN, because it is very difficult to restrict CloudFront access without at least one level of nested directory.
making a CloudFront Distribution
i think a lot of people’s first instinct is to serve things directly from S3, and this isn’t very efficient for Amazon, so they make this (relatively) expensive to try and encourage people to use CloudFront. but i think a lot of people ask for this anyway, so they added the “Serve a static website” option to S3.
i don’t recommend using this because if You’ve Got Opps and one of them deduces the name of your S3 bucket (i posted a screenshot of mine) they can really run up your cloud bill, a lot easier than they can at the CloudFront layer.
so i’m going to use CloudFront, not really as a CDN (i’m gonna use Cloudflare for that), but just as a glorified routing layer because CloudFront is easy to configure.
adding an Origin
to connect the CloudFront Distribution to the S3 Bucket i added an Origin, with an Origin Path of /bson-inspect
. this makes it so that requests to this Distribution can only ever access S3 objects that are descendants of /bson-inspect
, and nothing else that lives in that Bucket.
i’m not keeping anything Secret in the Bucket, and i’m operating under the assumption that everything in that Bucket will Eventually Be Compromised, but it’s just good security hygiene.
there are two more things you need to do that are very necessary and totally not Cargo Culty.
first, you need to add an Origin Access Control which has Signing behavior set to Always sign requests
. OACs are a resource for some reason because on AWS Everything Is A Resource, and i use the same one across all my CloudFronts.
if you don’t add this, CloudFront will make a naïve request to the S3 Bucket, and the Bucket will not respond because CloudFront did not sign the request.
second, you have to actually whitelist the CloudFront Distribution on the S3 Bucket’s side, and you do this by adding a Bucket Policy like this one:
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::tayloraswift-apps/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::1234567890:distribution/THE_DISTRIBUTION_ID"
}
}
}
]
}
CloudFront can generate one for you, just make sure the S3 Resource
path ends with a /*
.
creating a Behavior
to route requests against the Distribution to the Origin, i added a Behavior associated with the S3 Bucket Origin with Path pattern set to *
.
for some reason, you always have to have at least one Behavior mapped to *
, which is why i added the /bson-inspect
path prefix to the Origin earlier. otherwise you could fetch anything in the Bucket from this Distribution.
set the Viewer protocol policy to HTTP and HTTPS
. i did this wrong at first, setting it to HTTPS only
, and spent a lot of time discovering that Cloudflare doesn’t like that. so make sure HTTP is enabled.
i unchecked the Compress objects automatically option, because the resources are already compressed.
i also set the caching policy to Managed-CachingDisabled
, because i only want this to function as a routing layer.
other settings
the first time i made a CloudFront, i thought i had to create an SSL certificate for the Distribution, but you don’t need to do this, so Don’t Be Like Me. leave this part blank.
the last thing i did was point the Root object to index.html
. this makes it so that when you visit /
, CloudFront requests the object /bson-inspect/index.html
, where /bson-inspect
is the prefix i configured earlier.
to check that you did all this correctly, you can visit the raw CDN URL which is visible under Distribution domain name.
and sure enough, the application loads in Firefox!
the whole thing transfers less than 1.5 MB of data without caching. Swift is incredible!
Cloudflare for president
i used to think Cloudflare was insanely overhyped because i only worked on internal things, or things that serve dynamic HTML, and Cloudflare doesn’t really add a lot of value to those use cases. but now that i’m making what is effectively an SPA, i totally get the hype now. Cloudflare is a gift from the gods.
did you know Cloudflare is a domain registrar? i managed to snag bsoninspect.dev
for $12.18 a year!
under DNS Records, i added two CNAME records pointing to my CloudFront Distribution, and turned the proxying on.
then i went to the Caching settings and added a single Cache Rule called “Short Term Cache” with an Edge TTL of 5 minutes, and a Browser TTL of 2 hours.
this means that Firefox will make no more than one request (per page) to Cloudflare every 2 hours, which can be overridden in Firefox, and Cloudflare will make no more than one request (per page) to CloudFront every 5 minutes, which cannot be overridden.
one more thing
there’s one more thing you have to do back on the CloudFront side, and that’s add the domain names you’re using to Alternate domain names. apparently, CloudFront will not serve a request if it sees a Host:
header that doesn’t match one of these. Cloudflare, i think, will not replace the header with the CNAME it knows about, so it has to match the Host:
header that was actually sent from Firefox.
am i blacklisted???
it takes a few minutes for a CloudFront to deploy, and AWS will show the live status of your CloudFront in its web UI. it takes a few minutes for Cloudflare to deploy its own CDN too, and there is no UI indicator for Cloudflare, as far as i know.
but after more than a few minutes, i still could not access bsoninspect.dev
, which was weird.
i think, when you first purchase a domain, that domain is likely to be blacklisted by a lot of ISPs because before you bought it, it was probably being squatted by someone doing Weird Shit with it, and it takes a while for them to delist it. this happens at the DNS layer, and it turns out my ISP had blacklisted bsoninspect.dev
and was resolving it to a DNS sinkhole.
this has an easy fix, just point your computer’s default DNS server to something like 1.1.1.1
(Cloudflare) or 8.8.8.8
(Google).
step 1: get the name of the network interface with the paranoid DNS server
$ resolvectl status
Global
Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Link 2 (wlp59s0)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 172.20.1.36
DNS Servers: 172.20.1.36
step 2: set it to something that is not blacklisting you
$ resolvectl dns wlp59s0 1.1.1.1
step 3: profit
$ resolvectl status
Global
Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Link 2 (wlp59s0)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
DNS Servers: 1.1.1.1
$ resolvectl query bsoninspect.dev
bsoninspect.dev: 172.67.207.221 -- link: wlp59s0
104.21.22.242 -- link: wlp59s0
2606:4700:3032::6815:16f2 -- link: wlp59s0
2606:4700:3033::ac43:cfdd -- link: wlp59s0
-- Information acquired via protocol DNS in 68.2ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network
of course this doesn’t help other people trying to access your website, but that will probably get better with time.
conclusions
Swift on WebAssembly has really great performance! on a completely cold request, the app loads in just 0.8 seconds on my home internet connection.
if Cloudflare has the page in its cache, the request is even faster – on a Cloudflare hit, the WebAssembly loads after just 124ms. WOW
keep in mind, this is a Really Awful First Draft, with no preloading whatsoever. this means Firefox downloads the index.html
, and then it downloads the main.js
, and then it downloads the main.wasm
. that’s obviously Incredibly Dumb so i’m confident preloading would make this Even Faster.
how robust is this thing? well let’s imagine Swift Forums user @kanyewest has it out for me and writes a script that continuously performs a DoS attack on bsoninspect.dev.
-
Cloudflare gives me unlimited bandwidth, so Ye is just wasting his own bandwidth there.
-
Cloudflare makes up to 3 requests every 5 minutes to CloudFront, downloading ~1.5 MB of uncached data each time. this will transfer about 38.9 GB from CloudFront via 25,920 HTTP requests per month, which is like, 3.9 percent of the CloudFront Free Tier.
if you’ve exhausted the Free Tier, this will cost like, $3 a month.
Ye is also going to have a hard time querying my CloudFront Distribution directly, because Cloudflare performs CNAME flattening, so that provides an important layer of security.
-
CloudFront makes another 25,920 HTTP requests to S3, which is billed separately, and will cost a whopping 1¢ per month (really!)
Ye cannot query S3 at all, this is why we are using CloudFront instead of the static website feature.
it’s safe to assume the cost of storing a 1.5 MB WebAssembly app, even multiple versions of the app, will be negligible. data transfer between AWS services is free.
Cloudflare is using HTTP to access CloudFront, but i am not particularly worried about Ye performing a MITM against Cloudflare. plus, he would have to figure out where the CloudFront distribution lives, and Cloudflare keeps that hidden.
i write out all this math because i think even though Swift is already a very efficient server-side language, Traditional Server Applications still need to pay for things like IPv4 addresses and EC2 instances, and those costs themselves are tiny compared to the maintenance burden of securing those EC2 instances against malicious activity. this stuff just Doesn’t Apply to WebAssembly. i can just push an update to GitHub and Not Think About Deployment at all.
the bottom line is: Swift on WebAssembly gives you all the benefits of a modern, AoT compiled language that can run on the client side, with a marginal server-side cost that is Basically Free.