superacidjax / ember-cli-deploy-fastly

An Ember Plugin to Deploy index.html to a Fastly Edge Dictionary
MIT License
0 stars 0 forks source link

Open Discussion: Architecture #1

Open jamesarosen opened 8 years ago

jamesarosen commented 8 years ago

I haven't dug into ember-cli-deploy for about a year -- since when the v0.5.0 rewrite was in-progress -- so please forgive me if I'm off-track here. I want to help, but I'm not an expert on ember-cli-deploy.

Fastly Edge Dictionaries can't take a huge amount of data. Each item value can only be up to 8000 characters.

So we have two options for a Fastly index plugin:

  1. Put whole files in the dictionary
    • on build, if index.html is > 8000 characters, throw an error and stop the build
    • on build, if index.html is > 6000 characters (customizable?), print a warning
    • on build, write commit abcdef's index.html with PUT /service/service_id/dictionary/index-html/item/abcdef
    • to activate abcdef, write the current pointer with PUT /service/service_id/dictionary/index-html/item/current with abcdef
  2. Put whole files somewhere else
    • on build, rename index.html to index-abcdef.html
    • let the asset plugin upload index-abcdef.html to S3 or whatever persistent store you're using
    • to activate abcdef, write the current pointer with PUT /service/service_id/dictionary/index-html/item/current with abcdef

In the first case, it will also be important to prune the /service/service_id/dictionary/index-html/item/* entries regularly. If that dictionary gets large (MB), it could degrade the quality of service.

superacidjax commented 8 years ago

So the basic strategy is to copy the idea behind ember-cli-deploy-redis, which basically does the same thing, except ours will make a call to create/update an edge dictionary. So that seems to be straightforward, except for the character limiting.

Also, adding app.options.storeConfigInMeta = false; to ember-cli-build.js should also help keep the index.html to a smaller size. But the pointer idea is a great one.. That could even be automated: if index exceeds 8000 chars, it automatically creates an index-abcdef.html/etc and configures the pointer correctly. That would be killer as we really wouldn't need the warning or build failure -- just a notification as to what's happening.

superacidjax commented 8 years ago

Also @jamesarosen do you know if it's possible to programmatically update custom vcl on Fastly? -- because then the plugin could actually create the correct vcl if it doesn't already exist, rather than going to the Fastly UI and doing the upload manually. It would be a great UX since this would "just work" without users having to worry about directly creating custom vcl.

jamesarosen commented 8 years ago

do you know if it's possible to programmatically update custom vcl on Fastly?

It's definitely possible -- provided your account has custom VCL enabled. That's a request that has to go through Fastly customer service. See the custom VCL object API docs, Mixing and matching Fastly VCL with custom VCL, and Uploading custom VCL for more info.

You can do most of what you want with conditions and headers, which lets you avoid thorny custom VCL. The place where you need custom VCL is for routing requests to index.html if they didn't match an asset. You want something akin to nginx's try_files directive. Essentially, you would want (if it existed)

sub vcl_fetch {
  tryFiles(req.http.url, "index.html");
}

Unfortunately, that does not. The equivalent in VCL is a bit complicated. You need to do the following:

  1. in vcl_recv set a marker that you haven't looked for index.html yet for this request
  2. let the request process manually; if it's an asset like /assets/app-abcdef.js, it will get a 200 from the asset origin (e.g. S3).
  3. in vcl_error, check whether you got a 404 and haven't looked for index.html yet; if so, set the URL to index.html and restart the request

In VCL, that looks like

sub vcl_recv {
  if (req.restarts == 0) {
    // clear at the beginning so attackers can't tamper with the routing:
    unset req.http.x-ember-looked-for-index-html;
  }
  ...
}

sub vcl_error {
  if (obj.status == 404 && !req.http.x-ember-looked-for-index-html) {
    set req.url = "/index.html";
    return restart();
  }
}

Of course, that doesn't do the actual handling of serving index.html from an Edge Dictionary. It might be possible to do that without custom VCL, but custom VCL is the most straightforward and readable way of doing it. Something like

sub vcl_recv {
  ...

  if (req.restarts == 0) {
    // clear at the beginning so attackers can't tamper with the routing:
    unset req.http.ember-index-version;
  }

  if (req.url = "/index.html") {
    set req.http.ember-index-version = table.lookup("ember_index_versions", "current", "");
    if (req.http.ember-index-version == "") {
      error 404 "Not Found"; # no current version
    } else {
      // use errors for control-flow, because that's how Varnish works!
      error 810 req.http.ember-index-version;
    }
  }
}

sub vcl_error {
  if (obj.status == 810) {
    if (table.lookup("ember_index_versions", obj.response, "") == "") {
      # current version doesn't exist
      set obj.status = 404;
      set obj.response = "Not Found";
    } else {
      set obj.status = 200;
      set obj.response = "OK";
      synthetic table.lookup("ember_index_versions", obj.response, "");
    }
    return deliver;
  }
}
jamesarosen commented 8 years ago

The try_files discussion is to support apps that use the History Location. There's no way in VCL to distinguish GET /robots.txt (an asset served from origin) from GET /food/12 (an Ember route that runs in the HTML page). Without the retry-on-404, there's no way to serve the app for anything but /

jamesarosen commented 8 years ago

Also, adding app.options.storeConfigInMeta = false; to ember-cli-build.js should also help keep the index.html to a smaller size.

Something I've been toying with is creating an ember-cli-esi-config addon that does the following:

  1. sets app.options.storeConfigInMeta = false;
  2. writes a file called config.html to the build that just has the <meta> tag that would have otherwise been written to index.html
  3. adds <esi:include src="/config.html"> to index.html via contentFor('head')
  4. adds a basic ESI middleware to the ember serve stack so ESI works in development

You could write to config.html to change the configuration of your app without redeploying. Or you could have multiple configurations (config-staging.html, config-production.html) and use the same HTML and JS for each stage, but different configuration. (This would require conditionally routing /config.html to /config-staging.html or /config-production.html based on the request in VCL or another reverse proxy, but that's easy.)

See also Using ESI, Part 1.