Using a CDN to speed up MFEs

I’ve been playing around with the idea of using a CDN service to speed up the MFE pages, in the end it’s just a javascript bundle so it seems like a good target for this.

I’ve done this with edxapp simply by using cloudfront and setting the LMS as the origin server. I’ve found this approach to be pretty straightforward, I just need to configure my cloudfront distribution and add STATIC_URL = "https://d1234567890.cloudfront.net/static/" to my settings.

I tried to do something similar with the MFEs, but ran into some blockers. The approach that seemed more similar to the previous one was to use output.publicPath and set it to the CDN URL. The problem is that it doesn’t seem to be particularly easy to modify publicPath in the tutor-mfe plugin at the moment. Even then, after applying manual changes and setting ENV PUBLIC_PATH='https://d1234567890.cloudfront.net/{{ app_name }}/', I was able to load the assets from cloudfront but I encountered a blank page when accessing the app. Upon further investigation, the error seems to be related to the history object which expects and actual path instead of the full URL.

In the end, modifying frontend-build to use an additional variable when configuring output.publicPath did the trick.

One of my teammates faced the same issue and actually opened a PR (frontend-build#398).

I was wondering what kind of deployment strategies do you use for higher traffic installations, in particular those of you using tutor. I think this approach is quite straightforward once the frontend-build changes land, you would need a new MFE image but that doesn’t seem like a big deal.

I would like to push that PR forward, maybe have a discussion at the next BTR meeting?

4 Likes

This looks like a great initiative, and somewhat similar to some of the work by @ghassan. Do you have some numbers you can share in terms of performance improvement?

Yes this relate to an initiative I suggested in the DevOps meeting back in the conference, it’s detialed here Serving MFEs through Cloudflare · Issue #14 · openedx/wg-devops · GitHub .

I suggested using Cloudflare but it’s really doesn’t matter, be it, AWS Cloudfront, Fastly…etc assuming the end goal is to server the MFEs assests from a CDN.

It’s worth noting that by average a MFE assests is ~5MB, and compressed it’s ~1MB, so the effect of this on perforamance would depend on the connection speed/latency of the user on the platform and the load on the server it’s just an educated guess I haven’t tested it yet.

I think though there is a slight difference between what you are suggesting and what I had in mind, I wanted not only to enable the MFEs assests to be served through CDNs but also to simplify the workflow of an MFEs to get deployed, that is each MFE would be deployed/built by an external service and the URL of the the MFE would be unique. e.g. instead app.myopenedx.com/learning it would be learning.myopenedx.com and with this approach you no longer need to modifiy the publicPath. The only thing is that it require more configuration when setting up the platform.

Regarding how the assets get uploaded to Cloudfront did you use a webpack plugin to do that? may this one GitHub - matrus2/webpack-s3-uploader: Upload all your assets to AWS S3 during webpack build.

Do you have some numbers you can share in terms of performance improvement?

I only ran a small test in a k8s cluster that retrieved the static files used by the learning MFE (the ones listed when you check your network tab while accessing an wrong route /learning/nogood) with X amount of simultaneous connections. Looping through these files for 1 minute with 20 connections I made 3.1k requests and by grouping the files I got a response time (p95) of 2.35s. Doing the same test with 80 connections I made 4k requests and the p95 increased up to 9s. Repeating the 80 connections using cloudfront is obviously way faster with 20k+ requests and p(95) of 700ms.

This amount of load is obviously excessive in most scenarios, but it does show degradation in the performance and using a CDN seems like an easy fix. I’m not that concerned about speeding up delivery for users in ideal conditions where their own connection is the bottleneck, but rather when my service is under heavy load (an exam for example).

Regarding how the assets get uploaded to Cloudfront did you use a webpack plugin to do that? may this one GitHub - matrus2/webpack-s3-uploader: Upload all your assets to AWS S3 during webpack build.

What I did was set the MFE domain (apps.myopenedx.com) as the origin for my distribution, at first cloudfront would hit my server for the files but after that it will cache them and serve them to the users.

I think though there is a slight difference between what you are suggesting and what I had in mind, I wanted not only to enable the MFEs assests to be served through CDNs but also to simplify the workflow of an MFEs to get deployed, that is each MFE would be deployed/built by an external service and the URL of the the MFE would be unique.

I would like to eventually land on a more sophisticated method that allows me to have independent deployments for each MFE, especially when we are working on a single fork but have to rebuild all of them. But I think this approach offers minimal friction at the moment.

I also have a use case for hosting the MFEs on certain paths: we want to use multiple sites, and serving the MFE in lms.myopenedx.com/learning is really handy.

1 Like

@MoisesGonzalezS
I applied the fix Jlc/parser to know path by johanseto · Pull Request #568 · openedx/frontend-platform (github.com) and I’m setting this environment variable like so when building out the MFE frontend-app-grading.

ENV PUBLIC_PATH='https://XXXXXXXXXXXX.cloudfront.net/gradebook/'
# ENV PUBLIC_PATH='/gradebook/'

For some reason the header and footer load fine but the content and XHR calls to get information from the LMS APIs don’t seem to be called. I don’t see any errors in the logs files either or in the browser console.

Could you help me figure out why the gradebook MFE content won’t load. Learning, account, profile frontends load fine without error.

All the JavaScript files seem to load fine with HTTP 200 response I see information in the files.

It looks like the frontend-component-header and frontend-component-footer load successfully.

I just have publicPath: process.env.PUBLIC_PATH || '/', in the /frontend-build/config/webpack.prod.config.js for my release and I’m guessing that this PUBLIC_PATH is the full path the Cloudfront URL.
image

The frontend-app-learning loads no problem with the following configuration:

ENV PUBLIC_PATH='https://XXXXXXXXXXXX.cloudfront.net/learning/'
# ENV PUBLIC_PATH='/learning/'

After building out the MFE with tutor images build mfe it copies the /dist files for each MFE up to an S3 bucket.

I created a Lambda function that would be called on the origin request of the Cloudfront behavior and check to see if the request was a path and route to the corresponding S3 bucket directory for the given MFE app. The example below looks for /gradebook/ path and routes the request to the S3 ./gradebook/index.html page.

IIRC, the gradebook routing was kind of wonky, you may need to backport this commit: fix: Route with PUBLIC_PATH · openedx/frontend-app-gradebook@cb65877 · GitHub

Keep in mind that change was done after the react router upgrade. I don’t know how well it plays with the previous version.

1 Like

@MoisesGonzalezS
Thanks for this solution. This seemed to help get the gradebook to load.

Even after the path update above, the Gradebook MFE wouldn’t load until I removed the exact parameter then everything worked as expected.

            <Route
              exact
              path="/:courseId"
              component={GradebookPage}
            />

We’re still on the following, however, v6 of react-router-dom mentions the following. Anyway, I noticed that exact is gone anyhow in the redwood release frontend-app-gradebook/src/App.jsx at open-release/redwood.master · openedx/frontend-app-gradebook (github.com) but that’s probably because this release is using version 6.

Upgrading from v5 v6.25.1 | React Router

  • <Route exact> is gone. Instead, routes with descendant routes (defined in other components) use a trailing * in their path to indicate they match deeply
    "react-router": "5.2.0",
    "react-router-dom": "5.2.0",

@MoisesGonzalezS
Did you run into issues with the gradebook MFE looking for the first parameter for courseId. We used a Cloudfront distribution that pointed to S3 origin with /gradebook files in their own directory. When calling that endpoint with https://apps.maple3.ew-dev.com/gradebook/course-v1:CA+FAA-ACS-AM-IA-ACE+DEVELOPMENT, the MFE app picked this courseId up as 'gradebook' value and broke part of the logic in the app.

The MFE container in a default tutor local install builds all the MFE apps and puts the output at /openedx/dist with a corresponding directory for each app. I also noticed that calls has this defined for the gradebook. Notice the uri strip_prefix /gradebook portion. Is this how it prevents the MFE gradebook app from keep that value in the URL?

# Caddy configuration at /etc/caddy/Caddyfile for the Gradebook MFE.
    @mfe_gradebook {
        path /gradebook /gradebook/*
    }
    handle @mfe_gradebook {
        uri strip_prefix /gradebook
        root * /openedx/dist/gradebook
        try_files /{path} /index.html
        file_server
    }

I think the only other alternative is to keep each MFE in it’s own separate Cloudfront/MFE configuration. I tried putting all the built MFE files into a single Cloudfront/S3 configuration, but I seem to run into the gradebook MFE breaking because of how the URL is structured.

Let me know how you handled this.

@MoisesGonzalezS
I was able to get Cloudfront/S3 for the MFE gradebook endpoint working like so. It may still be helpful to see how you’ve done it on your end just to confirm though.

A separate Cloudfront distribution was set up to handle the gradebook.chooseaerospace.maple3.ew-dev.com domain with SSL certificate generated by AWS Certificate Manager. For now, I’m just testing the grading MFE endpoint for one of our branded sites.

The Origin path = '/gradebook' tells the Cloudfront request to forward all traffic to that S3 bucket /gradebook directory path.

The custom error page for error 403 Forbidden response from Cloudfront will be generated after the user tries to access a gradebook MFE React route path="/:courseId" of value /course-v1:CA+FAA-ACS-AM-IA-ACE+DEVELOPMENT. The Cloudfront error page will redirect the user to the React entry page /gradebook/index.html on S3 origin and a HTTP 200 success response is returned.

Here is the page working without error concerning the <h3>course-id</h3> and link << Back to Dashboard to take the learner back to the LMS MFE for the course.