The Problem
I had a request before to deploy multi-backend services which will be used by SPA ( Angular Frontend ). It was needed to deploy these services under a single hostname to make it easy for frontend APIs and to secure it by not directly exposing the origin server.
What do we have?
A multi-services are deployed on a single server (EC2) as Docker containers and every service is connected to the RDS database.
The Solution ( Simple )
This is a simple solution to use as a PoC and also can be used for low-budget projects or non-production environments which doesn’t care about high availability and failover.
Advanced Solution ( Recommended )
If you’re looking forward the same solution but with considering high availability it’s recommended to follow the below design as there won’t be a single point of failure unlike the simple one which maybe used by client who has a low-budget and low traffic with no guarantee of high availability 🤯 !
Continue with Simple Solution
Let’s continue with simple solution because our target is to create a PoC of manipulating host header by lambda@edge attached to Cloudfront.
So, I will use a single subdomain to serve the backends e.g. https://backend.xyz.com the domain DNS is managed by AWS Route 53 but you can use your own like Cloudflare.
This subdomain will point to a CloudFront Distribution with two essential configurations.
- Lambda@Edge attached to Origin request ( more details later )
- The origin with linker hostname ( The origin server hostname which resolves its IP as you can’t use the server IP address as an origin )
Lambda@Edge
Will be responsible for the editing hostname of the origin request depending on the referer header, for example, if we have 2 frontends with the following domain names :
- games.xyz.com
- news.xyz.com
Every request will contain the domain name in the referer header.
So, all we need to do is to match the request to the right internal backend
- Request from games.xyz.com –> referer : games.xyz.com –> reroute to origin with host:games.backend.internal
- Request from news.xyz.com –> referer : news.xyz.com –> reroute to origin with host:news.backend.internal
So we will manipulate the original host header value from backend.xyz.com to the internal hostname configured inside the Nginx in the origin server.
Configurations / Steps
AWS ACM – For SSL
Route 53
Just add an A Record that points to your EC2 public IP like : backend-linker.xyz.com — > 41.244.52.11 as It will be used as the origin for CloudFront Distribution.
Lambda@Edge
- Tutorial: Creating a simple Lambda@Edge function – Amazon CloudFront
- Consider Lambda runtime is NodeJs 14.x
- Add the following code to your main index.js or the file will contain the handler.
- Tune the domains to match your needs.
'use strict'; exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; const headers = request.headers const referer = request.headers['referer'][0].value; const refererDomain = (new URL(referer)).hostname; const baseDomain = refererDomain.split(".").slice(-2).join('.'); //Dynamic get backend name based on frontend subdomain var frontendName = refererDomain.split(".")[0]; if(baseDomain == 'xyz.com'){ const finalOrigin = frontendName+'.backend.internal' request.headers['host'] = [{ key: 'host', value: finalOrigin }]; }else{ const response = { status: '403', statusDescription: 'Access Denied', }; return response; } callback(null, request); };
CloudFront
- Create a distribution by following AWS Doc: Creating a distribution – Amazon CloudFront
- Add linker origin to CloudFront Dist.
- Go to Behavior and edit the default
Configure it as the following as we need to pass some headers and also add the Lambda function to Origin request by pasting your lambda ARN, don’t copy mine 😃.
Nginx
We need to setup Nginx as a reverse proxy to reroute based on the hostname to the local docker container port
server { listen 80; server_name games.backend.internal news.backend.internal; // Allow CORS add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; #SSL-START SSL related configuration, do NOT delete or modify the next line of commented-out 404 rules #error_page 404/404.html; #SSL-END location / { if ($host = 'games.backend.internal') { proxy_pass http://127.0.0.1:9091; } if ($host = 'news.backend.internal') { proxy_pass http://127.0.0.1:9092; } } # Forbidden files or directories location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md) { return 404; } # Directory verification related settings for one-click application for SSL certificate location ~ \.well-known{ allow all; } location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { expires 30d; error_log /dev/null; access_log off; } location ~ .*\.(js|css)?$ { expires 12h; error_log /dev/null; access_log off; } access_log /www/wwwlogs/backend.internal.log; error_log /www/wwwlogs/backend.internal.error.log; }