New Site (same as old site)

You’re looking at the new seancoates.com.

“I’m going to pay attention to my blog” posts on blogs are… passé, but…

I moved this site to a static site generator a few years ago when I had to move some server stuff around, and had let it decay. I spent most of the past week of evenings and weekend updating to what you see now.

It’s still built on Nikola, but now the current version.

I completely reworked the HTML and CSS. Turns out—after not touching it in earnest in probably a decade (‼️)—that CSS is a much more pleasant experience these days. Lately, I’ve been doing almost exclusively back-end and server/operations work, so it was actually a bit refreshing to see how far CSS has come along. Last time I did this kind of thing, nothing seemed to work—or if it did work, it didn’t work the same across browsers. This time, I used Nikola’s SCSS builder and actually got some things done, including passing the Accessibility tests (for new posts, anyway) in Lighthouse (one of the few reasons I fire up Chrome), and a small amount of Responsive Web Design to make some elements reflow on small screens. When we built the HTML for the previous site, so long ago, small screens were barely a thing, and neither were wide browsers for the most part.

From templates that I built, Nikola generates static HTML, which has a few limitations when it comes to serving requests. The canonical URL for this post is https://seancoates.com/blogs/new-site-same-as-old-site. Note the lack of trailing slash. There are ways to accomplish this directly with where I wanted to store this generated HTML + assets (on S3), but it’s always janky. I’ve been storing static sites on S3 and serving them up through CloudFront for what must be 7+ years, now, and it works great as long as you don’t want to do anything “fancy” like redirects. You just have to name your files in a clever way, and be sure to set the metadata’s Content-Type correctly. The file you’re reading right now comes from a .md file that is compiled into [output]/blogs/new-site-same-as-old-site/index.html. Dealing with the “directory” path, and index.html are a pain, so I knew I wanted to serve it through a very thin HTTP handling app.

At work, we deploy mostly on AWS (API Gateway and Lambda, via some bespoke tooling, a forked and customized runtime from Zappa, and SAM for packaging), but all of that seemed too heavy for what amounts to a static site with a slightly-more-intelligent HTTP handler. Chalice had been on my radar for quite a while now, and this seemed like the perfect opportunity to try it.

It has a few limitations, such as horrific 404s, and I couldn’t get binary serving to work (but I don’t need it, since I put the very few binary assets on a different CloudFront + S3 distribution), but all of that considered, it’s pretty nice.

Here’s the entire [current version of] app.py that serves this site:

 import functools
 
 from chalice import Chalice, Response
 import boto3
 
 
 app = Chalice(app_name="seancoates")
 s3 = boto3.client("s3")
 BUCKET = "seancoates-site-content"
 
 REDIRECT_HOSTS = ["www.seancoates.com"]
 
 
 def fetch_from_s3(path):
     k = f"output/{path}"
     obj = s3.get_object(Bucket=BUCKET, Key=k)
     return obj["Body"].read()
 
 
 def wrapped_s3(path, content_type="text/html; charset=utf-8"):
     if app.current_request.headers.get("Host") in REDIRECT_HOSTS:
         return redirect("https://seancoates.com/")
 
     try:
         data = fetch_from_s3(path)
         return Response(
             body=data, headers={"Content-Type": content_type}, status_code=200,
         )
     except s3.exceptions.NoSuchKey:
         return Response(
             body="404 not found.",
             headers={"Content-Type": "text/plain"},
             status_code=404,
         )
 
 
 def check_slash(handler):
     @functools.wraps(handler)
     def slash_wrapper(*args, **kwargs):
         path = app.current_request.context["path"]
         if path[-1] == "/":
             return redirect(path[0:-1])
         return handler(*args, **kwargs)
 
     return slash_wrapper
 
 
 def redirect(path, status_code=303):
     return Response(
         body="Redirecting.",
         headers={"Content-Type": "text/plain", "Location": path},
         status_code=status_code,
     )
 
 
 @app.route("/")
 def index():
     return wrapped_s3("index.html")
 
 
 @app.route("/assets/css/{filename}")
 def assets_css(filename):
     return wrapped_s3(f"assets/css/{filename}", "text/css")
 
 
 @app.route("/blogs/{slug}")
 @check_slash
 def blogs_slug(slug):
     return wrapped_s3(f"blogs/{slug}/index.html")
 
 
 @app.route("/brews")
 @app.route("/shares")
 @app.route("/is")
 @check_slash
 def pages():
     return wrapped_s3(f"{app.current_request.context['path'].lstrip('/')}/index.html")
 
 
 @app.route("/archive")
 @app.route("/blogs")
 def no_page():
     return redirect("/")
 
 
 @app.route("/archive/{archive_page}")
 @check_slash
 def archive(archive_page):
     return wrapped_s3(f"archive/{archive_page}/index.html")
 
 
 @app.route("/rss.xml")
 def rss():
     return wrapped_s3("rss.xml", "application/xml")
 
 
 @app.route("/assets/xml/rss.xsl")
 def rss_xsl():
     return wrapped_s3("assets/xml/rss.xsl", "application/xml")
 
 
 @app.route("/feed.atom")
 def atom():
     return wrapped_s3("feed.atom", "application/atom+xml")

Not bad for less than 100 lines (if you don’t count the mandated whitespace, at least).

Chalice handles the API Gateway, Custom Domain Name, permissions granting (for S3 access, via IAM policy) and deployments. It’s pretty slick. I provided DNS and a certificate ARN from Certificate Manager.

Last thing: I had to trick Nikola into serving “pretty URLs” without a trailing slash. It has two modes, basically: /blogs/post/ or /blogs/post/index.html. I want /blogs/post. Now, avoiding the trailing slash usually invokes a 30x HTTP redirect when the HTTPd that’s serving your static files needs to add it so you get the directory (index). But in my case, I was handling HTTP a little more intelligently, so I didn’t want it. You can see my app.py above handles the trailing slash redirects in the wrapped_s3 function, but to get Nikola to handle this in the RSS/Atom (I had control in the HTML templates, but not in the feeds), I had to trick it with some ugliness in conf.py:

# hacky hack hack monkeypatch
# strips / from the end of URLs
from nikola import post

post.Post._unpatched_permalink = post.Post.permalink
post.Post.permalink = lambda self, lang=None, absolute=False, extension=".html", query=None: self._unpatched_permalink(
    lang, absolute, extension, query
).rstrip("/")

I feel dirty about that part, but pretty great about the rest.

Faculty

Officially, as of the start of the year, I’ve joined Faculty.

Faculty is not just new to me, but something altogether new. It’s also something that feels older than it is. The familiar, experienced kind of old. The good kind. The kind I like.

It was founded by my good friend and long-term colleague Chris Shiflett, whom I’m very happy to be working with, directly, again.

People often ask us how long we’ve worked together, and the best answer I can come up with is “around 15 years”—nearly all of the mature part of my career. Since the early 2000s, Chris and I have attended and spoken at conferences together, he wrote a column for PHP Architect under my watch as the Editor-in-Chief, I worked on Chris’s team at OmniTI, we ran Web Advent together, and we worked collaboratively at Fictive Kin.

A surprised “fifteen years!” is a common response, understandably—that’s an eternity in web time. On a platform that reinvents itself regularly, where we (as a group) often find ourselves jumping from trend to trend, it’s a rare privilege to be able to work with friends with such a rich history.

This kind of long-haul thinking also informs the ethos of Faculty, itself. We’re particularly interested in mixing experience, proven methodologies and technology, and core values, together with promising newer (but solid) technologies and ideas. We help clients reduce bloat, slowness, and inefficiencies, while building solutions to web and app problems.

We care about doing things right, not just quickly. We want to help clients build projects they (and we) can be proud of. We remember the promise—and the output—of Web 2.0, and feel like we might have strayed a little away from the best Web that we can build. I’m really looking forward to leveraging our team’s collective and collected experience to bring excellence, attention to detail, durability, and robustness to help—even if in some small way—influence the next wave of web architecture and development.

If any of that sounds interesting to you, we’re actively seeking new clients. Drop us a note. I can’t wait.

Shortbread

December 1st. Shortbread season begins today.

(Here’s something a little different for long-time blog readers.)

Four years ago, in December, I made dozens and dozens of batches of shortbread, slowly tweaking my recipe, batch after batch. By Christmas, that year, I had what I consider (everyone’s tastes are different) the perfect ratio of flour/butter/sugar/salt, and exactly the right texture: dense, crisp, substantial, and short—not fluffy and light like whipped/cornstarch shortbread, and not crumbly like Walkers.

I present for you, friends, that recipe.

shortbread

Ingredients

  • 70g granulated white sugar (don’t use the fancy kinds)
  • 130g unsalted butter (slightly softened; it’s colder than normal room temperature in my kitchen)
  • 200g all purpose white flour (I use unbleached)
  • 4g kosher salt

Method

In a stand mixer, with the paddle attachment, cream (mix until smooth) the sugar and butter. I like to throw the salt in here, too. You’ll need to scrape down the sides with a spatula, as it mixes (turn the mixer off first, of course).

When well-creamed, and with the mixer on low speed, add the flour in small batches; a little at a time. Mix until combined so it looks like cheese curds. It’s not a huge deal if you over-mix, but the best cookies come when it’s a little crumblier than a cohesive dough, in my experience.

Turn out the near-dough onto a length of waxed paper, and roll into a log that’s ~4cm in diameter, pressing the “curds” together, firmly. Wrapped in the wax paper, refrigerate for 30 minutes.

Preheat your oven to 325°F, with a rack in the middle position.

Slice the chilled log (with a sharp, non-searated knife) into ~1.5cm thick rounds, and place onto a baking sheet with a silicone mat or parchment paper. (If you refrigerate longer, you’ll want to let it soften a little before slicing.)

Bake until right before the tops/sides brown. In my oven, this takes 22 minutes. Remove from oven and allow to cool on the baking sheet.

Eat and share!

Don’t try to add vanilla or top them with anything, unless you like inferior shortbread. (-; Avoid the temptation to eat them right away, because they’re 100 times better when they’ve cooled. Pop a couple in the freezer for 5 mins if you’re really impatient (and I am).

Enjoy! Let me know if you make these, and how they turned out.

API Gateway timeout workaround

The last of the four (see previous posts) big API Gateway limitations is the 30 second integration timeout.

This means that API Gateway will give up on trying to serve your request to the client after 30 seconds—even though Lambda has a 300 second limit.

In my opinion, this is a reasonable limit. No one wants to be waiting around for an API for more than 30 seconds. And if something takes longer than 30s to render, it should probably be batched. “Render this for me, and I’ll come back for it later. Thanks for the token. I’ll use this to retrieve the results.”

In an ideal world, all HTTP requests should definitely be served within 30 seconds. But in practice, that’s not always possible. Sometimes realtime requests need to go to a slow third party. Sometimes the client isn’t advanced enough to use the batch/token method hinted at above.

Indeed, 30s often falls below the client’s own timeout. We’re seeing a practical limitation where clients can often run for 90-600 seconds before timing out.

Terrible user experience aside, I needed to find a way to serve long-running requests, and I really didn’t want to violate our serverless architecure to do so.

But this 30 second timeout in API gateway is a hard limit. It can’t be increased via the regular AWS support request method. In fact, AWS says that it can’t be increased at all—which might even be true. (-:

As I mentioned in an earlier post, I did a lot of driving this summer. Lots of driving led to lots of thinking, and lots of thinking led to a partial solution to this problem.

What if I could use API Gateway to handle the initial request, but buy an additional 30 seconds, somehow. Or better yet, what if I could buy up to an additional 270 seconds (5 minutes total).

Simply put, an initial request can kick off an asynchronous job, and if it takes a long time, after nearly 30 seconds, we can return an HTTP 303 (See Other) redirect to another endpoint that checks the status of this job. If the result still isn’t available after another (nearly) 30s, redirect again. Repeat until the Lambda function call is killed after the hard-limited 300s, but if we don’t get to the hard timeout, and we find the job has finished, we can return that result instead of a 303.

But I didn’t really have a simple way to kick off an asynchronous job. Well, that’s not quite true. I did have a way to do that: Zappa’s asynchronous task execution. What I didn’t have was a way to get the results from these jobs.

So I wrote one, and Zappa’s maintainer, Rich, graciously merged it. And this week, it was released. Here’s a post I wrote about it over on the Zappa blog.

The result:

$ time curl -L 'https://REDACTED.apigwateway/dev/payload?delay=40'
{
  "MESSAGE": "It took 40 seconds to generate this."
}

real    0m52.363s
user    0m0.020s
sys     0m0.025s

Here’s the code (that uses Flask and Zappa); you’ll notice that it also uses a simple backoff algorithm:

@app.route('/payload')
def payload():
    delay = request.args.get('delay', 60)
    x = longrunner(delay)
    if request.args.get('noredirect', False):
        return 'Response will be here in ~{}s: <a href="{}">{}</a>'.format(
            delay, url_for('response', response_id=x.response_id), x.response_id)
    else:
        return redirect(url_for('response', response_id=x.response_id))


@app.route('/async-response/<response_id>')
def response(response_id):
    response = get_async_response(response_id)
    if response is None:
        abort(404)

    backoff = float(request.args.get('backoff', 1))

    if response['status'] == 'complete':
        return jsonify(response['response'])

    sleep(backoff)
    return "Not yet ready. Redirecting.", 303, {
        'Content-Type': 'text/plain; charset=utf-8',
        'Location': url_for(
            'response', response_id=response_id,
            backoff=min(backoff*1.5, 28)),
        'X-redirect-reason': "Not yet ready.",
    }


@task(capture_response=True)
def longrunner(delay):
    sleep(float(delay))
    return {'MESSAGE': "It took {} seconds to generate this.".format(delay)}

That’s it. Long-running tasks in API Gateway. Another tool for our serverless arsenal.

API Gateway Limitations

As I’ve mentioned a couple times in the past, I’ve been working with Lambda and API Gateway.

We’re using it to host/deploy a big app for a big client, as well as some of the ancillary tooling to support the app (such as testing/builds, scheduling, batch jobs, notifications, authentication services, etc.).

For the most part, I love it. It’s helped evaporate the most boring—and often most difficult—parts of deploying highly-available apps.

But it’s not all sunshine and rainbows. Once the necessary allowances are made for a new architecture (things like: if we have concurrency of 10,000, a runaway process’s consequences are amplified, database connection pools are easily exhausted, there’s no simple way to use static IP addresses), there are 4 main problems that I’ve encountered with serving an app on Lambda and API Gateway.

The first two problems are essentially the same. Both headers and query string parameters are clobbered by API Gateway when it creates an API event object.

Consider the following Lambda function (note: this does not use Zappa, but functions provisioned by Zappa have the same limitation):

import json

def respond(err, res=None):
    return {
        'statusCode': '400' if err else '200',
        'body': err.message if err else json.dumps(res),
        'headers': {
            'Content-Type': 'application/json',
        },
    }

def lambda_handler(event, context):
    return respond(None, event.get('queryStringParameters'))

Then if you call your (properly-configured) function via the API Gateway URL such as: https://lambdatest.example.com/test?foo=1&foo=2, you will only get the following queryStringParameters:

{"foo": "2"}

Similarly, a modified function that dumps the event’s headers, when called with duplicate headers, such as with:

curl 'https://lambdatest.example.com/test' -H'Foo: 1' -H'Foo: 2'

…will result in the second header overwriting the first:

{
    
    "headers":
        
        "Foo": "2",
        
    
}

The AWS folks have backed themselves into a bit of a corner, here. It’s not trivial to change the way these events work, without affecting the thousands (millions?) of existing API Gateway deployments out there.

If they could make a change like this, it might make sense to turn queryStringParameters into an array when there would previously have been a clobbering:

{"foo": ["1", "2"]}

This is a bit more dangerous for headers:

{
    
    "headers":
        
        "Foo": [
            "1",
            "2"
        ],
        
    
}

This is not impossible, but it is a BC-breaking change.

What AWS could do, without breaking BC, is (even optionally, based on the API’s Gateway configuration/metadata) supply us with an additional field in the event object: rawQueryString. In our example above, it would be foo=1&foo=2, and it would be up to my app to parse this string into something more useful.

Again, headers are a bit more difficult, but (optionally, as above), one solution might be to supply a rawHeaders field:

{
    
    "rawHeaders": [
        "Foo: 1",
        "Foo: 2",
        
    ],
    
}

We’ve been lucky so far in that these first two quirks haven’t been showstoppers for our apps. I was especially worried about a conflict with access-es, which is effectively a proxy server.

The next two limitations (API Gateway, Lambda) are more difficult, but I’ve come up with some workarounds:

Lambda payload size workaround

Another of the AWS Lambda + API Gateway limitations is in the size of the response body we can return.

AWS states that the full payload size for API Gateway is 10 MB, and the request body payload size is 6 MB in Lambda.

In practice, I’ve found this number to be significantly lower than 6 MB, but perhaps I’m just calculating incorrectly.

Using a Flask route like this:

@app.route('/giant')
def giant():
    payload = "x" * int(request.args.get('size', 1024 * 1024 * 6))
    return payload

…and calling it with curl, I get the following cutoff:

$ curl -s 'https://REDACTED/dev/giant?size=4718559' | wc -c
 4718559
$ curl -s 'https://REDACTED/dev/giant?size=4718560'
{"message": "Internal server error"}

Checking the logs (with zappa tail), I see the non-obvious-unless-you’ve-come-across-this-before error message:

body size is too long

Let’s just call this limit “4 MB” to be safe.

So, why does this matter? Well, sometimes—like it or not—APIs need to return more than 4 MB of data. In my opinion, this should usually (but not always) be resolved by requesting smaller results. But sometimes we don’t get control over this, or it’s just not practical.

Take Kibana, for example. In the past year, we started using Elasticsearch for logging certain types of structured data. We elected to use the AWS Elasticsearch Service to host this. AWS ES has an interesting authentication method: it requires signed requests, based on AWS IAM credentials. This is super useful for our Lambda-based app because we don’t have to rely on DB connection pools, firewalls, VPCs, and much of the other pain that comes with using an RDBMS in a highly-distributed system. Our app can use its inherited IAM profile to sign requests to AWS ES quite easily, but we also wanted to give our developers and certain partners access to our structured logs.

At first, we had our developers run a local copy of aws-es-kibana, which is a proxy server that uses the developer’s own AWS credentials (we distribute user or role credentials to our devs) to sign requests. Running a local proxy is a bit of a pain, though—especially for 3rd parties.

So, I wrote access-es (which is still in a very early “unstable” state, though we do use it “in production” (but not in user request flows)) to allow our users to access Kibana (and Elasticsearch). access-es runs on lambda and effectively works as a reverse HTTPS proxy that signs requests for session/cookie authenticated users, based on the IAM profile. This was a big win for our no-permanent-servers-managed-by-us architecture.

But the very first day we used access-es to load some large logs in Kibana, it failed on us.

It turns out that if you have large documents in Elasticsearch, Kibana loads very large blobs of JSON in order to render the discover stream (and possibly other streams). Larger than “4 MB”, I noticed. Our (non-structured) logs filled with body size is too long messages, and I had to make some adjustments to the page size in the discover feed. This bought us some time, but we ran into the payload size limitation far too often, and at the most inopportune moments, such as when trying to rapidly diagnose a production issue.

The “easy” solution to this problem is to concede that we probably can’t use Lambda + API Gateway to serve this kind of app. Maybe we should fire up some EC2 instances, provision them with Salt, manage upgrades, updates, security alerts, autoscalers, load balancers… and all of those things that we know how to do so well, but were really hoping to leave behind with the new “serverless” (no permanent servers managed by us) architecture.

This summer, I did a lot of driving, and during one of the longest of those driving sessions, I came up with an idea about how to handle this problem of using Lambda to serve documents that are larger than the Lambda maximum response size.

“What if,” I thought, “we could calculate the response, but never actually serve it with Lambda. That would fix it.” Turns out it did. The solution—which will probably seem obvious once I state it—is to use Lambda to calculate the response body, store that response body in a bucket in S3 (where we don’t have to manage any servers), use Lambda + API Gateway to redirect the client to the new resource on S3.

Here’s how I did it in access-es:

req = method(
    target_url,
    auth=awsauth,
    params=request.query_string,
    data=request.data,
    headers=headers,
    stream=False
)

content = req.content

if overflow_bucket is not None and len(content) > overflow_size:

    # the response would be bigger than overflow_size, so instead of trying to serve it,
    # we'll put the resulting body on S3, and redirect to a (temporary, signed) URL
    # this is especially useful because API Gateway has a body size limitation, and
    # Kibana serves *huge* blobs of JSON

    # UUID filename (same suffix as original request if possible)
    u = urlparse(target_url)
    if '.' in u.path:
        filename = str(uuid4()) + '.' + u.path.split('.')[-1]
    else:
        filename = str(uuid4())

    s3 = boto3.resource('s3')
    s3_client = boto3.client(
        's3', config=Config(signature_version='s3v4'))

    bucket = s3.Bucket(overflow_bucket)

    # actually put it in the bucket. beware that boto is really noisy
    # for this in debug log level
    obj = bucket.put_object(
        Key=filename,
        Body=content,
        ACL='authenticated-read',
        ContentType=req.headers['content-type']
    )

    # URL only works for 60 seconds
    url = s3_client.generate_presigned_url(
        'get_object',
        Params={'Bucket': overflow_bucket, 'Key': filename},
        ExpiresIn=60)

    # "see other"
    return redirect(url, 303)

else:
    # otherwise, just serve it normally
    return Response(content, content_type=req.headers['content-type'])

If the body size is larger than overflow_size, we store the result on S3, and the client receives a 303 see other with an appropriate Location header, completely bypassing the Lambda body size limitation, and saving the day for our “serverless” architecture.

The resulting URL is signed by AWS to make it only valid for 60 seconds, and the resource isn’t available without such a signature (unless otherwise authenticated with IAM + appropriate permissions). Additionally, we use S3’s lifecycle management to automatically delete old objects.

For clients that are modern browsers, though, you’ll need to properly manage the CORS configuration on that S3 bucket.

This approach fixed our Kibana problem, and now sits in our arsenal of tools for when we need to handle large responses in our other serverless Lambda + API Gateway apps.

Zappa

For the past few months, I’ve been focused primarily on a Python project that we’re deploying without any servers.

Well, of course that’s not really true, but we’re deploying it without any permanent servers.

The idea of “serverless” architecture isn’t brand new, anymore, but running serverless applications on the AWS infrastructure—which I’ve become very familiar with over the past few years—is still a pretty new concept.

AWS Lambda has been around for a few years, now. It’s a platform that allows you to run arbitary code, in response to events, and pay only for gigabyte-seconds of RAM time. This means that someone else (Amazon) manages the servers, networking, storage, etc.

At some point, Lambda gained the ability to run Python code (instead of just JavaScript, C#, and Java). This piqued my interest, but we didn’t have a whole lot of use for it in building web apps. Nevertheless, we used it to turn SNS notifications into IRC messages, so our #ops IRC channel would get inline notices that our Autoscalers were autoscaling.

In early 2015, I tweeted: “…too bad @AWSCloud Lambda can’t listen (and respond) to HTTP(S) events on Elastic Load Balancer…”.

A while later, Amazon introduced API Gateway, which—amid other functionality that we don’t use very much—had the ability to turn arbitrary HTTP(S) requests into AWS events. Things got interesting. You’ll recall, from above, that Lambda functions can run in response to events.

Interesting in that we could respond to HTTP events, but it wasn’t really possible to use regular tools and frameworks with API Gateway. We’re used to building apps in Flask, not monolithic Python functions that do their own HTTP request parsing.

As time went on, these tools got a little more mature and gained more useful features. I kept thinking back to my tweet where we could just run code, not servers.

Then, in October—increasingly tired of the grind of otherwise-simple operations work—I went searching a bit harder for something to help with the monolithic lambda function problem, and I stumbled upon Zappa. It seemed to be exactly the kind of thing I was looking for. With a bit of boilerplate, hackery, and near-magic, it turns API Gateway request events into WSGI requests, and Flask (plus other Python tools) speaks WSGI. This looked great.

Little did I know that right around that same time, there were some new, barely-documented (at the time), changes to API Gateway that would help reduce the magic and hacky parts of the Zappa boilerplate.

I quickly built my first simple Zappa-based app (it was actually porting a 10-year-old PHP app), and deployed it: paste.website.

We’re using this technology on a very large client project, too. It’s exciting that we’re going to be able to do it without having to worry about things like software upgrades, underutilized servers, and build nodes that cost us money while we’re all sleeping.

I’m not going to let this turn into yet-another-Zappa-tutorial—there are plenty of those out there—but if you’re interested in this kind of thing and hadn’t heard of Zappa before now, well… now you have.

We (mostly Rich) even managed to get it working on the brand-new Python 3.6 target in Lambda.

DST pain

Tonight, in Montreal (and many other North American cities), we change from Standard Time to Daylight Time.

I know we programmers complain about date/time math relentlessly, but I thought it was worth sharing this real-life problem that someone asked me about on Reddit this weekend:

It sounds like this is a serious problem that has effected [sic] you on more than one occasion. Story?

The simplest complicated scenario is: let’s say we have a call scheduled between our team on the east coast of North America and a colleague in the UK at 10AM Montreal time.

Normally Nottingham (UK, same as London time) is 5 hours ahead of Montreal. This is pretty easy. Our British colleague needs to join at 3PM.

However, tonight, we change from EST to EDT in Montreal (clocks move one hour ahead). But the UK will still be on GMT tomorrow. So, now, the daily 10AM call becomes a 2PM call for the Brits.

But this is only for the next 2 weeks, because BST starts on March 26th (BST is to GMT as EDT is to EST). Then, we go back to a 5 hour difference. So we can expect Europeans to show up an hour late for everything this week. Or maybe we’re just an hour early on this side of the Atlantic.

To make this more difficult, we often have calls between not only Montreal and England, but also those two plus Korea and Brazil.

Korea doesn’t employ Daylight Saving Time, so a standing 7AM call in Seoul (5PM in Montreal) becomes a 6PM call in Montreal.

And to even further complicate things, our partners in São Paulo switched FROM DST to standard time on Feb 17. Because they’re in the southern hemisphere the clock change is the opposite direction of ours, on a different day.

So: yes. It has affected our team on many occasions. It’s already very difficult to get that many international parties synced up. DST can make it nearly impossible.

Vermont

I get asked, from time to time, what things I would recommend when visiting Vermont. Here’s my list. I’ll update it as I learn about new gems.

DNS for VMs

Previously we talked about using Vagrant at Fictive Kin and how we typically have many Virtual Machines (VMs) on the go at once.

Addressing each of these VMs with a real hostname was proving to be difficult. We couldn’t just use the IP addresses of the machines because they’re unreasonably hard to remember, and other problems like browser cookies don’t work properly.

In the past, I’ve managed this by editing my local /etc/hosts file (or the Windows equivalent, whatever that’s called now). Turns out this wasn’t ideal. If my hosts don’t sync up with my colleagues’ hosts, stuff (usually cookies) can go wrong, for example. Plus, I strongly believe in setting up an environment that can be managed remotely (when possible) so less-technical members of our team don’t find themselves toiling under the burden of managing an obscurely-formatted text file deep within the parts of their operating systems that they — in all fairness — shouldn’t touch. Oh, and you also can’t do wildcards there.

As I mentioned in a previous post, we have the great fortune of having all of our VM users conveniently on one operating system platform (Mac OS X), so this post will also focus there, but a similar strategy to this one could be used on Windows or Linux, without the shiny resolver bits — you’d just have to run all of your host’s DNS traffic through a VM-managed name resolver; and these other operating systems might have something similar to resolver that I simply haven’t been enlightened to, and surely someone will point out my error on Twitter or email (please).

The short version (which I just hinted at) is that we run a DNS server on our gateway VM (all of our users have one of these), and we instruct the workstation’s operating system to resolve certain TLDs via this VM’s IP address.

We set up the VM side of this with configuration management, in our Salt sates. Our specific implementation is a little too hacky to share (we have a custom Python script that loads hostname configuration from disk, running within systemd), but I’ve recently been tinkering with Dnsmasq, and we might roll that out in the non-distant future.

Let’s say you want to manage the .sean TLD. Let’s additionally say that you have an app called saxophone (on a VM at 192.168.222.16) and another called trombone (on 192.168.222.17), and you’d like to address these via URLs like https://saxophone.sean/ and https://trombone.sean/, respectively. Let’s also say that you might want to make sure that http://www.trombone.sean/ redirects to https on trombone.sean (without the www). Finally, let’s say that the saxophone app has many subdomains like blog.saxophone.sean, admin.saxophone.sean, cdn.saxophone.sean, etc. As you can see, we’re now out of one-liner territory in /etc/hosts. (Well, maybe a couple long lines.)

To configure the DNS-resolving VM (“gateway” for us), with Dnsmasq, the configuration lines would look something like this:

address=/.saxophone.sean/192.168.222.16
address=/.trombone.sean/192.168.222.17

You can test with:

$ dig +short @gateway.sean admin.saxophone.sean
192.168.222.16
$ dig +short @gateway.sean www.trombone.sean
192.168.222.17
$ dig +short @gateway.sean trombone.sean
192.168.222.17

Now we’ve got the VM side set up. How do we best instruct the OS to resolve the new (fake) sean TLD “properly”?

Mac OS X has a mechanism called resolver that allows us to choose specific DNS servers for specific TLDs, which is very convenient.

Again, the short version of this is that you’d add the following line to /etc/resolver/sean (assuming the gateway is on 192.168.222.2) on your workstation (not the VM):

nameserver 192.168.222.2

Once complete (and mDNSResponder has been reloaded), your computer will use the specified name server to resolve the .sean TLD.

The longer version is that I don’t want to burden my VM users (especially those who get nervous touching anything in /etc — and with good reason), with this additional bit of configuration, so we manage this in our Vagrantfile, directly. Here’s an excerpt (we use something other than sean, but this has been altered to be consistent with our examples):

# set up custom resolver
if !File.exist? '/etc/resolver/sean'
  puts "Need to add the .sean resolver. We'll need sudo for this."
  puts "This should only happen once."
  print "\a"
  puts `sudo sh -c 'if [ ! -d /etc/resolver ]; then mkdir /etc/resolver; fi; echo "nameserver 192.168.222.2" > /etc/resolver/san; killall -HUP mDNSResponder;'`
end

Then, when the day comes that we want to add a new app — call it trumpet — we can do all of it through configuration management from the ops side. We create the new VM in Salt, and the next time the user’s gateway is highstated (that is: the configuration management is applied), the Vagrantfile is altered, and the DNS resolver configuration on the gateway VM is changed. Once the user has done vagrant up trumpet, they should be good to point their browsers at https://trumpet.sean/. We don’t (specifically Vagrant doesn’t) even need sudo on the workstation after the initial setup.