So on my journey to learn more about re-platforming and micro-services I came across that you could actually ‘host’ your react front-end application in an s3 bucket.
That’s pretty cool, its part of a strategy to go serverless for a lot of websites.
So I will be starting to do this with the site that I have too, www.yazii.co.uk.
To do this, I will follow a blog post here and the video here (which is doing the same thing). It’s impressive, within 8 minutes you’re able to host a basic site live.
The only part which I had slightly different on my end to the video is the s3 bucket permissions.
In order to add the S3 Policy permission, you have to disable the ‘Block all public access’ checkbox:
You’d need to disable the block as essentially you’re allowing users to access the website.
As usual, I also create a new repository in bitbucket to hold this new project and sync it.
I will look to now extract the react application from my Rails project and port it over to the s3 host.
In my journey I am simply copying all contents from javascript
directory of rails project. I have to rename a couple of things and resolve dependencies.
I would suggest installing the dependencies manually as to update them, but this will take longer than potentially just copying relevant ones from package.json
. This may be a good opportunity to upgrade several libraries, but do note that installing latest does not mean they will always work.
After a little bit of playing around, changing some of the imports location, such as for images as they belonged in ‘assets’ folder in Rails project, I manage to have a react application compiling within 30 minutes!
The next part is to have this project be able to communicate with my Rails server seamlessly. Out of the box, you will have something similar to this:
I was able to quickly resolve this in development environment with the use of ‘proxy‘.
All I needed to do was add a line to package.json
to tell it where to look:
"proxy": "http://localhost:3000"
So to get started, first I start up my rails server in API mode, I do this by adding:
config.api_only = true
to application.rb
I then start my server with rails c
I then start my react app with yarn start
– it will tell me that something is already running on localhost:3000
, do I want to use another port? -> yes
so now open up the application and give it a test
Now I have react app running on port 3001
and rails server on 3000
. The react app is able to send requests to rails and get responses as before.
I still have several things to move from application.html.erb
but after moving those, that’s it.
Having said that, moving the final parts is tricky, as it utilised RoR controller features and I would now have to create new API requests to fetch them.
This includes:
- Page canonical url header
- Page description metadata
- Page created at metadata (datePublished)
These are not big things, however these will require me to make some additional implementations for the re-platform. It can be much worse!
I found that I could utilise a library called ‘Helmet‘. The easiest blog post to implement it that I found was this one which also highlighted its importance in SEO.
In a small summary, all I needed to do was install the library, import it and add a small addition fairly close to root of the page I required it in:
return (
<Container className={classes.container} maxWidth="lg">
<Helmet>
<meta charSet="utf-8" />
<meta itemprop="datePublished" content={this.state.lastUpdatedAt} />
<title>{this.state.title}</title>
<link rel="canonical" href={get(['our_url'], this.state.allProperties)} />
<keywords>{get(['keywords'], this.state.allProperties)}</keywords>
</Helmet>
<MainAppBar title={this.state.title}/>
{ this.displayMainContent() }
</Container>
);
the <Helmet>
parts are relevant pieces.
Once happy that headers are now fine, I will have to simulate the sending of requests, i.e. in my fetch requests I will no longer use relative paths.
So earlier I included: "proxy": "http://localhost:3000"
in my package.json
but now I will rely on full path instead. Here’s a small example:
const rootUrl = "http://localhost:3000/"
export function getPostings() {
return fetch(rootUrl + "/api/v1/public/postings/", {
method: "GET",
headers: headers,
credentials: "same-origin"
});
}
Note I now added rootUrl +
.
If you run the application after making such a modification, you may find yourself coming across an error similar to this:
(FAILED) Access to fetch at 'http://localhost:3000//api/v1/public/postings/searches' from origin 'http://localhost:3001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
It may otherwise complain with 404 Not found
error.
After some small digging, I found this rack-cors gem will sort it. Simply follow the install / setup instructions and restart your server.
Bear in mind that disabling these settings, I open up the application to potentially malicious attacks, so be careful of doing that. If you’re unfamiliar with what CORS are, read about them here.
I will be soon setting up API keys for the application to use and different mechanisms for handling security. It would be best practice to sort these issues prior to releasing to prod.
In the meantime, I will disable API only mode in my rails app so that for temporary purposes (while my DNS is still pointing to original) my app will still work:
# config.api_only = true
And I will deploy the updated rails code and the react app.
After trying to open it, I came across this issue:
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>3225CC51D1066028</RequestId <HostId>dPW3tm3Xi8wbST0mkndz7zC6a/ybL6hZmIGhkGs8wmgZ3Ol0NdmLrsZa2qguGC8wtOrJjVJ9vEk=</HostId>
</Error>
What I noticed was that if you add /index.html
to your bucket location then it will work ok. However when the URL changes and you refresh for instance, you will have access denied.
This is by design and what I needed to do in order to overcome this is combine it with AWS CloudFront.
I took some information from this aws tutorial.
I also added 2 configurations for error pages:
This seemed to work pretty well and it actually worked.
The one additional thing that I noticed required fixing was my file URLs from Rails. I used relative paths from my Active Storage and the host was not setup, so the logos were not loading correctly.
To fix this in rails you will need to make the additional modifications:
development.rb ->
routes.default_url_options[:host] = 'localhost:3000'
production.rb ->
routes.default_url_options[:host] = 'yazii.co.uk'
Then when retrieving the url for object, you put only_path: false
For example:
Rails.application.routes.url_helpers.rails_representation_url(
object.organization_entity.org_logo.variant(resize_to_limit: [LOGO_MAX_WIDTH, LOGO_MAX_HEIGHT]), only_path: false)
With this in place, everything appears to be working as expected!
Sample served from cloudfront:
Notice that I’m using cloudfront URL, not the S3 bucket URL.
You get this from the general tab of your cloudfront settings.
And ofcourse the application still works as expected from Rails as usual:
Note for the time being, my api_only
mode is disabled.
The last steps remaining are:
- Go to domain settings and switch it to point to cloudfront
- If everything is working as expected, disable the front end in Rails (decomission)
- Start applying additional security to APIs as they are now disabled
But this has taken about 1 full day to debug and configure, which in the grand scheme of things was much faster than I expected!