Skip to content

Commit d655e06

Browse files
committed
Initial commit of CloudFront and Lambda@Edge demo
0 parents  commit d655e06

File tree

9 files changed

+520
-0
lines changed

9 files changed

+520
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
**/.terraform
2+
**/terraform.tfstate
3+
**/terraform.tfstate.backup
4+
5+
# VSCode
6+
.vscode/
7+
8+
**/*.zip

README.md

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Lambda@Edge On CloudFront
2+
3+
This project demonstrates how to use Lambda@Edge and a CloudFront distribution
4+
to manipulate a request at the edge. We pass a querystring from one HTML page to
5+
a second HTML page using a redirect initiated via the Lambda@Edge viewer request
6+
function.
7+
8+
## Pre-requisites
9+
10+
* Terraform 0.12.x
11+
* An AWS account
12+
13+
## Deploying
14+
15+
**NOTE**: You must use the AWS Northern Virginia region (`us-east-1`) because in
16+
order to replicate Lambda@Edge functions to all regions, AWS requires that you
17+
are deploying your functions into this region only. This requirement may be
18+
relaxed in the future but this is the reality at the moment.
19+
20+
### Prepare to run terraform
21+
22+
These steps include:
23+
24+
* Install Terraform 0.12.x
25+
* Clone this repository
26+
* Change into the `terraform/` directory
27+
* Run `terraform init`
28+
* Exporting environment variable for your AWS_PROFILE and AWS_REGION. This looks
29+
something like:
30+
31+
`export AWS_PROFILE=blah && export AWS_REGION=us-east-1`
32+
33+
Another option is to use [aws-vault](https://github.com/99designs/aws-vault)
34+
to manage AWS credentials. I personally use aws-vault, as you will see in the
35+
commands used below.
36+
37+
### Deploy
38+
39+
1. Run the following commands to deploy:
40+
41+
```
42+
aws-vault exec profile_name -- terraform plan -out=devplan.$(date +%F.%H.%M.%S).out
43+
```
44+
45+
This will create a Terraform plan file, which you can then apply with the command:
46+
47+
```
48+
aws-vault exec profile_name -- terraform apply devplan.2020-01-19.08.44.07.out
49+
```
50+
51+
**NOTE**: Terraform apply` it can take up to 20 or 30 minutes to replicate the
52+
Lambda@Edge function across all the (supported) AWS regions. You may not want to
53+
replicate and that would be fine, but doesn't fit the model of using all AWS
54+
regions to run Lambda at their edge locations. If you don't create the Lambda
55+
function in the `us-east-1` (N. Virginia) region, as far as I understand (at
56+
this time) it will not be replicated to all AWS regions.
57+
58+
The distribution name will be shown in the output at the end, something such as:
59+
60+
```
61+
Outputs:
62+
63+
cloudfront_distribution_id = E2U0H2YCG3AYFU
64+
s3_bucket = terraform-20200118165546224400000003
65+
```
66+
67+
Then you will need to determine the distribution domain name. This can be shown
68+
with the command, using the ID shown in the above output:
69+
70+
```
71+
aws-vault exec --no-session experiments_user1 -- aws cloudfront get-distribution --id E2U0H2YCG3AYFU
72+
```
73+
74+
Look in the JSON output for the Distribution.DomainName, it appears like:
75+
76+
```
77+
"DomainName": "d11e7fgi9rskgz.cloudfront.net",
78+
```
79+
80+
2. After the cloudfront distribution is deployed successfully, you need to place
81+
the HTML and script resources in the S3 bucket. You need to know the S3
82+
bucket previous step to do this. Run the command:
83+
84+
```
85+
aws-vault exec profile_name -- aws s3 cp index.html s3://terraform-20200118165546224400000003/ --acl public-read
86+
aws-vault exec profile_name -- aws s3 cp other.html s3://terraform-20200118165546224400000003/ --acl public-read
87+
aws-vault exec profile_name -- aws s3 cp script.js s3://terraform-20200118165546224400000003/ --acl public-read
88+
```
89+
90+
### Re-deploying
91+
92+
Remember from the above NOTE, when running `terraform apply` it can take up to
93+
20 or 30 minutes to replicate the Lambda@Edge function across all the
94+
(supported) AWS regions. You may not want to replicate and that would be fine,
95+
but doesn't fit the model of using all AWS regions to run Lambda at their edge
96+
locations. If you don't create the Lambda function in the `us-east-1` (N.
97+
Virginia) region, as far as I understand (at this time) it will not be
98+
replicated to all AWS regions.
99+
100+
## Viewing the site
101+
102+
Browse to your index page using the CloudFront DomainName you obtained above. In
103+
our example that URL is(this URL is no longer valid!!! I have since removed the
104+
CloudFront distribution.):
105+
106+
https://d11e7fgi9rskgz.cloudfront.net/index.html
107+
108+
Type a GitHub user name in the input box, and hit the Submit button. You will be
109+
redirected to the other.html page, and the querystring will be used to query the
110+
GitHub API, returning that user's information in the response section of the web
111+
page.
112+
113+
### Troubleshooting
114+
115+
If you don't see the output you expect, i.e. the GitHub user information, there
116+
are a couple ways to find problems.
117+
118+
1. Open the Developer Console in Chrome (or whatever browser you are using)
119+
2. Go to AWS CloudWatch Logs, open the Log Group `/aws/lambda/us-east-1.viewer_request_lambda` or `/aws/lambda/us-east-1.origin_response_lambda`, and view the recent logs, to see if there are server side errors.
120+
121+
## Tearing down the infrastructure
122+
123+
Deleting the replicated Lambda functions takes time, just like deploying the
124+
Lambda to all replicated regions does. In order to perform this deletion in a
125+
timely fashion, you first need to remove the function association in the
126+
CloudFront distribution via the AWS console. Browse to CloudFront in the AWS
127+
console. Then click on your distribution ID from above. Go to the "Behaviors"
128+
tab and select the "Origin or Origin Group" `s3_origin`, then choose "Edit".
129+
Finally at the bottom of the "Edit Behavior" page, click on the X for both the
130+
"Viewer Request" and "Origin Response" CloudFront Events. Once these are removed
131+
select "Yes, Edit", which is apparently the "Save" button in CloudFront.
132+
133+
Now, wait 20-30 minutes before running `terraform destroy` to remove the
134+
infrastucture via Terraform.
135+
136+
This wait time is required because Terraform will attempt to remove the Lambda
137+
function before the CloudFront distribution, and this won't work because the
138+
function association is still defined between the CloudFront distribution and
139+
the Lambda function. We must remove this association first before attempting to
140+
tear down all the infrastructure via Terraform.
141+
142+
If you don't do this step, you would likely see the following error message:
143+
144+
```
145+
Error: Error deleting Lambda Function: InvalidParameterValueException: Lambda was unable to delete arn:aws:lambda:us-east-1:592431548397:function:origin_response_lambda:5 because it is a replicated function. Please see our documentation for Deleting Lambda@Edge Functions and Replicas.
146+
status code: 400, request id: e47befaa-79f8-42c3-84f8-774df27f31d4
147+
```
148+
149+
Either wait 20-30 minutes to re-run `terraform destroy` or remove the
150+
association with the CloudFront distribution as described above.
151+
152+
Enjoy!

index.html

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
7+
<title>Testing Lambda@Edge</title>
8+
9+
</head>
10+
11+
<body bgcolor="FFFFFF">
12+
13+
<div>
14+
<h1>Testing Lambda@Edge</h1>
15+
</div>
16+
17+
<div>
18+
<!-- This will take the input text which should be a valid GitHub username,
19+
and append it to the GET method querystring as a named key value pair. -->
20+
<form action="index.html?" method="GET">
21+
Enter GitHub username to query from GitHub API (example: mbacchi):
22+
<input type="text" length=25 name="username"></input>
23+
<button type="submit" id="submit">Submit</button>
24+
</form>
25+
</div>
26+
</body>
27+
</html>

lambda/origin_response.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import json
2+
from urllib.parse import parse_qs
3+
4+
def handler(event, context):
5+
print("DEBUG: Printing the entire event:")
6+
print(event)
7+
print("DEBUG: Adding security headers to CloudFront response")
8+
script_src = "https://" + event['Records'][0]['cf']['config']['distributionDomainName']
9+
connect_src = "https://api.github.com"
10+
11+
csp = "default-src 'none'; img-src 'self'; script-src 'self' {}; style-src 'self'; object-src 'none'; connect-src {}".format(script_src, connect_src)
12+
13+
response = event['Records'][0]['cf']['response']
14+
headers = response["headers"]
15+
headers['referrer-policy'] = [{'key': 'Referrer-Policy', 'value': 'same-origin'}]
16+
headers['strict-transport-security'] = [{'key': 'Strict-Transport-Security', 'value': 'max-age= 63072000; includeSubdomains; preload'}]
17+
headers['content-security-policy'] = [{'key': 'Content-Security-Policy', 'value': csp}]
18+
headers['x-content-type-options'] = [{'key': 'X-Content-Type-Options', 'value': 'nosniff'}]
19+
headers['x-frame-options'] = [{'key': 'X-Frame-Options', 'value': 'DENY'}]
20+
headers['x-xss-protection'] = [{'key': 'X-XSS-Protection', 'value': '1; mode=block'}]
21+
22+
print("DEBUG: distributionDomainName: {}".format(event['Records'][0]['cf']['config']['distributionDomainName']))
23+
24+
print("DEBUG: Returning response: {}".format(response))
25+
return response

lambda/viewer_request.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from urllib.parse import parse_qs, urlencode
2+
from re import search
3+
4+
def handler(event, context):
5+
print(event)
6+
7+
request = event['Records'][0]['cf']['request']
8+
9+
if request.get('uri'):
10+
if '/index.html' in request.get('uri'):
11+
if request.get('querystring'):
12+
print("DEBUG: found querystring: {}".format(request.get('querystring')))
13+
14+
print("DEBUG: distributionDomainName: {}".format(event['Records'][0]['cf']['config']['distributionDomainName']))
15+
redirectURL = 'https://' + event['Records'][0]['cf']['config']['distributionDomainName'] + '/other.html?' + request.get('querystring')
16+
print("DEBUG: redirectURL: {}".format(redirectURL))
17+
18+
response = {
19+
'status': '302',
20+
'statusDescription': 'Found',
21+
'headers': {
22+
'location': [{
23+
'key': 'Location',
24+
'value': redirectURL
25+
}],
26+
'cache-control': [{
27+
'key': 'Cache-Control',
28+
'value': 'max-age=300'
29+
}],
30+
'content-security-policy': [{
31+
'key': 'Content-Security-Policy',
32+
'value': "unsafe-inline"
33+
}]
34+
}
35+
}
36+
37+
print("DEBUG: Returning response: {}".format(response))
38+
return response
39+
return request

other.html

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
7+
<title>Other Page Lambda@Edge</title>
8+
9+
</head>
10+
11+
<body bgcolor="FFFFFF">
12+
13+
<div>
14+
<h1>Other Page Lambda@Edge</h1>
15+
</div>
16+
17+
<div>
18+
This is the s3cond page.
19+
</div>
20+
<br><br>
21+
<div>
22+
The username sent to this page via the querystring was:
23+
<div class="querystring">
24+
Placeholder...
25+
</div>
26+
<br>
27+
<br>
28+
The response from https://api.github.com/user/ was:
29+
<div class="userinfo">
30+
Placeholder...
31+
</div>
32+
</div>
33+
</body>
34+
</html>
35+
36+
<script src="./script.js"></script>

script.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
window.addEventListener("load", getuuid());
2+
3+
function getuuid() {
4+
var url=window.location.href
5+
console.log(`url: ${url}`)
6+
var after_question_mark = url.substring(url.lastIndexOf('?') + 1, url.length);
7+
8+
console.log(`after_question_mark: ${after_question_mark}`)
9+
var username = after_question_mark.split('=')[1]
10+
11+
awaitGitubRequest(username)
12+
}
13+
14+
async function awaitGitubRequest(username) {
15+
let response = await fetch('https://api.github.com/users/' + username);
16+
17+
let data = await response.json();
18+
19+
document.getElementsByClassName("querystring")[0].innerHTML = username
20+
document.getElementsByClassName("userinfo")[0].innerHTML = JSON.stringify(data, null, " ")
21+
}

0 commit comments

Comments
 (0)