From 33549060412f05344dc40c4afc370595c028b30d Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Sun, 27 Jul 2025 15:36:13 -0500 Subject: [PATCH 01/16] fix sciunit cope with aws s3 --- sciunit2/command/copy.py | 4 ++-- sciunit2/s3.py | 35 +++++++++++++++++++++++++++++++++++ sciunit2/workspace.py | 5 +++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 sciunit2/s3.py diff --git a/sciunit2/command/copy.py b/sciunit2/command/copy.py index 5044b8c..262a0db 100644 --- a/sciunit2/command/copy.py +++ b/sciunit2/command/copy.py @@ -4,7 +4,7 @@ from sciunit2.exceptions import CommandLineError import sciunit2.workspace import sciunit2.archiver -import sciunit2.ephemeral +import sciunit2.s3 from sciunit2.util import quoted_format from getopt import getopt @@ -29,7 +29,7 @@ def run(self, args): if optlist: print(fn) else: - print(sciunit2.ephemeral.live(fn)) + print(sciunit2.s3.live(fn)) def note(self, user_data): return quoted_format('Copied sciunit at {0}\n', user_data) diff --git a/sciunit2/s3.py b/sciunit2/s3.py new file mode 100644 index 0000000..77a366a --- /dev/null +++ b/sciunit2/s3.py @@ -0,0 +1,35 @@ +import boto3 +import tempfile +import shutil +import os +from datetime import datetime +from retry import retry +import urllib + + +def live(fn, bucket="sciunit2-talha"): + """ + Uploads a file to S3 and returns a pre-signed download URL. + """ + s3 = boto3.client('s3') + key = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "/" + fn + s3.upload_file(fn, bucket, key) + url = s3.generate_presigned_url( + 'get_object', + Params={'Bucket': bucket, 'Key': key}, + ExpiresIn=86400 * 3 # 3 days + ) + return url + +@retry(urllib.error.HTTPError, tries=3, delay=0.3, backoff=2) +def fetch(url, base): + """ + Downloads a file from a pre-signed S3 URL and returns a file-like object. + """ + import requests + with requests.get(url, stream=True) as resp: + resp.raise_for_status() + f = tempfile.NamedTemporaryFile(prefix=base, dir="") + shutil.copyfileobj(resp.raw, f) + f.seek(0) + return f \ No newline at end of file diff --git a/sciunit2/workspace.py b/sciunit2/workspace.py index d8c54dd..62f5137 100644 --- a/sciunit2/workspace.py +++ b/sciunit2/workspace.py @@ -7,6 +7,7 @@ import sciunit2.records import sciunit2.archiver import sciunit2.ephemeral +import sciunit2.s3 import sciunit2.wget import os @@ -62,6 +63,8 @@ def _is_path_component(s): def _is_once_token(s): return re.match(r'^[\w]+#$', s) +def _is_s3_url(s): + return re.match(r'^https://[a-zA-Z0-9.\-]+\.s3\.amazonaws\.com/.*$', s) def location_for(name): return os.path.expanduser('~/sciunit/%s' % name) @@ -110,6 +113,8 @@ def open(s): p = _extract(s) elif _is_once_token(s): p = _extract(sciunit2.ephemeral.fetch(s, location_for('tmp'))) + elif _is_s3_url(s): + p = _extract(sciunit2.s3.fetch(s, location_for('tmp'))) elif _is_path_component(s): p = location_for(s) if not os.path.isdir(p): From 3b9b043aff5b0259357379f9420c206348328622 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Sun, 27 Jul 2025 15:38:47 -0500 Subject: [PATCH 02/16] add boto3 requiremts --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6434cee..7ce7a1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ hs_restclient contextlib2 tqdm>=4.17.1 backports.tempfile +boto3 \ No newline at end of file From 4edc72fb2e550d3c310d5fc14e8adb7f2fdaa8ee Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Thu, 28 Aug 2025 16:04:54 -0500 Subject: [PATCH 03/16] using CF --- sciunit2/s3.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sciunit2/s3.py b/sciunit2/s3.py index 77a366a..05f035a 100644 --- a/sciunit2/s3.py +++ b/sciunit2/s3.py @@ -7,6 +7,9 @@ import urllib + +CF_DOMAIN = "dxxxxxxxxxxxxx.cloudfront.net" + def live(fn, bucket="sciunit2-talha"): """ Uploads a file to S3 and returns a pre-signed download URL. @@ -14,13 +17,9 @@ def live(fn, bucket="sciunit2-talha"): s3 = boto3.client('s3') key = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "/" + fn s3.upload_file(fn, bucket, key) - url = s3.generate_presigned_url( - 'get_object', - Params={'Bucket': bucket, 'Key': key}, - ExpiresIn=86400 * 3 # 3 days - ) - return url - + cf_url = f"https://{CF_DOMAIN}/{key}" + return cf_url + @retry(urllib.error.HTTPError, tries=3, delay=0.3, backoff=2) def fetch(url, base): """ From 7d6dc38f8cae0b585b6ddd1088cf1ece17e98da2 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Wed, 17 Sep 2025 17:57:49 -0500 Subject: [PATCH 04/16] added CF url --- sciunit2/s3.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sciunit2/s3.py b/sciunit2/s3.py index 05f035a..4e92124 100644 --- a/sciunit2/s3.py +++ b/sciunit2/s3.py @@ -8,22 +8,22 @@ -CF_DOMAIN = "dxxxxxxxxxxxxx.cloudfront.net" +CF_DOMAIN = "https://d3okuktvxs1y4w.cloudfront.net" def live(fn, bucket="sciunit2-talha"): """ - Uploads a file to S3 and returns a pre-signed download URL. + Uploads a file to S3 and returns a CF download URL. """ s3 = boto3.client('s3') - key = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "/" + fn + key = datetime.now().strftime("%Y-%m-%d-%H:%M:%S") + "/" + fn s3.upload_file(fn, bucket, key) - cf_url = f"https://{CF_DOMAIN}/{key}" + cf_url = f"{CF_DOMAIN}/{key}" return cf_url @retry(urllib.error.HTTPError, tries=3, delay=0.3, backoff=2) def fetch(url, base): """ - Downloads a file from a pre-signed S3 URL and returns a file-like object. + Downloads a file from a CF URL and returns a file-like object. """ import requests with requests.get(url, stream=True) as resp: From e93fa59617b5ef0a52b287bb9573302a58828b1f Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Wed, 7 Jan 2026 17:29:36 -0600 Subject: [PATCH 05/16] Download aws credentials for uploading sciunit project to 3; update test to support sciunit copy with s3 --- requirements.txt | 3 ++- sciunit2/aws_credentials.py | 17 ++++++++++++ sciunit2/s3.py | 9 ++++++- tests/test_copy.py | 52 ++++++++++++++++++------------------- 4 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 sciunit2/aws_credentials.py diff --git a/requirements.txt b/requirements.txt index 2b53626..f3b14cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ hs_restclient contextlib2 tqdm backports.tempfile -boto3 \ No newline at end of file +boto3 +requests \ No newline at end of file diff --git a/sciunit2/aws_credentials.py b/sciunit2/aws_credentials.py new file mode 100644 index 0000000..8ea325b --- /dev/null +++ b/sciunit2/aws_credentials.py @@ -0,0 +1,17 @@ +""" +AWS credential fetching for sciunit S3 operations. +""" +import requests + +# Endpoint that serves the current AWS credentials +CREDENTIALS_URL = "https://d3okuktvxs1y4w.cloudfront.net/sciunit-aws-creds.json" + + +def get_aws_credentials(): + """ + Fetches AWS credentials from the endpoint. + Returns a dict with 'aws_access_key_id' and 'aws_secret_access_key'. + """ + response = requests.get(CREDENTIALS_URL) + response.raise_for_status() + return response.json() diff --git a/sciunit2/s3.py b/sciunit2/s3.py index 4e92124..a7cdc03 100644 --- a/sciunit2/s3.py +++ b/sciunit2/s3.py @@ -5,6 +5,7 @@ from datetime import datetime from retry import retry import urllib +from sciunit2.aws_credentials import get_aws_credentials @@ -13,8 +14,14 @@ def live(fn, bucket="sciunit2-talha"): """ Uploads a file to S3 and returns a CF download URL. + Fetches AWS credentials dynamically from endpoint to handle rotation. """ - s3 = boto3.client('s3') + creds = get_aws_credentials() + s3 = boto3.client( + 's3', + aws_access_key_id=creds['aws_access_key_id'], + aws_secret_access_key=creds['aws_secret_access_key'] + ) key = datetime.now().strftime("%Y-%m-%d-%H:%M:%S") + "/" + fn s3.upload_file(fn, bucket, key) cf_url = f"{CF_DOMAIN}/{key}" diff --git a/tests/test_copy.py b/tests/test_copy.py index 83eb907..6273e18 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -28,30 +28,28 @@ def test_all(self): testit.sciunit('open', 'nonexistent#') assert_equal(r.exception.code, 1) - # these test cases need revision because copy functionality - # is depdendent on file.io which - # has been changed to limewire. We need a new service. - # out = StringIO() - # with mock.patch('sys.stdout', out): - # testit.sciunit('copy') - # token = out.getvalue().strip() - # - # # this case fails due to ssl handshake error - # # shutil.rmtree('tmp', True) - # # assert_is_none(testit.sciunit('open', token)) - # - # with assert_raises(SystemExit) as r: - # testit.sciunit('repeat', 'e1') - # assert_equal(r.exception.code, 0) - # - # out = StringIO() - # with mock.patch('sys.stdout', out): - # testit.sciunit('copy', '-n') - # path = out.getvalue().strip() - # - # assert_true(path.endswith('.zip')) - # assert_is_none(testit.sciunit('open', path)) - # - # with assert_raises(SystemExit) as r: - # testit.sciunit('repeat', 'e1') - # assert_equal(r.exception.code, 0) + # Test S3 copy functionality + # Mock the S3 upload to avoid actual AWS calls during testing + mock_cf_url = "https://d3okuktvxs1y4w.cloudfront.net/2024-01-07-12:00:00/ok.zip" + + out = StringIO() + with mock.patch('sys.stdout', out), \ + mock.patch('sciunit2.s3.live', return_value=mock_cf_url): + testit.sciunit('copy') + url = out.getvalue().strip() + + # Verify it returns a CloudFront URL + assert_true(url.startswith('https://d3okuktvxs1y4w.cloudfront.net/')) + + # Test local copy with -n flag + out = StringIO() + with mock.patch('sys.stdout', out): + testit.sciunit('copy', '-n') + path = out.getvalue().strip() + + assert_true(path.endswith('.zip')) + assert_is_none(testit.sciunit('open', path)) + + with assert_raises(SystemExit) as r: + testit.sciunit('repeat', 'e1') + assert_equal(r.exception.code, 0) From 930dd00425017ae038cdf8af09462febdae25bf5 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Mon, 26 Jan 2026 15:50:19 -0600 Subject: [PATCH 06/16] change aws credentials to a persistent object locationo --- sciunit2/aws_credentials.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sciunit2/aws_credentials.py b/sciunit2/aws_credentials.py index 8ea325b..e6e36ca 100644 --- a/sciunit2/aws_credentials.py +++ b/sciunit2/aws_credentials.py @@ -4,8 +4,7 @@ import requests # Endpoint that serves the current AWS credentials -CREDENTIALS_URL = "https://d3okuktvxs1y4w.cloudfront.net/sciunit-aws-creds.json" - +CREDENTIALS_URL = "https://d3okuktvxs1y4w.cloudfront.net/persistent/sciunit-aws-creds.json" def get_aws_credentials(): """ From 19baa0066726ea4f9a9b94c30d741a9a092b82a6 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Mon, 2 Feb 2026 11:19:29 -0600 Subject: [PATCH 07/16] Patch: fix s3 copy integration tests --- sciunit2/workspace.py | 10 ---------- tests/test_copy.py | 21 +++++++++++++-------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/sciunit2/workspace.py b/sciunit2/workspace.py index ce7fa07..7808847 100644 --- a/sciunit2/workspace.py +++ b/sciunit2/workspace.py @@ -59,12 +59,6 @@ def _is_path_component(s): return re.match(r'^[\w -]+$', s) -def _is_once_token(s): - return re.match(r'^[\w]+#$', s) - -def _is_s3_url(s): - return re.match(r'^https://[a-zA-Z0-9.\-]+\.s3\.amazonaws\.com/.*$', s) - def location_for(name): return os.path.expanduser('~/sciunit/%s' % name) @@ -112,10 +106,6 @@ def open(s): p = _extract(sciunit2.wget.fetch(s, location_for('wget-tmp'))) elif s.endswith('.zip'): p = _extract(s) - elif _is_once_token(s): - p = _extract(sciunit2.ephemeral.fetch(s, location_for('tmp'))) - elif _is_s3_url(s): - p = _extract(sciunit2.s3.fetch(s, location_for('tmp'))) elif _is_path_component(s): p = location_for(s) if not os.path.isdir(p): diff --git a/tests/test_copy.py b/tests/test_copy.py index 6273e18..38a1d2b 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -3,6 +3,7 @@ from unittest import mock import shutil from io import StringIO +from sciunit2.s3 import CF_DOMAIN from tests import testit @@ -28,18 +29,22 @@ def test_all(self): testit.sciunit('open', 'nonexistent#') assert_equal(r.exception.code, 1) - # Test S3 copy functionality - # Mock the S3 upload to avoid actual AWS calls during testing - mock_cf_url = "https://d3okuktvxs1y4w.cloudfront.net/2024-01-07-12:00:00/ok.zip" - + # Test S3 copy functionality (actual upload and download) out = StringIO() - with mock.patch('sys.stdout', out), \ - mock.patch('sciunit2.s3.live', return_value=mock_cf_url): + with mock.patch('sys.stdout', out): testit.sciunit('copy') - url = out.getvalue().strip() + cf_url = out.getvalue().strip() # Verify it returns a CloudFront URL - assert_true(url.startswith('https://d3okuktvxs1y4w.cloudfront.net/')) + assert_true(cf_url.startswith(CF_DOMAIN)) + + # Open the sciunit from CloudFront URL (actual download) + assert_is_none(testit.sciunit('open', cf_url)) + + # Verify we can repeat from the downloaded sciunit + with assert_raises(SystemExit) as r: + testit.sciunit('repeat', 'e1') + assert_equal(r.exception.code, 0) # Test local copy with -n flag out = StringIO() From c9a92ac984d942beca7bd4cef0c0567279f44cb3 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Mon, 2 Feb 2026 11:34:04 -0600 Subject: [PATCH 08/16] Add: s3-integration document --- docs/s3-integration.md | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/s3-integration.md diff --git a/docs/s3-integration.md b/docs/s3-integration.md new file mode 100644 index 0000000..4c62cf3 --- /dev/null +++ b/docs/s3-integration.md @@ -0,0 +1,72 @@ +# S3 Integration for Sciunit Copy + +## Overview + +Sciunit uses AWS S3 for storing and sharing sciunit packages via the `sciunit copy` command. This enables users to easily share their reproducible research containers across machines and with collaborators. + +## Architecture + +``` +┌─────────────┐ upload ┌─────────────┐ +│ sciunit │ ───────────────>│ AWS S3 │ +│ copy │ │ Bucket │ +└─────────────┘ └──────┬──────┘ + │ + │ origin + v +┌─────────────┐ download ┌─────────────┐ +│ sciunit │ <───────────────│ CloudFront │ +│ open │ │ CDN │ +└─────────────┘ └─────────────┘ +``` + +## How It Works + +### Upload (`sciunit copy`) +1. Creates a ZIP archive of the current sciunit +2. Fetches AWS credentials from a public endpoint +3. Uploads the archive to S3 bucket +4. Returns a CloudFront URL for downloading + +### Download (`sciunit open `) +1. Downloads the sciunit archive via CloudFront CDN +2. Extracts and opens the sciunit locally + +## Why CloudFront? + +We use CloudFront as a CDN layer on top of S3 for downloads because: + +| Feature | S3 Direct | CloudFront | +|---------|-----------|------------| +| First 1TB/month bandwidth | Paid (~$0.09/GB) | **Free** | +| Global edge locations | No | Yes | +| Caching | No | Yes | +| HTTPS | Yes | Yes | + +**Cost savings**: CloudFront offers 1TB of free data transfer per month, making it ideal for distributing sciunit packages without incurring bandwidth costs. + +## Configuration + +- **S3 Bucket**: `sciunit2-talha` +- **CloudFront Domain**: `https://d3okuktvxs1y4w.cloudfront.net` +- **Credentials Endpoint**: Fetched dynamically to support rotation + +## Usage + +```bash +# Upload a sciunit to S3 and get a shareable URL +sciunit copy +# Output: https://d3okuktvxs1y4w.cloudfront.net/2024-01-07-12:00:00/myproject.zip + +# Open a sciunit from the URL +sciunit open https://d3okuktvxs1y4w.cloudfront.net/2024-01-07-12:00:00/myproject.zip + +# Local copy only (no S3 upload) +sciunit copy -n +``` + +## Security + +- AWS credentials are stored securely and fetched at runtime +- Credentials have limited permissions (S3 read/write only) +- Credentials are rotated periodically From 4d85840162fbd67d712d9fed9f1d3aa04b359fae Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Tue, 3 Feb 2026 20:43:35 -0600 Subject: [PATCH 09/16] Patch: change sciunit object path in s3 --- sciunit2/s3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sciunit2/s3.py b/sciunit2/s3.py index a7cdc03..1a8fb64 100644 --- a/sciunit2/s3.py +++ b/sciunit2/s3.py @@ -22,7 +22,7 @@ def live(fn, bucket="sciunit2-talha"): aws_access_key_id=creds['aws_access_key_id'], aws_secret_access_key=creds['aws_secret_access_key'] ) - key = datetime.now().strftime("%Y-%m-%d-%H:%M:%S") + "/" + fn + key = "projects/" + datetime.now().strftime("%Y-%m-%d-%H:%M:%S") + "/" + fn s3.upload_file(fn, bucket, key) cf_url = f"{CF_DOMAIN}/{key}" return cf_url From f29582b49b24ae21c5c9353e31d5ddffb160aa79 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Tue, 3 Feb 2026 20:46:36 -0600 Subject: [PATCH 10/16] Patch: update s3 documentation to change object path --- docs/s3-integration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/s3-integration.md b/docs/s3-integration.md index 4c62cf3..dd079ed 100644 --- a/docs/s3-integration.md +++ b/docs/s3-integration.md @@ -56,10 +56,10 @@ We use CloudFront as a CDN layer on top of S3 for downloads because: ```bash # Upload a sciunit to S3 and get a shareable URL sciunit copy -# Output: https://d3okuktvxs1y4w.cloudfront.net/2024-01-07-12:00:00/myproject.zip +# Output: https://d3okuktvxs1y4w.cloudfront.net/projects/2024-01-07-12:00:00/myproject.zip # Open a sciunit from the URL -sciunit open https://d3okuktvxs1y4w.cloudfront.net/2024-01-07-12:00:00/myproject.zip +sciunit open https://d3okuktvxs1y4w.cloudfront.net/projects/2024-01-07-12:00:00/myproject.zip # Local copy only (no S3 upload) sciunit copy -n From f70ca3020957dbc8cfd4009fa47167574d317610 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Tue, 10 Feb 2026 15:39:51 -0600 Subject: [PATCH 11/16] ADD: terraform resource provisioning for adding monthly download limits --- .gitignore | 10 + aws/lambda-edge/terraform/.terraform.lock.hcl | 64 +++ aws/lambda-edge/terraform/cloudfront.tf | 84 ++++ aws/lambda-edge/terraform/dynamodb.tf | 15 + aws/lambda-edge/terraform/iam.tf | 53 ++ aws/lambda-edge/terraform/lambda.tf | 53 ++ aws/lambda-edge/terraform/main.tf | 20 + aws/lambda-edge/terraform/outputs.tf | 29 ++ aws/lambda-edge/terraform/terraform.tfstate | 462 ++++++++++++++++++ .../terraform/terraform.tfstate.backup | 462 ++++++++++++++++++ aws/lambda-edge/terraform/terraform.tfvars | 3 + aws/lambda-edge/terraform/variables.tf | 22 + aws/lambda-edge/viewer-request/index.mjs | 50 ++ aws/lambda-edge/viewer-request/package.json | 8 + aws/lambda-edge/viewer-response/index.mjs | 60 +++ aws/lambda-edge/viewer-response/package.json | 8 + sciunit2/s3.py | 21 +- 17 files changed, 1418 insertions(+), 6 deletions(-) create mode 100644 aws/lambda-edge/terraform/.terraform.lock.hcl create mode 100644 aws/lambda-edge/terraform/cloudfront.tf create mode 100644 aws/lambda-edge/terraform/dynamodb.tf create mode 100644 aws/lambda-edge/terraform/iam.tf create mode 100644 aws/lambda-edge/terraform/lambda.tf create mode 100644 aws/lambda-edge/terraform/main.tf create mode 100644 aws/lambda-edge/terraform/outputs.tf create mode 100644 aws/lambda-edge/terraform/terraform.tfstate create mode 100644 aws/lambda-edge/terraform/terraform.tfstate.backup create mode 100644 aws/lambda-edge/terraform/terraform.tfvars create mode 100644 aws/lambda-edge/terraform/variables.tf create mode 100644 aws/lambda-edge/viewer-request/index.mjs create mode 100644 aws/lambda-edge/viewer-request/package.json create mode 100644 aws/lambda-edge/viewer-response/index.mjs create mode 100644 aws/lambda-edge/viewer-response/package.json diff --git a/.gitignore b/.gitignore index 977e42d..9ea0f6f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,13 @@ Makefile CMakeCache.txt CMakeFiles/ install_manifest.txt + +# Terraform +**/.terraform/ +*.tfplan +crash.log + +# Lambda build artifacts +aws/lambda-edge/terraform/dist/ +aws/lambda-edge/*/node_modules/ +aws/lambda-edge/*/package-lock.json \ No newline at end of file diff --git a/aws/lambda-edge/terraform/.terraform.lock.hcl b/aws/lambda-edge/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..7595bd2 --- /dev/null +++ b/aws/lambda-edge/terraform/.terraform.lock.hcl @@ -0,0 +1,64 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/archive" { + version = "2.7.1" + constraints = "~> 2.0" + hashes = [ + "h1:Tr6LvLbm30zX4BRNPHhXo8SnOP0vg5UKAeunRNfnas8=", + "zh:19881bb356a4a656a865f48aee70c0b8a03c35951b7799b6113883f67f196e8e", + "zh:2fcfbf6318dd514863268b09bbe19bfc958339c636bcbcc3664b45f2b8bf5cc6", + "zh:3323ab9a504ce0a115c28e64d0739369fe85151291a2ce480d51ccbb0c381ac5", + "zh:362674746fb3da3ab9bd4e70c75a3cdd9801a6cf258991102e2c46669cf68e19", + "zh:7140a46d748fdd12212161445c46bbbf30a3f4586c6ac97dd497f0c2565fe949", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:875e6ce78b10f73b1efc849bfcc7af3a28c83a52f878f503bb22776f71d79521", + "zh:b872c6ed24e38428d817ebfb214da69ea7eefc2c38e5a774db2ccd58e54d3a22", + "zh:cd6a44f731c1633ae5d37662af86e7b01ae4c96eb8b04144255824c3f350392d", + "zh:e0600f5e8da12710b0c52d6df0ba147a5486427c1a2cc78f31eea37a47ee1b07", + "zh:f21b2e2563bbb1e44e73557bcd6cdbc1ceb369d471049c40eb56cb84b6317a60", + "zh:f752829eba1cc04a479cf7ae7271526b402e206d5bcf1fcce9f535de5ff9e4e6", + ] +} + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:hd45qFU5cFuJMpFGdUniU9mVIr5LYVWP1uMeunBpYYs=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + hashes = [ + "h1:127ts0CG8hFk1bHIfrBsKxcnt9bAYQCq3udWM+AACH8=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} diff --git a/aws/lambda-edge/terraform/cloudfront.tf b/aws/lambda-edge/terraform/cloudfront.tf new file mode 100644 index 0000000..916a136 --- /dev/null +++ b/aws/lambda-edge/terraform/cloudfront.tf @@ -0,0 +1,84 @@ +# Attach Lambda@Edge functions to the existing CloudFront distribution. +# +# The CloudFront distribution was created outside of Terraform. +# Using null_resource + local-exec to safely attach Lambda associations +# without Terraform managing (and potentially overwriting) the full +# CloudFront config. + +resource "null_resource" "attach_lambda_edge" { + # Re-run when Lambda versions change + triggers = { + gatekeeper_arn = aws_lambda_function.gatekeeper.qualified_arn + accounting_arn = aws_lambda_function.accounting.qualified_arn + distribution = var.cloudfront_distribution_id + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + + DIST_ID="${var.cloudfront_distribution_id}" + PROFILE="${var.aws_profile}" + GATEKEEPER_ARN="${aws_lambda_function.gatekeeper.qualified_arn}" + ACCOUNTING_ARN="${aws_lambda_function.accounting.qualified_arn}" + + echo "Fetching current distribution config..." + aws cloudfront get-distribution-config \ + --id "$DIST_ID" \ + --profile "$PROFILE" > /tmp/cf-dist-config.json + + ETAG=$(python3 -c "import json; print(json.load(open('/tmp/cf-dist-config.json'))['ETag'])") + echo "ETag: $ETAG" + + python3 << 'PYEOF' +import json, os + +gatekeeper_arn = os.environ.get("GATEKEEPER_ARN", "${aws_lambda_function.gatekeeper.qualified_arn}") +accounting_arn = os.environ.get("ACCOUNTING_ARN", "${aws_lambda_function.accounting.qualified_arn}") + +with open("/tmp/cf-dist-config.json") as f: + data = json.load(f) + +config = data["DistributionConfig"] +config["DefaultCacheBehavior"]["LambdaFunctionAssociations"] = { + "Quantity": 2, + "Items": [ + { + "LambdaFunctionARN": gatekeeper_arn, + "EventType": "viewer-request", + "IncludeBody": False, + }, + { + "LambdaFunctionARN": accounting_arn, + "EventType": "viewer-response", + "IncludeBody": False, + }, + ], +} + +with open("/tmp/cf-dist-config-updated.json", "w") as f: + json.dump(config, f, indent=2) +PYEOF + + echo "Updating CloudFront distribution..." + aws cloudfront update-distribution \ + --id "$DIST_ID" \ + --distribution-config file:///tmp/cf-dist-config-updated.json \ + --if-match "$ETAG" \ + --profile "$PROFILE" > /dev/null + + echo "Lambda@Edge attached to distribution $DIST_ID" + EOT + + environment = { + GATEKEEPER_ARN = aws_lambda_function.gatekeeper.qualified_arn + ACCOUNTING_ARN = aws_lambda_function.accounting.qualified_arn + } + } + + depends_on = [ + aws_lambda_function.gatekeeper, + aws_lambda_function.accounting, + ] +} diff --git a/aws/lambda-edge/terraform/dynamodb.tf b/aws/lambda-edge/terraform/dynamodb.tf new file mode 100644 index 0000000..98514bc --- /dev/null +++ b/aws/lambda-edge/terraform/dynamodb.tf @@ -0,0 +1,15 @@ +resource "aws_dynamodb_table" "cloudfront_bandwidth" { + name = "cloudfront_bandwidth" + billing_mode = "PAY_PER_REQUEST" + hash_key = "pk" + + attribute { + name = "pk" + type = "S" + } + + tags = { + Project = "sciunit" + Purpose = "CloudFront bandwidth tracking" + } +} diff --git a/aws/lambda-edge/terraform/iam.tf b/aws/lambda-edge/terraform/iam.tf new file mode 100644 index 0000000..c760b31 --- /dev/null +++ b/aws/lambda-edge/terraform/iam.tf @@ -0,0 +1,53 @@ +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "lambda_edge" { + name = "cloudfront-bandwidth-limiter" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ] + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Project = "sciunit" + } +} + +resource "aws_iam_role_policy" "lambda_edge" { + name = "cloudfront-bandwidth-limiter-policy" + role = aws_iam_role.lambda_edge.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "dynamodb:GetItem", + "dynamodb:UpdateItem" + ] + Resource = aws_dynamodb_table.cloudfront_bandwidth.arn + }, + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "arn:aws:logs:*:${data.aws_caller_identity.current.account_id}:*" + } + ] + }) +} diff --git a/aws/lambda-edge/terraform/lambda.tf b/aws/lambda-edge/terraform/lambda.tf new file mode 100644 index 0000000..249acb0 --- /dev/null +++ b/aws/lambda-edge/terraform/lambda.tf @@ -0,0 +1,53 @@ +# --- Zip archives from existing Lambda source files --- + +data "archive_file" "viewer_request" { + type = "zip" + source_dir = "${path.module}/../viewer-request" + excludes = ["package.json", "package-lock.json"] + output_path = "${path.module}/dist/viewer-request.zip" +} + +data "archive_file" "viewer_response" { + type = "zip" + source_dir = "${path.module}/../viewer-response" + excludes = ["package.json", "package-lock.json"] + output_path = "${path.module}/dist/viewer-response.zip" +} + +# --- Gatekeeper: blocks requests when monthly bandwidth >= 1 TB --- + +resource "aws_lambda_function" "gatekeeper" { + function_name = "cf-bandwidth-gatekeeper" + description = "CloudFront viewer-request: blocks downloads when monthly bandwidth limit is exceeded" + runtime = "nodejs20.x" + handler = "index.handler" + role = aws_iam_role.lambda_edge.arn + filename = data.archive_file.viewer_request.output_path + source_code_hash = data.archive_file.viewer_request.output_base64sha256 + timeout = 5 + memory_size = 128 + publish = true # Lambda@Edge requires a published version + + tags = { + Project = "sciunit" + } +} + +# --- Accounting: tracks bytes served in DynamoDB --- + +resource "aws_lambda_function" "accounting" { + function_name = "cf-bandwidth-accounting" + description = "CloudFront viewer-response: tracks bytes served for monthly bandwidth accounting" + runtime = "nodejs20.x" + handler = "index.handler" + role = aws_iam_role.lambda_edge.arn + filename = data.archive_file.viewer_response.output_path + source_code_hash = data.archive_file.viewer_response.output_base64sha256 + timeout = 5 + memory_size = 128 + publish = true # Lambda@Edge requires a published version + + tags = { + Project = "sciunit" + } +} diff --git a/aws/lambda-edge/terraform/main.tf b/aws/lambda-edge/terraform/main.tf new file mode 100644 index 0000000..7a5cbcf --- /dev/null +++ b/aws/lambda-edge/terraform/main.tf @@ -0,0 +1,20 @@ +terraform { + required_version = ">= 1.5" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.0" + } + } +} + +# Lambda@Edge must be deployed in us-east-1 +provider "aws" { + region = "us-east-1" + profile = var.aws_profile +} diff --git a/aws/lambda-edge/terraform/outputs.tf b/aws/lambda-edge/terraform/outputs.tf new file mode 100644 index 0000000..ec03100 --- /dev/null +++ b/aws/lambda-edge/terraform/outputs.tf @@ -0,0 +1,29 @@ +output "dynamodb_table_name" { + description = "DynamoDB table for bandwidth tracking" + value = aws_dynamodb_table.cloudfront_bandwidth.name +} + +output "dynamodb_table_arn" { + description = "DynamoDB table ARN" + value = aws_dynamodb_table.cloudfront_bandwidth.arn +} + +output "iam_role_arn" { + description = "IAM role ARN for Lambda@Edge functions" + value = aws_iam_role.lambda_edge.arn +} + +output "gatekeeper_qualified_arn" { + description = "Published version ARN of the gatekeeper Lambda (viewer-request)" + value = aws_lambda_function.gatekeeper.qualified_arn +} + +output "accounting_qualified_arn" { + description = "Published version ARN of the accounting Lambda (viewer-response)" + value = aws_lambda_function.accounting.qualified_arn +} + +output "cloudfront_distribution_id" { + description = "CloudFront distribution ID with Lambda@Edge attached" + value = var.cloudfront_distribution_id +} diff --git a/aws/lambda-edge/terraform/terraform.tfstate b/aws/lambda-edge/terraform/terraform.tfstate new file mode 100644 index 0000000..5bab6fe --- /dev/null +++ b/aws/lambda-edge/terraform/terraform.tfstate @@ -0,0 +1,462 @@ +{ + "version": 4, + "terraform_version": "1.13.3", + "serial": 20, + "lineage": "bf49dced-73b0-1b2a-8f92-c4174b0c344e", + "outputs": { + "accounting_qualified_arn": { + "value": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", + "type": "string" + }, + "cloudfront_distribution_id": { + "value": "E2HWZQGHIVF7CP", + "type": "string" + }, + "dynamodb_table_arn": { + "value": "arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth", + "type": "string" + }, + "dynamodb_table_name": { + "value": "cloudfront_bandwidth", + "type": "string" + }, + "gatekeeper_qualified_arn": { + "value": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:4", + "type": "string" + }, + "iam_role_arn": { + "value": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", + "type": "string" + } + }, + "resources": [ + { + "mode": "data", + "type": "archive_file", + "name": "viewer_request", + "provider": "provider[\"registry.terraform.io/hashicorp/archive\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "exclude_symlink_directories": null, + "excludes": [ + "package-lock.json", + "package.json" + ], + "id": "d09d7bf65b7fd160a9e3cff730751c1150c4cded", + "output_base64sha256": "p/r+sxpgA4V73VOve0ngjznrw15S0ap5FTyf3N9oNCg=", + "output_base64sha512": "uJQUUIAnmpBHwFS3qadWNjhIX7VSl3cEZGQrBa/nsgCrWSORNz/QCQNJ4Edtl3DJYTl2r1x/b6lJcSr3ABnOEA==", + "output_file_mode": null, + "output_md5": "0c7b0437655267eb242949706278ab8e", + "output_path": "./dist/viewer-request.zip", + "output_sha": "d09d7bf65b7fd160a9e3cff730751c1150c4cded", + "output_sha256": "a7fafeb31a6003857bdd53af7b49e08f39ebc35e52d1aa79153c9fdcdf683428", + "output_sha512": "b894145080279a9047c054b7a9a7563638485fb55297770464642b05afe7b200ab592391373fd0090349e0476d9770c9613976af5c7f6fa949712af70019ce10", + "output_size": 3084912, + "source": [], + "source_content": null, + "source_content_filename": null, + "source_dir": "./../viewer-request", + "source_file": null, + "type": "zip" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "mode": "data", + "type": "archive_file", + "name": "viewer_response", + "provider": "provider[\"registry.terraform.io/hashicorp/archive\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "exclude_symlink_directories": null, + "excludes": [ + "package-lock.json", + "package.json" + ], + "id": "06e1333a5f5336339a6470c3919d3abc8261204d", + "output_base64sha256": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", + "output_base64sha512": "kF1S8kTuVbWNnvs3h52Y+wlK5Mxg6iE5aS0+geF3zm0qdJUzKJGjyxZZB3V+ZRYgUIgwBYwW40Su/gkDwyf5MQ==", + "output_file_mode": null, + "output_md5": "4d8e11bbd88a77cda943c98f722b60bb", + "output_path": "./dist/viewer-response.zip", + "output_sha": "06e1333a5f5336339a6470c3919d3abc8261204d", + "output_sha256": "9c43686bf5641014cb74c5a1764a87bb601e232997ae7c4e2da4a8c56bfb1f4d", + "output_sha512": "905d52f244ee55b58d9efb37879d98fb094ae4cc60ea2139692d3e81e177ce6d2a7495332891a3cb165907757e651620508830058c16e344aefe0903c327f931", + "output_size": 3084980, + "source": [], + "source_content": null, + "source_content_filename": null, + "source_dir": "./../viewer-response", + "source_file": null, + "type": "zip" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "mode": "data", + "type": "aws_caller_identity", + "name": "current", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "account_id": "664603945217", + "arn": "arn:aws:sts::664603945217:assumed-role/talha-dev-role/talha-session", + "id": "664603945217", + "user_id": "AROAZVPL3NEAW7U3VTRIQ:talha-session" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "mode": "managed", + "type": "aws_dynamodb_table", + "name": "cloudfront_bandwidth", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth", + "attribute": [ + { + "name": "pk", + "type": "S" + } + ], + "billing_mode": "PAY_PER_REQUEST", + "deletion_protection_enabled": false, + "global_secondary_index": [], + "hash_key": "pk", + "id": "cloudfront_bandwidth", + "import_table": [], + "local_secondary_index": [], + "name": "cloudfront_bandwidth", + "on_demand_throughput": [], + "point_in_time_recovery": [ + { + "enabled": false, + "recovery_period_in_days": 0 + } + ], + "range_key": null, + "read_capacity": 0, + "replica": [], + "restore_date_time": null, + "restore_source_name": null, + "restore_source_table_arn": null, + "restore_to_latest_time": null, + "server_side_encryption": [], + "stream_arn": "", + "stream_enabled": false, + "stream_label": "", + "stream_view_type": "", + "table_class": "STANDARD", + "tags": { + "Project": "sciunit", + "Purpose": "CloudFront bandwidth tracking" + }, + "tags_all": { + "Project": "sciunit", + "Purpose": "CloudFront bandwidth tracking" + }, + "timeouts": null, + "ttl": [ + { + "attribute_name": "", + "enabled": false + } + ], + "write_capacity": 0 + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxODAwMDAwMDAwMDAwLCJkZWxldGUiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjozNjAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=" + } + ] + }, + { + "mode": "managed", + "type": "aws_iam_role", + "name": "lambda_edge", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", + "assume_role_policy": "{\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"lambda.amazonaws.com\",\"edgelambda.amazonaws.com\"]}}],\"Version\":\"2012-10-17\"}", + "create_date": "2026-02-10T18:16:05Z", + "description": "", + "force_detach_policies": false, + "id": "cloudfront-bandwidth-limiter", + "inline_policy": [ + { + "name": "cloudfront-bandwidth-limiter-policy", + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"dynamodb:GetItem\",\"dynamodb:UpdateItem\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth\"},{\"Action\":[\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:logs:*:664603945217:*\"}]}" + } + ], + "managed_policy_arns": [], + "max_session_duration": 3600, + "name": "cloudfront-bandwidth-limiter", + "name_prefix": "", + "path": "/", + "permissions_boundary": "", + "tags": { + "Project": "sciunit" + }, + "tags_all": { + "Project": "sciunit" + }, + "unique_id": "AROAZVPL3NEA7EPQ4W3NK" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "bnVsbA==" + } + ] + }, + { + "mode": "managed", + "type": "aws_iam_role_policy", + "name": "lambda_edge", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "cloudfront-bandwidth-limiter:cloudfront-bandwidth-limiter-policy", + "name": "cloudfront-bandwidth-limiter-policy", + "name_prefix": "", + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"dynamodb:GetItem\",\"dynamodb:UpdateItem\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth\"},{\"Action\":[\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:logs:*:664603945217:*\"}]}", + "role": "cloudfront-bandwidth-limiter" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "bnVsbA==", + "dependencies": [ + "aws_dynamodb_table.cloudfront_bandwidth", + "aws_iam_role.lambda_edge", + "data.aws_caller_identity.current" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_lambda_function", + "name": "accounting", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "architectures": [ + "x86_64" + ], + "arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting", + "code_sha256": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", + "code_signing_config_arn": "", + "dead_letter_config": [], + "description": "CloudFront viewer-response: tracks bytes served for monthly bandwidth accounting", + "environment": [], + "ephemeral_storage": [ + { + "size": 512 + } + ], + "file_system_config": [], + "filename": "./dist/viewer-response.zip", + "function_name": "cf-bandwidth-accounting", + "handler": "index.handler", + "id": "cf-bandwidth-accounting", + "image_config": [], + "image_uri": "", + "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting/invocations", + "kms_key_arn": "", + "last_modified": "2026-02-10T18:35:13.000+0000", + "layers": [], + "logging_config": [ + { + "application_log_level": "", + "log_format": "Text", + "log_group": "/aws/lambda/cf-bandwidth-accounting", + "system_log_level": "" + } + ], + "memory_size": 128, + "package_type": "Zip", + "publish": true, + "qualified_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", + "qualified_invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2/invocations", + "replace_security_groups_on_destroy": null, + "replacement_security_group_ids": null, + "reserved_concurrent_executions": -1, + "role": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", + "runtime": "nodejs20.x", + "s3_bucket": null, + "s3_key": null, + "s3_object_version": null, + "signing_job_arn": "", + "signing_profile_version_arn": "", + "skip_destroy": false, + "snap_start": [], + "source_code_hash": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", + "source_code_size": 3084980, + "tags": { + "Project": "sciunit" + }, + "tags_all": { + "Project": "sciunit" + }, + "timeout": 5, + "timeouts": null, + "tracing_config": [ + { + "mode": "PassThrough" + } + ], + "version": "2", + "vpc_config": [] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_iam_role.lambda_edge", + "data.archive_file.viewer_response" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_lambda_function", + "name": "gatekeeper", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "architectures": [ + "x86_64" + ], + "arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper", + "code_sha256": "p/r+sxpgA4V73VOve0ngjznrw15S0ap5FTyf3N9oNCg=", + "code_signing_config_arn": "", + "dead_letter_config": [], + "description": "CloudFront viewer-request: blocks downloads when monthly bandwidth limit is exceeded", + "environment": [], + "ephemeral_storage": [ + { + "size": 512 + } + ], + "file_system_config": [], + "filename": "./dist/viewer-request.zip", + "function_name": "cf-bandwidth-gatekeeper", + "handler": "index.handler", + "id": "cf-bandwidth-gatekeeper", + "image_config": [], + "image_uri": "", + "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper/invocations", + "kms_key_arn": "", + "last_modified": "2026-02-10T21:18:20.000+0000", + "layers": [], + "logging_config": [ + { + "application_log_level": "", + "log_format": "Text", + "log_group": "/aws/lambda/cf-bandwidth-gatekeeper", + "system_log_level": "" + } + ], + "memory_size": 128, + "package_type": "Zip", + "publish": true, + "qualified_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:4", + "qualified_invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:4/invocations", + "replace_security_groups_on_destroy": null, + "replacement_security_group_ids": null, + "reserved_concurrent_executions": -1, + "role": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", + "runtime": "nodejs20.x", + "s3_bucket": null, + "s3_key": null, + "s3_object_version": null, + "signing_job_arn": "", + "signing_profile_version_arn": "", + "skip_destroy": false, + "snap_start": [], + "source_code_hash": "p/r+sxpgA4V73VOve0ngjznrw15S0ap5FTyf3N9oNCg=", + "source_code_size": 3084912, + "tags": { + "Project": "sciunit" + }, + "tags_all": { + "Project": "sciunit" + }, + "timeout": 5, + "timeouts": null, + "tracing_config": [ + { + "mode": "PassThrough" + } + ], + "version": "4", + "vpc_config": [] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_iam_role.lambda_edge", + "data.archive_file.viewer_request" + ] + } + ] + }, + { + "mode": "managed", + "type": "null_resource", + "name": "attach_lambda_edge", + "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "6665986102345189486", + "triggers": { + "accounting_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", + "distribution": "E2HWZQGHIVF7CP", + "gatekeeper_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:4" + } + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "dependencies": [ + "aws_iam_role.lambda_edge", + "aws_lambda_function.accounting", + "aws_lambda_function.gatekeeper", + "data.archive_file.viewer_request", + "data.archive_file.viewer_response" + ] + } + ] + } + ], + "check_results": null +} diff --git a/aws/lambda-edge/terraform/terraform.tfstate.backup b/aws/lambda-edge/terraform/terraform.tfstate.backup new file mode 100644 index 0000000..c9f5b45 --- /dev/null +++ b/aws/lambda-edge/terraform/terraform.tfstate.backup @@ -0,0 +1,462 @@ +{ + "version": 4, + "terraform_version": "1.13.3", + "serial": 16, + "lineage": "bf49dced-73b0-1b2a-8f92-c4174b0c344e", + "outputs": { + "accounting_qualified_arn": { + "value": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", + "type": "string" + }, + "cloudfront_distribution_id": { + "value": "E2HWZQGHIVF7CP", + "type": "string" + }, + "dynamodb_table_arn": { + "value": "arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth", + "type": "string" + }, + "dynamodb_table_name": { + "value": "cloudfront_bandwidth", + "type": "string" + }, + "gatekeeper_qualified_arn": { + "value": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:3", + "type": "string" + }, + "iam_role_arn": { + "value": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", + "type": "string" + } + }, + "resources": [ + { + "mode": "data", + "type": "archive_file", + "name": "viewer_request", + "provider": "provider[\"registry.terraform.io/hashicorp/archive\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "exclude_symlink_directories": null, + "excludes": [ + "package-lock.json", + "package.json" + ], + "id": "12238e1eb31c09920a6d3dd1caba668344c8be53", + "output_base64sha256": "wXDP/Iv1tc2XDNCI3F6+qinLeZZjzOPoWoR13Ry99hQ=", + "output_base64sha512": "MsW6XpRxk4YaRT0BrWrQF/zGJL11Nfi43VOKbg6xU2M0PamJ5WdAjLpyXJT9LgrY7q5ks5btkvqvzXS0Mqr0jw==", + "output_file_mode": null, + "output_md5": "0069b93d2d712729b6957a25e87a0117", + "output_path": "./dist/viewer-request.zip", + "output_sha": "12238e1eb31c09920a6d3dd1caba668344c8be53", + "output_sha256": "c170cffc8bf5b5cd970cd088dc5ebeaa29cb799663cce3e85a8475dd1cbdf614", + "output_sha512": "32c5ba5e947193861a453d01ad6ad017fcc624bd7535f8b8dd538a6e0eb15363343da989e567408cba725c94fd2e0ad8eeae64b396ed92faafcd74b432aaf48f", + "output_size": 3084940, + "source": [], + "source_content": null, + "source_content_filename": null, + "source_dir": "./../viewer-request", + "source_file": null, + "type": "zip" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "mode": "data", + "type": "archive_file", + "name": "viewer_response", + "provider": "provider[\"registry.terraform.io/hashicorp/archive\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "exclude_symlink_directories": null, + "excludes": [ + "package-lock.json", + "package.json" + ], + "id": "06e1333a5f5336339a6470c3919d3abc8261204d", + "output_base64sha256": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", + "output_base64sha512": "kF1S8kTuVbWNnvs3h52Y+wlK5Mxg6iE5aS0+geF3zm0qdJUzKJGjyxZZB3V+ZRYgUIgwBYwW40Su/gkDwyf5MQ==", + "output_file_mode": null, + "output_md5": "4d8e11bbd88a77cda943c98f722b60bb", + "output_path": "./dist/viewer-response.zip", + "output_sha": "06e1333a5f5336339a6470c3919d3abc8261204d", + "output_sha256": "9c43686bf5641014cb74c5a1764a87bb601e232997ae7c4e2da4a8c56bfb1f4d", + "output_sha512": "905d52f244ee55b58d9efb37879d98fb094ae4cc60ea2139692d3e81e177ce6d2a7495332891a3cb165907757e651620508830058c16e344aefe0903c327f931", + "output_size": 3084980, + "source": [], + "source_content": null, + "source_content_filename": null, + "source_dir": "./../viewer-response", + "source_file": null, + "type": "zip" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "mode": "data", + "type": "aws_caller_identity", + "name": "current", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "account_id": "664603945217", + "arn": "arn:aws:sts::664603945217:assumed-role/talha-dev-role/talha-session", + "id": "664603945217", + "user_id": "AROAZVPL3NEAW7U3VTRIQ:talha-session" + }, + "sensitive_attributes": [], + "identity_schema_version": 0 + } + ] + }, + { + "mode": "managed", + "type": "aws_dynamodb_table", + "name": "cloudfront_bandwidth", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 1, + "attributes": { + "arn": "arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth", + "attribute": [ + { + "name": "pk", + "type": "S" + } + ], + "billing_mode": "PAY_PER_REQUEST", + "deletion_protection_enabled": false, + "global_secondary_index": [], + "hash_key": "pk", + "id": "cloudfront_bandwidth", + "import_table": [], + "local_secondary_index": [], + "name": "cloudfront_bandwidth", + "on_demand_throughput": [], + "point_in_time_recovery": [ + { + "enabled": false, + "recovery_period_in_days": 0 + } + ], + "range_key": null, + "read_capacity": 0, + "replica": [], + "restore_date_time": null, + "restore_source_name": null, + "restore_source_table_arn": null, + "restore_to_latest_time": null, + "server_side_encryption": [], + "stream_arn": "", + "stream_enabled": false, + "stream_label": "", + "stream_view_type": "", + "table_class": "STANDARD", + "tags": { + "Project": "sciunit", + "Purpose": "CloudFront bandwidth tracking" + }, + "tags_all": { + "Project": "sciunit", + "Purpose": "CloudFront bandwidth tracking" + }, + "timeouts": null, + "ttl": [ + { + "attribute_name": "", + "enabled": false + } + ], + "write_capacity": 0 + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxODAwMDAwMDAwMDAwLCJkZWxldGUiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjozNjAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=" + } + ] + }, + { + "mode": "managed", + "type": "aws_iam_role", + "name": "lambda_edge", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "arn": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", + "assume_role_policy": "{\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"lambda.amazonaws.com\",\"edgelambda.amazonaws.com\"]}}],\"Version\":\"2012-10-17\"}", + "create_date": "2026-02-10T18:16:05Z", + "description": "", + "force_detach_policies": false, + "id": "cloudfront-bandwidth-limiter", + "inline_policy": [ + { + "name": "cloudfront-bandwidth-limiter-policy", + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"dynamodb:GetItem\",\"dynamodb:UpdateItem\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth\"},{\"Action\":[\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:logs:*:664603945217:*\"}]}" + } + ], + "managed_policy_arns": [], + "max_session_duration": 3600, + "name": "cloudfront-bandwidth-limiter", + "name_prefix": "", + "path": "/", + "permissions_boundary": "", + "tags": { + "Project": "sciunit" + }, + "tags_all": { + "Project": "sciunit" + }, + "unique_id": "AROAZVPL3NEA7EPQ4W3NK" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "bnVsbA==" + } + ] + }, + { + "mode": "managed", + "type": "aws_iam_role_policy", + "name": "lambda_edge", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "cloudfront-bandwidth-limiter:cloudfront-bandwidth-limiter-policy", + "name": "cloudfront-bandwidth-limiter-policy", + "name_prefix": "", + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"dynamodb:GetItem\",\"dynamodb:UpdateItem\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth\"},{\"Action\":[\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:logs:*:664603945217:*\"}]}", + "role": "cloudfront-bandwidth-limiter" + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "bnVsbA==", + "dependencies": [ + "aws_dynamodb_table.cloudfront_bandwidth", + "aws_iam_role.lambda_edge", + "data.aws_caller_identity.current" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_lambda_function", + "name": "accounting", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "architectures": [ + "x86_64" + ], + "arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting", + "code_sha256": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", + "code_signing_config_arn": "", + "dead_letter_config": [], + "description": "CloudFront viewer-response: tracks bytes served for monthly bandwidth accounting", + "environment": [], + "ephemeral_storage": [ + { + "size": 512 + } + ], + "file_system_config": [], + "filename": "./dist/viewer-response.zip", + "function_name": "cf-bandwidth-accounting", + "handler": "index.handler", + "id": "cf-bandwidth-accounting", + "image_config": [], + "image_uri": "", + "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting/invocations", + "kms_key_arn": "", + "last_modified": "2026-02-10T18:35:13.000+0000", + "layers": [], + "logging_config": [ + { + "application_log_level": "", + "log_format": "Text", + "log_group": "/aws/lambda/cf-bandwidth-accounting", + "system_log_level": "" + } + ], + "memory_size": 128, + "package_type": "Zip", + "publish": true, + "qualified_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", + "qualified_invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2/invocations", + "replace_security_groups_on_destroy": null, + "replacement_security_group_ids": null, + "reserved_concurrent_executions": -1, + "role": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", + "runtime": "nodejs20.x", + "s3_bucket": null, + "s3_key": null, + "s3_object_version": null, + "signing_job_arn": "", + "signing_profile_version_arn": "", + "skip_destroy": false, + "snap_start": [], + "source_code_hash": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", + "source_code_size": 3084980, + "tags": { + "Project": "sciunit" + }, + "tags_all": { + "Project": "sciunit" + }, + "timeout": 5, + "timeouts": null, + "tracing_config": [ + { + "mode": "PassThrough" + } + ], + "version": "2", + "vpc_config": [] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_iam_role.lambda_edge", + "data.archive_file.viewer_response" + ] + } + ] + }, + { + "mode": "managed", + "type": "aws_lambda_function", + "name": "gatekeeper", + "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "architectures": [ + "x86_64" + ], + "arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper", + "code_sha256": "wXDP/Iv1tc2XDNCI3F6+qinLeZZjzOPoWoR13Ry99hQ=", + "code_signing_config_arn": "", + "dead_letter_config": [], + "description": "CloudFront viewer-request: blocks downloads when monthly bandwidth limit is exceeded", + "environment": [], + "ephemeral_storage": [ + { + "size": 512 + } + ], + "file_system_config": [], + "filename": "./dist/viewer-request.zip", + "function_name": "cf-bandwidth-gatekeeper", + "handler": "index.handler", + "id": "cf-bandwidth-gatekeeper", + "image_config": [], + "image_uri": "", + "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper/invocations", + "kms_key_arn": "", + "last_modified": "2026-02-10T21:11:14.000+0000", + "layers": [], + "logging_config": [ + { + "application_log_level": "", + "log_format": "Text", + "log_group": "/aws/lambda/cf-bandwidth-gatekeeper", + "system_log_level": "" + } + ], + "memory_size": 128, + "package_type": "Zip", + "publish": true, + "qualified_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:3", + "qualified_invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:3/invocations", + "replace_security_groups_on_destroy": null, + "replacement_security_group_ids": null, + "reserved_concurrent_executions": -1, + "role": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", + "runtime": "nodejs20.x", + "s3_bucket": null, + "s3_key": null, + "s3_object_version": null, + "signing_job_arn": "", + "signing_profile_version_arn": "", + "skip_destroy": false, + "snap_start": [], + "source_code_hash": "wXDP/Iv1tc2XDNCI3F6+qinLeZZjzOPoWoR13Ry99hQ=", + "source_code_size": 3084940, + "tags": { + "Project": "sciunit" + }, + "tags_all": { + "Project": "sciunit" + }, + "timeout": 5, + "timeouts": null, + "tracing_config": [ + { + "mode": "PassThrough" + } + ], + "version": "3", + "vpc_config": [] + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19", + "dependencies": [ + "aws_iam_role.lambda_edge", + "data.archive_file.viewer_request" + ] + } + ] + }, + { + "mode": "managed", + "type": "null_resource", + "name": "attach_lambda_edge", + "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "8890989979717853854", + "triggers": { + "accounting_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", + "distribution": "E2HWZQGHIVF7CP", + "gatekeeper_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:3" + } + }, + "sensitive_attributes": [], + "identity_schema_version": 0, + "dependencies": [ + "aws_iam_role.lambda_edge", + "aws_lambda_function.accounting", + "aws_lambda_function.gatekeeper", + "data.archive_file.viewer_request", + "data.archive_file.viewer_response" + ] + } + ] + } + ], + "check_results": null +} diff --git a/aws/lambda-edge/terraform/terraform.tfvars b/aws/lambda-edge/terraform/terraform.tfvars new file mode 100644 index 0000000..ee823e3 --- /dev/null +++ b/aws/lambda-edge/terraform/terraform.tfvars @@ -0,0 +1,3 @@ +aws_profile = "talha-temp" +bucket_name = "sciunit2-talha" +cloudfront_distribution_id = "E2HWZQGHIVF7CP" diff --git a/aws/lambda-edge/terraform/variables.tf b/aws/lambda-edge/terraform/variables.tf new file mode 100644 index 0000000..2d66f68 --- /dev/null +++ b/aws/lambda-edge/terraform/variables.tf @@ -0,0 +1,22 @@ +variable "aws_profile" { + description = "AWS CLI profile to use" + type = string + default = "talha-temp" +} + +variable "bucket_name" { + description = "S3 bucket name used as DynamoDB partition key prefix" + type = string + default = "sciunit2-talha" +} + +variable "cloudfront_distribution_id" { + description = "ID of the existing CloudFront distribution to attach Lambda@Edge to" + type = string +} + +variable "bandwidth_limit_bytes" { + description = "Monthly bandwidth limit in bytes (1 TB = 1000000000000)" + type = number + default = 1000000000000 +} diff --git a/aws/lambda-edge/viewer-request/index.mjs b/aws/lambda-edge/viewer-request/index.mjs new file mode 100644 index 0000000..d8f65eb --- /dev/null +++ b/aws/lambda-edge/viewer-request/index.mjs @@ -0,0 +1,50 @@ +// Viewer-Request Lambda@Edge — Gatekeeper +// Checks monthly bandwidth usage and blocks downloads if 1 TB is exceeded. + +import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; + +const BUCKET_NAME = "sciunit2-talha"; +const LIMIT_BYTES = 1_000_000_000_000; // 1 TB (decimal) + +// Lambda@Edge runs at edge locations — DynamoDB is in us-east-1 +const ddb = new DynamoDBClient({ region: "us-east-1" }); + +export async function handler(event) { + const request = event.Records[0].cf.request; + + try { + // Compute current month key: "sciunit2-talha#2026-02" + const now = new Date(); + const month = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; + const pk = `${BUCKET_NAME}#${month}`; + + const result = await ddb.send( + new GetItemCommand({ + TableName: "cloudfront_bandwidth", + Key: { pk: { S: pk } }, + ProjectionExpression: "bytes", + }) + ); + + const usedBytes = result.Item?.bytes?.N ? Number(result.Item.bytes.N) : 0; + + if (usedBytes >= LIMIT_BYTES) { + // Monthly limit exceeded — return 429 Too Many Requests + return { + status: "429", + statusDescription: "Too Many Requests", + headers: { + "content-type": [{ key: "Content-Type", value: "text/plain" }], + "retry-after": [{ key: "Retry-After", value: "86400" }], + }, + body: "Monthly download bandwidth limit (1 TB) exceeded. Try again next month.", + }; + } + } catch (err) { + // Fail-open: if DynamoDB is unreachable, allow the request through + console.error("Gatekeeper error (fail-open):", err); + } + + // Allow the request + return request; +} diff --git a/aws/lambda-edge/viewer-request/package.json b/aws/lambda-edge/viewer-request/package.json new file mode 100644 index 0000000..b8f95b7 --- /dev/null +++ b/aws/lambda-edge/viewer-request/package.json @@ -0,0 +1,8 @@ +{ + "name": "cf-bandwidth-gatekeeper", + "version": "1.0.0", + "type": "module", + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.0.0" + } +} diff --git a/aws/lambda-edge/viewer-response/index.mjs b/aws/lambda-edge/viewer-response/index.mjs new file mode 100644 index 0000000..7d1b7f4 --- /dev/null +++ b/aws/lambda-edge/viewer-response/index.mjs @@ -0,0 +1,60 @@ +// Viewer-Response Lambda@Edge — Accounting +// Tracks bytes served by atomically incrementing the monthly counter in DynamoDB. + +import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb"; + +const BUCKET_NAME = "sciunit2-talha"; + +// Lambda@Edge runs at edge locations — DynamoDB is in us-east-1 +const ddb = new DynamoDBClient({ region: "us-east-1" }); + +export async function handler(event) { + const response = event.Records[0].cf.response; + + // Only count successful responses (2xx) + const status = parseInt(response.status, 10); + if (status < 200 || status >= 300) { + return response; + } + + try { + const headers = response.headers; + let bytesServed = 0; + + // For range/resumed downloads, use Content-Range to get actual bytes served + // Format: "bytes 0-999/5000" → served 1000 bytes + if (headers["content-range"]?.[0]?.value) { + const match = headers["content-range"][0].value.match(/bytes\s+(\d+)-(\d+)/); + if (match) { + bytesServed = parseInt(match[2], 10) - parseInt(match[1], 10) + 1; + } + } else if (headers["content-length"]?.[0]?.value) { + // Full response — use Content-Length + bytesServed = parseInt(headers["content-length"][0].value, 10); + } + + if (bytesServed > 0) { + // Compute current month key + const now = new Date(); + const month = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; + const pk = `${BUCKET_NAME}#${month}`; + + // Atomically increment the byte counter + await ddb.send( + new UpdateItemCommand({ + TableName: "cloudfront_bandwidth", + Key: { pk: { S: pk } }, + UpdateExpression: "ADD bytes :b", + ExpressionAttributeValues: { + ":b": { N: String(bytesServed) }, + }, + }) + ); + } + } catch (err) { + // Fail-open: don't block the response if accounting fails + console.error("Accounting error:", err); + } + + return response; +} diff --git a/aws/lambda-edge/viewer-response/package.json b/aws/lambda-edge/viewer-response/package.json new file mode 100644 index 0000000..5e233e6 --- /dev/null +++ b/aws/lambda-edge/viewer-response/package.json @@ -0,0 +1,8 @@ +{ + "name": "cf-bandwidth-accounting", + "version": "1.0.0", + "type": "module", + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.0.0" + } +} diff --git a/sciunit2/s3.py b/sciunit2/s3.py index 1a8fb64..7bce856 100644 --- a/sciunit2/s3.py +++ b/sciunit2/s3.py @@ -33,9 +33,18 @@ def fetch(url, base): Downloads a file from a CF URL and returns a file-like object. """ import requests - with requests.get(url, stream=True) as resp: - resp.raise_for_status() - f = tempfile.NamedTemporaryFile(prefix=base, dir="") - shutil.copyfileobj(resp.raw, f) - f.seek(0) - return f \ No newline at end of file + from sciunit2.exceptions import CommandError + try: + with requests.get(url, stream=True) as resp: + if resp.status_code == 429: + raise CommandError( + "Monthly download bandwidth limit exceeded. " + "Please try again next month or contact the sciunit maintainers." + ) + resp.raise_for_status() + f = tempfile.NamedTemporaryFile(prefix=base, dir="") + shutil.copyfileobj(resp.raw, f) + f.seek(0) + return f + except requests.exceptions.RequestException as exc: + raise CommandError("Failed to download: %s" % exc) \ No newline at end of file From 768915c258456cd44c372161de655228bb95f423 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Tue, 10 Feb 2026 15:46:12 -0600 Subject: [PATCH 12/16] ADD: documentation for aws resources and how to provision --- docs/aws-resources.md | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/aws-resources.md diff --git a/docs/aws-resources.md b/docs/aws-resources.md new file mode 100644 index 0000000..3819cb7 --- /dev/null +++ b/docs/aws-resources.md @@ -0,0 +1,87 @@ +# AWS Resources + +All resources are in `us-east-1` and managed via Terraform in `aws/lambda-edge/terraform/`. + +## Resource Overview + +``` + sciunit copy (upload) + │ + v + ┌──────────────────┐ + │ S3 Bucket │ + │ sciunit2-talha │ + └────────┬─────────┘ + │ OAC + v + ┌──────────────────┐ + │ CloudFront │──── viewer-request ──── Gatekeeper Lambda + │ E2HWZQGHIVF7CP │ (checks bandwidth) + │ │──── viewer-response ─── Accounting Lambda + └────────┬─────────┘ (tracks bytes) + │ │ + │ v + v ┌──────────────────┐ + sciunit open │ DynamoDB │ + (download) │cloudfront_bandwidth│ + └──────────────────┘ +``` + +## Resources + +### S3 Bucket — `sciunit2-talha` +- Stores sciunit project archives uploaded via `sciunit copy` +- All public access blocked; only accessible through CloudFront via OAC +- Objects stored under `projects//` +- Credentials stored under `persistent/` (not subject to lifecycle rules) + +### CloudFront Distribution — `E2HWZQGHIVF7CP` +- Domain: `d3okuktvxs1y4w.cloudfront.net` +- Origin Access Control (OAC): `EZDWNYQO6UZRC` — authenticates CloudFront requests to S3 +- Serves downloads for `sciunit open ` +- First 1 TB/month of bandwidth is free (vs ~$0.09/GB from S3 direct) +- Created outside Terraform; Lambda associations attached via `null_resource` + +### Lambda@Edge — Gatekeeper (`cf-bandwidth-gatekeeper`) +- **Event**: `viewer-request` — runs before CloudFront fetches the object +- **Purpose**: Checks monthly bandwidth usage in DynamoDB; returns HTTP 429 if the 1 TB limit is exceeded +- **Fail-open**: If DynamoDB is unreachable, the request is allowed through +- **Runtime**: Node.js 20.x +- Terraform resource: `aws_lambda_function.gatekeeper` + +### Lambda@Edge — Accounting (`cf-bandwidth-accounting`) +- **Event**: `viewer-response` — runs after CloudFront sends the response +- **Purpose**: Reads `Content-Length` (or `Content-Range`) from the response and atomically increments the monthly byte counter in DynamoDB +- **Runtime**: Node.js 20.x +- Terraform resource: `aws_lambda_function.accounting` + +### DynamoDB Table — `cloudfront_bandwidth` +- **Billing**: PAY_PER_REQUEST (no provisioned capacity) +- **Schema**: Partition key `pk` (String), attribute `bytes` (Number) +- **Key format**: `sciunit2-talha#YYYY-MM` (e.g., `sciunit2-talha#2026-02`) +- Counter resets naturally each month since the key includes the year-month +- Atomic updates via `UpdateItem ADD` ensure correctness under concurrent requests + +### IAM Role — `cloudfront-bandwidth-limiter` +- Trusted by `lambda.amazonaws.com` and `edgelambda.amazonaws.com` +- Permissions: `dynamodb:GetItem`, `dynamodb:UpdateItem` on the bandwidth table, plus CloudWatch Logs + +## Bandwidth Limiting + +When cumulative downloads for the current month reach 1 TB: +1. Gatekeeper Lambda reads the counter from DynamoDB +2. Returns HTTP 429 with body: "Monthly download bandwidth limit (1 TB) exceeded" +3. The sciunit CLI catches this and displays: "Monthly download bandwidth limit exceeded. Please try again next month or contact the sciunit maintainers." + +## Deployment + +```bash +# Install Lambda dependencies (required before first deploy) +cd aws/lambda-edge/viewer-request && npm install +cd aws/lambda-edge/viewer-response && npm install + +# Deploy/update all resources +cd aws/lambda-edge/terraform +terraform init +terraform apply +``` From 942eca0bc38839443299ec38c32cef6d42a17d5e Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Fri, 13 Feb 2026 14:35:26 -0600 Subject: [PATCH 13/16] Remove: resource provisioning script moved to sciunit-dev --- aws/lambda-edge/terraform/.terraform.lock.hcl | 64 --- aws/lambda-edge/terraform/cloudfront.tf | 84 ---- aws/lambda-edge/terraform/dynamodb.tf | 15 - aws/lambda-edge/terraform/iam.tf | 53 -- aws/lambda-edge/terraform/lambda.tf | 53 -- aws/lambda-edge/terraform/main.tf | 20 - aws/lambda-edge/terraform/outputs.tf | 29 -- aws/lambda-edge/terraform/terraform.tfstate | 462 ------------------ .../terraform/terraform.tfstate.backup | 462 ------------------ aws/lambda-edge/terraform/terraform.tfvars | 3 - aws/lambda-edge/terraform/variables.tf | 22 - aws/lambda-edge/viewer-request/index.mjs | 50 -- aws/lambda-edge/viewer-request/package.json | 8 - aws/lambda-edge/viewer-response/index.mjs | 60 --- aws/lambda-edge/viewer-response/package.json | 8 - docs/aws-resources.md | 87 ---- docs/s3-integration.md | 72 --- 17 files changed, 1552 deletions(-) delete mode 100644 aws/lambda-edge/terraform/.terraform.lock.hcl delete mode 100644 aws/lambda-edge/terraform/cloudfront.tf delete mode 100644 aws/lambda-edge/terraform/dynamodb.tf delete mode 100644 aws/lambda-edge/terraform/iam.tf delete mode 100644 aws/lambda-edge/terraform/lambda.tf delete mode 100644 aws/lambda-edge/terraform/main.tf delete mode 100644 aws/lambda-edge/terraform/outputs.tf delete mode 100644 aws/lambda-edge/terraform/terraform.tfstate delete mode 100644 aws/lambda-edge/terraform/terraform.tfstate.backup delete mode 100644 aws/lambda-edge/terraform/terraform.tfvars delete mode 100644 aws/lambda-edge/terraform/variables.tf delete mode 100644 aws/lambda-edge/viewer-request/index.mjs delete mode 100644 aws/lambda-edge/viewer-request/package.json delete mode 100644 aws/lambda-edge/viewer-response/index.mjs delete mode 100644 aws/lambda-edge/viewer-response/package.json delete mode 100644 docs/aws-resources.md delete mode 100644 docs/s3-integration.md diff --git a/aws/lambda-edge/terraform/.terraform.lock.hcl b/aws/lambda-edge/terraform/.terraform.lock.hcl deleted file mode 100644 index 7595bd2..0000000 --- a/aws/lambda-edge/terraform/.terraform.lock.hcl +++ /dev/null @@ -1,64 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/archive" { - version = "2.7.1" - constraints = "~> 2.0" - hashes = [ - "h1:Tr6LvLbm30zX4BRNPHhXo8SnOP0vg5UKAeunRNfnas8=", - "zh:19881bb356a4a656a865f48aee70c0b8a03c35951b7799b6113883f67f196e8e", - "zh:2fcfbf6318dd514863268b09bbe19bfc958339c636bcbcc3664b45f2b8bf5cc6", - "zh:3323ab9a504ce0a115c28e64d0739369fe85151291a2ce480d51ccbb0c381ac5", - "zh:362674746fb3da3ab9bd4e70c75a3cdd9801a6cf258991102e2c46669cf68e19", - "zh:7140a46d748fdd12212161445c46bbbf30a3f4586c6ac97dd497f0c2565fe949", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:875e6ce78b10f73b1efc849bfcc7af3a28c83a52f878f503bb22776f71d79521", - "zh:b872c6ed24e38428d817ebfb214da69ea7eefc2c38e5a774db2ccd58e54d3a22", - "zh:cd6a44f731c1633ae5d37662af86e7b01ae4c96eb8b04144255824c3f350392d", - "zh:e0600f5e8da12710b0c52d6df0ba147a5486427c1a2cc78f31eea37a47ee1b07", - "zh:f21b2e2563bbb1e44e73557bcd6cdbc1ceb369d471049c40eb56cb84b6317a60", - "zh:f752829eba1cc04a479cf7ae7271526b402e206d5bcf1fcce9f535de5ff9e4e6", - ] -} - -provider "registry.terraform.io/hashicorp/aws" { - version = "5.100.0" - constraints = "~> 5.0" - hashes = [ - "h1:hd45qFU5cFuJMpFGdUniU9mVIr5LYVWP1uMeunBpYYs=", - "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", - "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", - "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", - "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", - "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", - "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", - "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", - "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", - "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", - "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", - "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", - "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", - "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", - ] -} - -provider "registry.terraform.io/hashicorp/null" { - version = "3.2.4" - hashes = [ - "h1:127ts0CG8hFk1bHIfrBsKxcnt9bAYQCq3udWM+AACH8=", - "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", - "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", - "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", - "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", - "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", - "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", - "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", - "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", - "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", - "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", - ] -} diff --git a/aws/lambda-edge/terraform/cloudfront.tf b/aws/lambda-edge/terraform/cloudfront.tf deleted file mode 100644 index 916a136..0000000 --- a/aws/lambda-edge/terraform/cloudfront.tf +++ /dev/null @@ -1,84 +0,0 @@ -# Attach Lambda@Edge functions to the existing CloudFront distribution. -# -# The CloudFront distribution was created outside of Terraform. -# Using null_resource + local-exec to safely attach Lambda associations -# without Terraform managing (and potentially overwriting) the full -# CloudFront config. - -resource "null_resource" "attach_lambda_edge" { - # Re-run when Lambda versions change - triggers = { - gatekeeper_arn = aws_lambda_function.gatekeeper.qualified_arn - accounting_arn = aws_lambda_function.accounting.qualified_arn - distribution = var.cloudfront_distribution_id - } - - provisioner "local-exec" { - interpreter = ["bash", "-c"] - command = <<-EOT - set -euo pipefail - - DIST_ID="${var.cloudfront_distribution_id}" - PROFILE="${var.aws_profile}" - GATEKEEPER_ARN="${aws_lambda_function.gatekeeper.qualified_arn}" - ACCOUNTING_ARN="${aws_lambda_function.accounting.qualified_arn}" - - echo "Fetching current distribution config..." - aws cloudfront get-distribution-config \ - --id "$DIST_ID" \ - --profile "$PROFILE" > /tmp/cf-dist-config.json - - ETAG=$(python3 -c "import json; print(json.load(open('/tmp/cf-dist-config.json'))['ETag'])") - echo "ETag: $ETAG" - - python3 << 'PYEOF' -import json, os - -gatekeeper_arn = os.environ.get("GATEKEEPER_ARN", "${aws_lambda_function.gatekeeper.qualified_arn}") -accounting_arn = os.environ.get("ACCOUNTING_ARN", "${aws_lambda_function.accounting.qualified_arn}") - -with open("/tmp/cf-dist-config.json") as f: - data = json.load(f) - -config = data["DistributionConfig"] -config["DefaultCacheBehavior"]["LambdaFunctionAssociations"] = { - "Quantity": 2, - "Items": [ - { - "LambdaFunctionARN": gatekeeper_arn, - "EventType": "viewer-request", - "IncludeBody": False, - }, - { - "LambdaFunctionARN": accounting_arn, - "EventType": "viewer-response", - "IncludeBody": False, - }, - ], -} - -with open("/tmp/cf-dist-config-updated.json", "w") as f: - json.dump(config, f, indent=2) -PYEOF - - echo "Updating CloudFront distribution..." - aws cloudfront update-distribution \ - --id "$DIST_ID" \ - --distribution-config file:///tmp/cf-dist-config-updated.json \ - --if-match "$ETAG" \ - --profile "$PROFILE" > /dev/null - - echo "Lambda@Edge attached to distribution $DIST_ID" - EOT - - environment = { - GATEKEEPER_ARN = aws_lambda_function.gatekeeper.qualified_arn - ACCOUNTING_ARN = aws_lambda_function.accounting.qualified_arn - } - } - - depends_on = [ - aws_lambda_function.gatekeeper, - aws_lambda_function.accounting, - ] -} diff --git a/aws/lambda-edge/terraform/dynamodb.tf b/aws/lambda-edge/terraform/dynamodb.tf deleted file mode 100644 index 98514bc..0000000 --- a/aws/lambda-edge/terraform/dynamodb.tf +++ /dev/null @@ -1,15 +0,0 @@ -resource "aws_dynamodb_table" "cloudfront_bandwidth" { - name = "cloudfront_bandwidth" - billing_mode = "PAY_PER_REQUEST" - hash_key = "pk" - - attribute { - name = "pk" - type = "S" - } - - tags = { - Project = "sciunit" - Purpose = "CloudFront bandwidth tracking" - } -} diff --git a/aws/lambda-edge/terraform/iam.tf b/aws/lambda-edge/terraform/iam.tf deleted file mode 100644 index c760b31..0000000 --- a/aws/lambda-edge/terraform/iam.tf +++ /dev/null @@ -1,53 +0,0 @@ -data "aws_caller_identity" "current" {} - -resource "aws_iam_role" "lambda_edge" { - name = "cloudfront-bandwidth-limiter" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Principal = { - Service = [ - "lambda.amazonaws.com", - "edgelambda.amazonaws.com" - ] - } - Action = "sts:AssumeRole" - } - ] - }) - - tags = { - Project = "sciunit" - } -} - -resource "aws_iam_role_policy" "lambda_edge" { - name = "cloudfront-bandwidth-limiter-policy" - role = aws_iam_role.lambda_edge.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "dynamodb:GetItem", - "dynamodb:UpdateItem" - ] - Resource = aws_dynamodb_table.cloudfront_bandwidth.arn - }, - { - Effect = "Allow" - Action = [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ] - Resource = "arn:aws:logs:*:${data.aws_caller_identity.current.account_id}:*" - } - ] - }) -} diff --git a/aws/lambda-edge/terraform/lambda.tf b/aws/lambda-edge/terraform/lambda.tf deleted file mode 100644 index 249acb0..0000000 --- a/aws/lambda-edge/terraform/lambda.tf +++ /dev/null @@ -1,53 +0,0 @@ -# --- Zip archives from existing Lambda source files --- - -data "archive_file" "viewer_request" { - type = "zip" - source_dir = "${path.module}/../viewer-request" - excludes = ["package.json", "package-lock.json"] - output_path = "${path.module}/dist/viewer-request.zip" -} - -data "archive_file" "viewer_response" { - type = "zip" - source_dir = "${path.module}/../viewer-response" - excludes = ["package.json", "package-lock.json"] - output_path = "${path.module}/dist/viewer-response.zip" -} - -# --- Gatekeeper: blocks requests when monthly bandwidth >= 1 TB --- - -resource "aws_lambda_function" "gatekeeper" { - function_name = "cf-bandwidth-gatekeeper" - description = "CloudFront viewer-request: blocks downloads when monthly bandwidth limit is exceeded" - runtime = "nodejs20.x" - handler = "index.handler" - role = aws_iam_role.lambda_edge.arn - filename = data.archive_file.viewer_request.output_path - source_code_hash = data.archive_file.viewer_request.output_base64sha256 - timeout = 5 - memory_size = 128 - publish = true # Lambda@Edge requires a published version - - tags = { - Project = "sciunit" - } -} - -# --- Accounting: tracks bytes served in DynamoDB --- - -resource "aws_lambda_function" "accounting" { - function_name = "cf-bandwidth-accounting" - description = "CloudFront viewer-response: tracks bytes served for monthly bandwidth accounting" - runtime = "nodejs20.x" - handler = "index.handler" - role = aws_iam_role.lambda_edge.arn - filename = data.archive_file.viewer_response.output_path - source_code_hash = data.archive_file.viewer_response.output_base64sha256 - timeout = 5 - memory_size = 128 - publish = true # Lambda@Edge requires a published version - - tags = { - Project = "sciunit" - } -} diff --git a/aws/lambda-edge/terraform/main.tf b/aws/lambda-edge/terraform/main.tf deleted file mode 100644 index 7a5cbcf..0000000 --- a/aws/lambda-edge/terraform/main.tf +++ /dev/null @@ -1,20 +0,0 @@ -terraform { - required_version = ">= 1.5" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - archive = { - source = "hashicorp/archive" - version = "~> 2.0" - } - } -} - -# Lambda@Edge must be deployed in us-east-1 -provider "aws" { - region = "us-east-1" - profile = var.aws_profile -} diff --git a/aws/lambda-edge/terraform/outputs.tf b/aws/lambda-edge/terraform/outputs.tf deleted file mode 100644 index ec03100..0000000 --- a/aws/lambda-edge/terraform/outputs.tf +++ /dev/null @@ -1,29 +0,0 @@ -output "dynamodb_table_name" { - description = "DynamoDB table for bandwidth tracking" - value = aws_dynamodb_table.cloudfront_bandwidth.name -} - -output "dynamodb_table_arn" { - description = "DynamoDB table ARN" - value = aws_dynamodb_table.cloudfront_bandwidth.arn -} - -output "iam_role_arn" { - description = "IAM role ARN for Lambda@Edge functions" - value = aws_iam_role.lambda_edge.arn -} - -output "gatekeeper_qualified_arn" { - description = "Published version ARN of the gatekeeper Lambda (viewer-request)" - value = aws_lambda_function.gatekeeper.qualified_arn -} - -output "accounting_qualified_arn" { - description = "Published version ARN of the accounting Lambda (viewer-response)" - value = aws_lambda_function.accounting.qualified_arn -} - -output "cloudfront_distribution_id" { - description = "CloudFront distribution ID with Lambda@Edge attached" - value = var.cloudfront_distribution_id -} diff --git a/aws/lambda-edge/terraform/terraform.tfstate b/aws/lambda-edge/terraform/terraform.tfstate deleted file mode 100644 index 5bab6fe..0000000 --- a/aws/lambda-edge/terraform/terraform.tfstate +++ /dev/null @@ -1,462 +0,0 @@ -{ - "version": 4, - "terraform_version": "1.13.3", - "serial": 20, - "lineage": "bf49dced-73b0-1b2a-8f92-c4174b0c344e", - "outputs": { - "accounting_qualified_arn": { - "value": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", - "type": "string" - }, - "cloudfront_distribution_id": { - "value": "E2HWZQGHIVF7CP", - "type": "string" - }, - "dynamodb_table_arn": { - "value": "arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth", - "type": "string" - }, - "dynamodb_table_name": { - "value": "cloudfront_bandwidth", - "type": "string" - }, - "gatekeeper_qualified_arn": { - "value": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:4", - "type": "string" - }, - "iam_role_arn": { - "value": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", - "type": "string" - } - }, - "resources": [ - { - "mode": "data", - "type": "archive_file", - "name": "viewer_request", - "provider": "provider[\"registry.terraform.io/hashicorp/archive\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "exclude_symlink_directories": null, - "excludes": [ - "package-lock.json", - "package.json" - ], - "id": "d09d7bf65b7fd160a9e3cff730751c1150c4cded", - "output_base64sha256": "p/r+sxpgA4V73VOve0ngjznrw15S0ap5FTyf3N9oNCg=", - "output_base64sha512": "uJQUUIAnmpBHwFS3qadWNjhIX7VSl3cEZGQrBa/nsgCrWSORNz/QCQNJ4Edtl3DJYTl2r1x/b6lJcSr3ABnOEA==", - "output_file_mode": null, - "output_md5": "0c7b0437655267eb242949706278ab8e", - "output_path": "./dist/viewer-request.zip", - "output_sha": "d09d7bf65b7fd160a9e3cff730751c1150c4cded", - "output_sha256": "a7fafeb31a6003857bdd53af7b49e08f39ebc35e52d1aa79153c9fdcdf683428", - "output_sha512": "b894145080279a9047c054b7a9a7563638485fb55297770464642b05afe7b200ab592391373fd0090349e0476d9770c9613976af5c7f6fa949712af70019ce10", - "output_size": 3084912, - "source": [], - "source_content": null, - "source_content_filename": null, - "source_dir": "./../viewer-request", - "source_file": null, - "type": "zip" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - }, - { - "mode": "data", - "type": "archive_file", - "name": "viewer_response", - "provider": "provider[\"registry.terraform.io/hashicorp/archive\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "exclude_symlink_directories": null, - "excludes": [ - "package-lock.json", - "package.json" - ], - "id": "06e1333a5f5336339a6470c3919d3abc8261204d", - "output_base64sha256": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", - "output_base64sha512": "kF1S8kTuVbWNnvs3h52Y+wlK5Mxg6iE5aS0+geF3zm0qdJUzKJGjyxZZB3V+ZRYgUIgwBYwW40Su/gkDwyf5MQ==", - "output_file_mode": null, - "output_md5": "4d8e11bbd88a77cda943c98f722b60bb", - "output_path": "./dist/viewer-response.zip", - "output_sha": "06e1333a5f5336339a6470c3919d3abc8261204d", - "output_sha256": "9c43686bf5641014cb74c5a1764a87bb601e232997ae7c4e2da4a8c56bfb1f4d", - "output_sha512": "905d52f244ee55b58d9efb37879d98fb094ae4cc60ea2139692d3e81e177ce6d2a7495332891a3cb165907757e651620508830058c16e344aefe0903c327f931", - "output_size": 3084980, - "source": [], - "source_content": null, - "source_content_filename": null, - "source_dir": "./../viewer-response", - "source_file": null, - "type": "zip" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - }, - { - "mode": "data", - "type": "aws_caller_identity", - "name": "current", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "account_id": "664603945217", - "arn": "arn:aws:sts::664603945217:assumed-role/talha-dev-role/talha-session", - "id": "664603945217", - "user_id": "AROAZVPL3NEAW7U3VTRIQ:talha-session" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - }, - { - "mode": "managed", - "type": "aws_dynamodb_table", - "name": "cloudfront_bandwidth", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 1, - "attributes": { - "arn": "arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth", - "attribute": [ - { - "name": "pk", - "type": "S" - } - ], - "billing_mode": "PAY_PER_REQUEST", - "deletion_protection_enabled": false, - "global_secondary_index": [], - "hash_key": "pk", - "id": "cloudfront_bandwidth", - "import_table": [], - "local_secondary_index": [], - "name": "cloudfront_bandwidth", - "on_demand_throughput": [], - "point_in_time_recovery": [ - { - "enabled": false, - "recovery_period_in_days": 0 - } - ], - "range_key": null, - "read_capacity": 0, - "replica": [], - "restore_date_time": null, - "restore_source_name": null, - "restore_source_table_arn": null, - "restore_to_latest_time": null, - "server_side_encryption": [], - "stream_arn": "", - "stream_enabled": false, - "stream_label": "", - "stream_view_type": "", - "table_class": "STANDARD", - "tags": { - "Project": "sciunit", - "Purpose": "CloudFront bandwidth tracking" - }, - "tags_all": { - "Project": "sciunit", - "Purpose": "CloudFront bandwidth tracking" - }, - "timeouts": null, - "ttl": [ - { - "attribute_name": "", - "enabled": false - } - ], - "write_capacity": 0 - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxODAwMDAwMDAwMDAwLCJkZWxldGUiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjozNjAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=" - } - ] - }, - { - "mode": "managed", - "type": "aws_iam_role", - "name": "lambda_edge", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "arn": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", - "assume_role_policy": "{\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"lambda.amazonaws.com\",\"edgelambda.amazonaws.com\"]}}],\"Version\":\"2012-10-17\"}", - "create_date": "2026-02-10T18:16:05Z", - "description": "", - "force_detach_policies": false, - "id": "cloudfront-bandwidth-limiter", - "inline_policy": [ - { - "name": "cloudfront-bandwidth-limiter-policy", - "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"dynamodb:GetItem\",\"dynamodb:UpdateItem\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth\"},{\"Action\":[\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:logs:*:664603945217:*\"}]}" - } - ], - "managed_policy_arns": [], - "max_session_duration": 3600, - "name": "cloudfront-bandwidth-limiter", - "name_prefix": "", - "path": "/", - "permissions_boundary": "", - "tags": { - "Project": "sciunit" - }, - "tags_all": { - "Project": "sciunit" - }, - "unique_id": "AROAZVPL3NEA7EPQ4W3NK" - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "bnVsbA==" - } - ] - }, - { - "mode": "managed", - "type": "aws_iam_role_policy", - "name": "lambda_edge", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "id": "cloudfront-bandwidth-limiter:cloudfront-bandwidth-limiter-policy", - "name": "cloudfront-bandwidth-limiter-policy", - "name_prefix": "", - "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"dynamodb:GetItem\",\"dynamodb:UpdateItem\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth\"},{\"Action\":[\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:logs:*:664603945217:*\"}]}", - "role": "cloudfront-bandwidth-limiter" - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "bnVsbA==", - "dependencies": [ - "aws_dynamodb_table.cloudfront_bandwidth", - "aws_iam_role.lambda_edge", - "data.aws_caller_identity.current" - ] - } - ] - }, - { - "mode": "managed", - "type": "aws_lambda_function", - "name": "accounting", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "architectures": [ - "x86_64" - ], - "arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting", - "code_sha256": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", - "code_signing_config_arn": "", - "dead_letter_config": [], - "description": "CloudFront viewer-response: tracks bytes served for monthly bandwidth accounting", - "environment": [], - "ephemeral_storage": [ - { - "size": 512 - } - ], - "file_system_config": [], - "filename": "./dist/viewer-response.zip", - "function_name": "cf-bandwidth-accounting", - "handler": "index.handler", - "id": "cf-bandwidth-accounting", - "image_config": [], - "image_uri": "", - "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting/invocations", - "kms_key_arn": "", - "last_modified": "2026-02-10T18:35:13.000+0000", - "layers": [], - "logging_config": [ - { - "application_log_level": "", - "log_format": "Text", - "log_group": "/aws/lambda/cf-bandwidth-accounting", - "system_log_level": "" - } - ], - "memory_size": 128, - "package_type": "Zip", - "publish": true, - "qualified_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", - "qualified_invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2/invocations", - "replace_security_groups_on_destroy": null, - "replacement_security_group_ids": null, - "reserved_concurrent_executions": -1, - "role": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", - "runtime": "nodejs20.x", - "s3_bucket": null, - "s3_key": null, - "s3_object_version": null, - "signing_job_arn": "", - "signing_profile_version_arn": "", - "skip_destroy": false, - "snap_start": [], - "source_code_hash": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", - "source_code_size": 3084980, - "tags": { - "Project": "sciunit" - }, - "tags_all": { - "Project": "sciunit" - }, - "timeout": 5, - "timeouts": null, - "tracing_config": [ - { - "mode": "PassThrough" - } - ], - "version": "2", - "vpc_config": [] - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19", - "dependencies": [ - "aws_iam_role.lambda_edge", - "data.archive_file.viewer_response" - ] - } - ] - }, - { - "mode": "managed", - "type": "aws_lambda_function", - "name": "gatekeeper", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "architectures": [ - "x86_64" - ], - "arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper", - "code_sha256": "p/r+sxpgA4V73VOve0ngjznrw15S0ap5FTyf3N9oNCg=", - "code_signing_config_arn": "", - "dead_letter_config": [], - "description": "CloudFront viewer-request: blocks downloads when monthly bandwidth limit is exceeded", - "environment": [], - "ephemeral_storage": [ - { - "size": 512 - } - ], - "file_system_config": [], - "filename": "./dist/viewer-request.zip", - "function_name": "cf-bandwidth-gatekeeper", - "handler": "index.handler", - "id": "cf-bandwidth-gatekeeper", - "image_config": [], - "image_uri": "", - "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper/invocations", - "kms_key_arn": "", - "last_modified": "2026-02-10T21:18:20.000+0000", - "layers": [], - "logging_config": [ - { - "application_log_level": "", - "log_format": "Text", - "log_group": "/aws/lambda/cf-bandwidth-gatekeeper", - "system_log_level": "" - } - ], - "memory_size": 128, - "package_type": "Zip", - "publish": true, - "qualified_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:4", - "qualified_invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:4/invocations", - "replace_security_groups_on_destroy": null, - "replacement_security_group_ids": null, - "reserved_concurrent_executions": -1, - "role": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", - "runtime": "nodejs20.x", - "s3_bucket": null, - "s3_key": null, - "s3_object_version": null, - "signing_job_arn": "", - "signing_profile_version_arn": "", - "skip_destroy": false, - "snap_start": [], - "source_code_hash": "p/r+sxpgA4V73VOve0ngjznrw15S0ap5FTyf3N9oNCg=", - "source_code_size": 3084912, - "tags": { - "Project": "sciunit" - }, - "tags_all": { - "Project": "sciunit" - }, - "timeout": 5, - "timeouts": null, - "tracing_config": [ - { - "mode": "PassThrough" - } - ], - "version": "4", - "vpc_config": [] - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19", - "dependencies": [ - "aws_iam_role.lambda_edge", - "data.archive_file.viewer_request" - ] - } - ] - }, - { - "mode": "managed", - "type": "null_resource", - "name": "attach_lambda_edge", - "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "id": "6665986102345189486", - "triggers": { - "accounting_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", - "distribution": "E2HWZQGHIVF7CP", - "gatekeeper_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:4" - } - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "dependencies": [ - "aws_iam_role.lambda_edge", - "aws_lambda_function.accounting", - "aws_lambda_function.gatekeeper", - "data.archive_file.viewer_request", - "data.archive_file.viewer_response" - ] - } - ] - } - ], - "check_results": null -} diff --git a/aws/lambda-edge/terraform/terraform.tfstate.backup b/aws/lambda-edge/terraform/terraform.tfstate.backup deleted file mode 100644 index c9f5b45..0000000 --- a/aws/lambda-edge/terraform/terraform.tfstate.backup +++ /dev/null @@ -1,462 +0,0 @@ -{ - "version": 4, - "terraform_version": "1.13.3", - "serial": 16, - "lineage": "bf49dced-73b0-1b2a-8f92-c4174b0c344e", - "outputs": { - "accounting_qualified_arn": { - "value": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", - "type": "string" - }, - "cloudfront_distribution_id": { - "value": "E2HWZQGHIVF7CP", - "type": "string" - }, - "dynamodb_table_arn": { - "value": "arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth", - "type": "string" - }, - "dynamodb_table_name": { - "value": "cloudfront_bandwidth", - "type": "string" - }, - "gatekeeper_qualified_arn": { - "value": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:3", - "type": "string" - }, - "iam_role_arn": { - "value": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", - "type": "string" - } - }, - "resources": [ - { - "mode": "data", - "type": "archive_file", - "name": "viewer_request", - "provider": "provider[\"registry.terraform.io/hashicorp/archive\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "exclude_symlink_directories": null, - "excludes": [ - "package-lock.json", - "package.json" - ], - "id": "12238e1eb31c09920a6d3dd1caba668344c8be53", - "output_base64sha256": "wXDP/Iv1tc2XDNCI3F6+qinLeZZjzOPoWoR13Ry99hQ=", - "output_base64sha512": "MsW6XpRxk4YaRT0BrWrQF/zGJL11Nfi43VOKbg6xU2M0PamJ5WdAjLpyXJT9LgrY7q5ks5btkvqvzXS0Mqr0jw==", - "output_file_mode": null, - "output_md5": "0069b93d2d712729b6957a25e87a0117", - "output_path": "./dist/viewer-request.zip", - "output_sha": "12238e1eb31c09920a6d3dd1caba668344c8be53", - "output_sha256": "c170cffc8bf5b5cd970cd088dc5ebeaa29cb799663cce3e85a8475dd1cbdf614", - "output_sha512": "32c5ba5e947193861a453d01ad6ad017fcc624bd7535f8b8dd538a6e0eb15363343da989e567408cba725c94fd2e0ad8eeae64b396ed92faafcd74b432aaf48f", - "output_size": 3084940, - "source": [], - "source_content": null, - "source_content_filename": null, - "source_dir": "./../viewer-request", - "source_file": null, - "type": "zip" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - }, - { - "mode": "data", - "type": "archive_file", - "name": "viewer_response", - "provider": "provider[\"registry.terraform.io/hashicorp/archive\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "exclude_symlink_directories": null, - "excludes": [ - "package-lock.json", - "package.json" - ], - "id": "06e1333a5f5336339a6470c3919d3abc8261204d", - "output_base64sha256": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", - "output_base64sha512": "kF1S8kTuVbWNnvs3h52Y+wlK5Mxg6iE5aS0+geF3zm0qdJUzKJGjyxZZB3V+ZRYgUIgwBYwW40Su/gkDwyf5MQ==", - "output_file_mode": null, - "output_md5": "4d8e11bbd88a77cda943c98f722b60bb", - "output_path": "./dist/viewer-response.zip", - "output_sha": "06e1333a5f5336339a6470c3919d3abc8261204d", - "output_sha256": "9c43686bf5641014cb74c5a1764a87bb601e232997ae7c4e2da4a8c56bfb1f4d", - "output_sha512": "905d52f244ee55b58d9efb37879d98fb094ae4cc60ea2139692d3e81e177ce6d2a7495332891a3cb165907757e651620508830058c16e344aefe0903c327f931", - "output_size": 3084980, - "source": [], - "source_content": null, - "source_content_filename": null, - "source_dir": "./../viewer-response", - "source_file": null, - "type": "zip" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - }, - { - "mode": "data", - "type": "aws_caller_identity", - "name": "current", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "account_id": "664603945217", - "arn": "arn:aws:sts::664603945217:assumed-role/talha-dev-role/talha-session", - "id": "664603945217", - "user_id": "AROAZVPL3NEAW7U3VTRIQ:talha-session" - }, - "sensitive_attributes": [], - "identity_schema_version": 0 - } - ] - }, - { - "mode": "managed", - "type": "aws_dynamodb_table", - "name": "cloudfront_bandwidth", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 1, - "attributes": { - "arn": "arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth", - "attribute": [ - { - "name": "pk", - "type": "S" - } - ], - "billing_mode": "PAY_PER_REQUEST", - "deletion_protection_enabled": false, - "global_secondary_index": [], - "hash_key": "pk", - "id": "cloudfront_bandwidth", - "import_table": [], - "local_secondary_index": [], - "name": "cloudfront_bandwidth", - "on_demand_throughput": [], - "point_in_time_recovery": [ - { - "enabled": false, - "recovery_period_in_days": 0 - } - ], - "range_key": null, - "read_capacity": 0, - "replica": [], - "restore_date_time": null, - "restore_source_name": null, - "restore_source_table_arn": null, - "restore_to_latest_time": null, - "server_side_encryption": [], - "stream_arn": "", - "stream_enabled": false, - "stream_label": "", - "stream_view_type": "", - "table_class": "STANDARD", - "tags": { - "Project": "sciunit", - "Purpose": "CloudFront bandwidth tracking" - }, - "tags_all": { - "Project": "sciunit", - "Purpose": "CloudFront bandwidth tracking" - }, - "timeouts": null, - "ttl": [ - { - "attribute_name": "", - "enabled": false - } - ], - "write_capacity": 0 - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxODAwMDAwMDAwMDAwLCJkZWxldGUiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjozNjAwMDAwMDAwMDAwfSwic2NoZW1hX3ZlcnNpb24iOiIxIn0=" - } - ] - }, - { - "mode": "managed", - "type": "aws_iam_role", - "name": "lambda_edge", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "arn": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", - "assume_role_policy": "{\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"lambda.amazonaws.com\",\"edgelambda.amazonaws.com\"]}}],\"Version\":\"2012-10-17\"}", - "create_date": "2026-02-10T18:16:05Z", - "description": "", - "force_detach_policies": false, - "id": "cloudfront-bandwidth-limiter", - "inline_policy": [ - { - "name": "cloudfront-bandwidth-limiter-policy", - "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"dynamodb:GetItem\",\"dynamodb:UpdateItem\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth\"},{\"Action\":[\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:logs:*:664603945217:*\"}]}" - } - ], - "managed_policy_arns": [], - "max_session_duration": 3600, - "name": "cloudfront-bandwidth-limiter", - "name_prefix": "", - "path": "/", - "permissions_boundary": "", - "tags": { - "Project": "sciunit" - }, - "tags_all": { - "Project": "sciunit" - }, - "unique_id": "AROAZVPL3NEA7EPQ4W3NK" - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "bnVsbA==" - } - ] - }, - { - "mode": "managed", - "type": "aws_iam_role_policy", - "name": "lambda_edge", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "id": "cloudfront-bandwidth-limiter:cloudfront-bandwidth-limiter-policy", - "name": "cloudfront-bandwidth-limiter-policy", - "name_prefix": "", - "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"dynamodb:GetItem\",\"dynamodb:UpdateItem\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:dynamodb:us-east-1:664603945217:table/cloudfront_bandwidth\"},{\"Action\":[\"logs:CreateLogGroup\",\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Effect\":\"Allow\",\"Resource\":\"arn:aws:logs:*:664603945217:*\"}]}", - "role": "cloudfront-bandwidth-limiter" - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "bnVsbA==", - "dependencies": [ - "aws_dynamodb_table.cloudfront_bandwidth", - "aws_iam_role.lambda_edge", - "data.aws_caller_identity.current" - ] - } - ] - }, - { - "mode": "managed", - "type": "aws_lambda_function", - "name": "accounting", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "architectures": [ - "x86_64" - ], - "arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting", - "code_sha256": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", - "code_signing_config_arn": "", - "dead_letter_config": [], - "description": "CloudFront viewer-response: tracks bytes served for monthly bandwidth accounting", - "environment": [], - "ephemeral_storage": [ - { - "size": 512 - } - ], - "file_system_config": [], - "filename": "./dist/viewer-response.zip", - "function_name": "cf-bandwidth-accounting", - "handler": "index.handler", - "id": "cf-bandwidth-accounting", - "image_config": [], - "image_uri": "", - "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting/invocations", - "kms_key_arn": "", - "last_modified": "2026-02-10T18:35:13.000+0000", - "layers": [], - "logging_config": [ - { - "application_log_level": "", - "log_format": "Text", - "log_group": "/aws/lambda/cf-bandwidth-accounting", - "system_log_level": "" - } - ], - "memory_size": 128, - "package_type": "Zip", - "publish": true, - "qualified_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", - "qualified_invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2/invocations", - "replace_security_groups_on_destroy": null, - "replacement_security_group_ids": null, - "reserved_concurrent_executions": -1, - "role": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", - "runtime": "nodejs20.x", - "s3_bucket": null, - "s3_key": null, - "s3_object_version": null, - "signing_job_arn": "", - "signing_profile_version_arn": "", - "skip_destroy": false, - "snap_start": [], - "source_code_hash": "nENoa/VkEBTLdMWhdkqHu2AeIymXrnxOLaSoxWv7H00=", - "source_code_size": 3084980, - "tags": { - "Project": "sciunit" - }, - "tags_all": { - "Project": "sciunit" - }, - "timeout": 5, - "timeouts": null, - "tracing_config": [ - { - "mode": "PassThrough" - } - ], - "version": "2", - "vpc_config": [] - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19", - "dependencies": [ - "aws_iam_role.lambda_edge", - "data.archive_file.viewer_response" - ] - } - ] - }, - { - "mode": "managed", - "type": "aws_lambda_function", - "name": "gatekeeper", - "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "architectures": [ - "x86_64" - ], - "arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper", - "code_sha256": "wXDP/Iv1tc2XDNCI3F6+qinLeZZjzOPoWoR13Ry99hQ=", - "code_signing_config_arn": "", - "dead_letter_config": [], - "description": "CloudFront viewer-request: blocks downloads when monthly bandwidth limit is exceeded", - "environment": [], - "ephemeral_storage": [ - { - "size": 512 - } - ], - "file_system_config": [], - "filename": "./dist/viewer-request.zip", - "function_name": "cf-bandwidth-gatekeeper", - "handler": "index.handler", - "id": "cf-bandwidth-gatekeeper", - "image_config": [], - "image_uri": "", - "invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper/invocations", - "kms_key_arn": "", - "last_modified": "2026-02-10T21:11:14.000+0000", - "layers": [], - "logging_config": [ - { - "application_log_level": "", - "log_format": "Text", - "log_group": "/aws/lambda/cf-bandwidth-gatekeeper", - "system_log_level": "" - } - ], - "memory_size": 128, - "package_type": "Zip", - "publish": true, - "qualified_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:3", - "qualified_invoke_arn": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:3/invocations", - "replace_security_groups_on_destroy": null, - "replacement_security_group_ids": null, - "reserved_concurrent_executions": -1, - "role": "arn:aws:iam::664603945217:role/cloudfront-bandwidth-limiter", - "runtime": "nodejs20.x", - "s3_bucket": null, - "s3_key": null, - "s3_object_version": null, - "signing_job_arn": "", - "signing_profile_version_arn": "", - "skip_destroy": false, - "snap_start": [], - "source_code_hash": "wXDP/Iv1tc2XDNCI3F6+qinLeZZjzOPoWoR13Ry99hQ=", - "source_code_size": 3084940, - "tags": { - "Project": "sciunit" - }, - "tags_all": { - "Project": "sciunit" - }, - "timeout": 5, - "timeouts": null, - "tracing_config": [ - { - "mode": "PassThrough" - } - ], - "version": "3", - "vpc_config": [] - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjYwMDAwMDAwMDAwMH19", - "dependencies": [ - "aws_iam_role.lambda_edge", - "data.archive_file.viewer_request" - ] - } - ] - }, - { - "mode": "managed", - "type": "null_resource", - "name": "attach_lambda_edge", - "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "id": "8890989979717853854", - "triggers": { - "accounting_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-accounting:2", - "distribution": "E2HWZQGHIVF7CP", - "gatekeeper_arn": "arn:aws:lambda:us-east-1:664603945217:function:cf-bandwidth-gatekeeper:3" - } - }, - "sensitive_attributes": [], - "identity_schema_version": 0, - "dependencies": [ - "aws_iam_role.lambda_edge", - "aws_lambda_function.accounting", - "aws_lambda_function.gatekeeper", - "data.archive_file.viewer_request", - "data.archive_file.viewer_response" - ] - } - ] - } - ], - "check_results": null -} diff --git a/aws/lambda-edge/terraform/terraform.tfvars b/aws/lambda-edge/terraform/terraform.tfvars deleted file mode 100644 index ee823e3..0000000 --- a/aws/lambda-edge/terraform/terraform.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -aws_profile = "talha-temp" -bucket_name = "sciunit2-talha" -cloudfront_distribution_id = "E2HWZQGHIVF7CP" diff --git a/aws/lambda-edge/terraform/variables.tf b/aws/lambda-edge/terraform/variables.tf deleted file mode 100644 index 2d66f68..0000000 --- a/aws/lambda-edge/terraform/variables.tf +++ /dev/null @@ -1,22 +0,0 @@ -variable "aws_profile" { - description = "AWS CLI profile to use" - type = string - default = "talha-temp" -} - -variable "bucket_name" { - description = "S3 bucket name used as DynamoDB partition key prefix" - type = string - default = "sciunit2-talha" -} - -variable "cloudfront_distribution_id" { - description = "ID of the existing CloudFront distribution to attach Lambda@Edge to" - type = string -} - -variable "bandwidth_limit_bytes" { - description = "Monthly bandwidth limit in bytes (1 TB = 1000000000000)" - type = number - default = 1000000000000 -} diff --git a/aws/lambda-edge/viewer-request/index.mjs b/aws/lambda-edge/viewer-request/index.mjs deleted file mode 100644 index d8f65eb..0000000 --- a/aws/lambda-edge/viewer-request/index.mjs +++ /dev/null @@ -1,50 +0,0 @@ -// Viewer-Request Lambda@Edge — Gatekeeper -// Checks monthly bandwidth usage and blocks downloads if 1 TB is exceeded. - -import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; - -const BUCKET_NAME = "sciunit2-talha"; -const LIMIT_BYTES = 1_000_000_000_000; // 1 TB (decimal) - -// Lambda@Edge runs at edge locations — DynamoDB is in us-east-1 -const ddb = new DynamoDBClient({ region: "us-east-1" }); - -export async function handler(event) { - const request = event.Records[0].cf.request; - - try { - // Compute current month key: "sciunit2-talha#2026-02" - const now = new Date(); - const month = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; - const pk = `${BUCKET_NAME}#${month}`; - - const result = await ddb.send( - new GetItemCommand({ - TableName: "cloudfront_bandwidth", - Key: { pk: { S: pk } }, - ProjectionExpression: "bytes", - }) - ); - - const usedBytes = result.Item?.bytes?.N ? Number(result.Item.bytes.N) : 0; - - if (usedBytes >= LIMIT_BYTES) { - // Monthly limit exceeded — return 429 Too Many Requests - return { - status: "429", - statusDescription: "Too Many Requests", - headers: { - "content-type": [{ key: "Content-Type", value: "text/plain" }], - "retry-after": [{ key: "Retry-After", value: "86400" }], - }, - body: "Monthly download bandwidth limit (1 TB) exceeded. Try again next month.", - }; - } - } catch (err) { - // Fail-open: if DynamoDB is unreachable, allow the request through - console.error("Gatekeeper error (fail-open):", err); - } - - // Allow the request - return request; -} diff --git a/aws/lambda-edge/viewer-request/package.json b/aws/lambda-edge/viewer-request/package.json deleted file mode 100644 index b8f95b7..0000000 --- a/aws/lambda-edge/viewer-request/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "cf-bandwidth-gatekeeper", - "version": "1.0.0", - "type": "module", - "dependencies": { - "@aws-sdk/client-dynamodb": "^3.0.0" - } -} diff --git a/aws/lambda-edge/viewer-response/index.mjs b/aws/lambda-edge/viewer-response/index.mjs deleted file mode 100644 index 7d1b7f4..0000000 --- a/aws/lambda-edge/viewer-response/index.mjs +++ /dev/null @@ -1,60 +0,0 @@ -// Viewer-Response Lambda@Edge — Accounting -// Tracks bytes served by atomically incrementing the monthly counter in DynamoDB. - -import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb"; - -const BUCKET_NAME = "sciunit2-talha"; - -// Lambda@Edge runs at edge locations — DynamoDB is in us-east-1 -const ddb = new DynamoDBClient({ region: "us-east-1" }); - -export async function handler(event) { - const response = event.Records[0].cf.response; - - // Only count successful responses (2xx) - const status = parseInt(response.status, 10); - if (status < 200 || status >= 300) { - return response; - } - - try { - const headers = response.headers; - let bytesServed = 0; - - // For range/resumed downloads, use Content-Range to get actual bytes served - // Format: "bytes 0-999/5000" → served 1000 bytes - if (headers["content-range"]?.[0]?.value) { - const match = headers["content-range"][0].value.match(/bytes\s+(\d+)-(\d+)/); - if (match) { - bytesServed = parseInt(match[2], 10) - parseInt(match[1], 10) + 1; - } - } else if (headers["content-length"]?.[0]?.value) { - // Full response — use Content-Length - bytesServed = parseInt(headers["content-length"][0].value, 10); - } - - if (bytesServed > 0) { - // Compute current month key - const now = new Date(); - const month = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; - const pk = `${BUCKET_NAME}#${month}`; - - // Atomically increment the byte counter - await ddb.send( - new UpdateItemCommand({ - TableName: "cloudfront_bandwidth", - Key: { pk: { S: pk } }, - UpdateExpression: "ADD bytes :b", - ExpressionAttributeValues: { - ":b": { N: String(bytesServed) }, - }, - }) - ); - } - } catch (err) { - // Fail-open: don't block the response if accounting fails - console.error("Accounting error:", err); - } - - return response; -} diff --git a/aws/lambda-edge/viewer-response/package.json b/aws/lambda-edge/viewer-response/package.json deleted file mode 100644 index 5e233e6..0000000 --- a/aws/lambda-edge/viewer-response/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "cf-bandwidth-accounting", - "version": "1.0.0", - "type": "module", - "dependencies": { - "@aws-sdk/client-dynamodb": "^3.0.0" - } -} diff --git a/docs/aws-resources.md b/docs/aws-resources.md deleted file mode 100644 index 3819cb7..0000000 --- a/docs/aws-resources.md +++ /dev/null @@ -1,87 +0,0 @@ -# AWS Resources - -All resources are in `us-east-1` and managed via Terraform in `aws/lambda-edge/terraform/`. - -## Resource Overview - -``` - sciunit copy (upload) - │ - v - ┌──────────────────┐ - │ S3 Bucket │ - │ sciunit2-talha │ - └────────┬─────────┘ - │ OAC - v - ┌──────────────────┐ - │ CloudFront │──── viewer-request ──── Gatekeeper Lambda - │ E2HWZQGHIVF7CP │ (checks bandwidth) - │ │──── viewer-response ─── Accounting Lambda - └────────┬─────────┘ (tracks bytes) - │ │ - │ v - v ┌──────────────────┐ - sciunit open │ DynamoDB │ - (download) │cloudfront_bandwidth│ - └──────────────────┘ -``` - -## Resources - -### S3 Bucket — `sciunit2-talha` -- Stores sciunit project archives uploaded via `sciunit copy` -- All public access blocked; only accessible through CloudFront via OAC -- Objects stored under `projects//` -- Credentials stored under `persistent/` (not subject to lifecycle rules) - -### CloudFront Distribution — `E2HWZQGHIVF7CP` -- Domain: `d3okuktvxs1y4w.cloudfront.net` -- Origin Access Control (OAC): `EZDWNYQO6UZRC` — authenticates CloudFront requests to S3 -- Serves downloads for `sciunit open ` -- First 1 TB/month of bandwidth is free (vs ~$0.09/GB from S3 direct) -- Created outside Terraform; Lambda associations attached via `null_resource` - -### Lambda@Edge — Gatekeeper (`cf-bandwidth-gatekeeper`) -- **Event**: `viewer-request` — runs before CloudFront fetches the object -- **Purpose**: Checks monthly bandwidth usage in DynamoDB; returns HTTP 429 if the 1 TB limit is exceeded -- **Fail-open**: If DynamoDB is unreachable, the request is allowed through -- **Runtime**: Node.js 20.x -- Terraform resource: `aws_lambda_function.gatekeeper` - -### Lambda@Edge — Accounting (`cf-bandwidth-accounting`) -- **Event**: `viewer-response` — runs after CloudFront sends the response -- **Purpose**: Reads `Content-Length` (or `Content-Range`) from the response and atomically increments the monthly byte counter in DynamoDB -- **Runtime**: Node.js 20.x -- Terraform resource: `aws_lambda_function.accounting` - -### DynamoDB Table — `cloudfront_bandwidth` -- **Billing**: PAY_PER_REQUEST (no provisioned capacity) -- **Schema**: Partition key `pk` (String), attribute `bytes` (Number) -- **Key format**: `sciunit2-talha#YYYY-MM` (e.g., `sciunit2-talha#2026-02`) -- Counter resets naturally each month since the key includes the year-month -- Atomic updates via `UpdateItem ADD` ensure correctness under concurrent requests - -### IAM Role — `cloudfront-bandwidth-limiter` -- Trusted by `lambda.amazonaws.com` and `edgelambda.amazonaws.com` -- Permissions: `dynamodb:GetItem`, `dynamodb:UpdateItem` on the bandwidth table, plus CloudWatch Logs - -## Bandwidth Limiting - -When cumulative downloads for the current month reach 1 TB: -1. Gatekeeper Lambda reads the counter from DynamoDB -2. Returns HTTP 429 with body: "Monthly download bandwidth limit (1 TB) exceeded" -3. The sciunit CLI catches this and displays: "Monthly download bandwidth limit exceeded. Please try again next month or contact the sciunit maintainers." - -## Deployment - -```bash -# Install Lambda dependencies (required before first deploy) -cd aws/lambda-edge/viewer-request && npm install -cd aws/lambda-edge/viewer-response && npm install - -# Deploy/update all resources -cd aws/lambda-edge/terraform -terraform init -terraform apply -``` diff --git a/docs/s3-integration.md b/docs/s3-integration.md deleted file mode 100644 index dd079ed..0000000 --- a/docs/s3-integration.md +++ /dev/null @@ -1,72 +0,0 @@ -# S3 Integration for Sciunit Copy - -## Overview - -Sciunit uses AWS S3 for storing and sharing sciunit packages via the `sciunit copy` command. This enables users to easily share their reproducible research containers across machines and with collaborators. - -## Architecture - -``` -┌─────────────┐ upload ┌─────────────┐ -│ sciunit │ ───────────────>│ AWS S3 │ -│ copy │ │ Bucket │ -└─────────────┘ └──────┬──────┘ - │ - │ origin - v -┌─────────────┐ download ┌─────────────┐ -│ sciunit │ <───────────────│ CloudFront │ -│ open │ │ CDN │ -└─────────────┘ └─────────────┘ -``` - -## How It Works - -### Upload (`sciunit copy`) -1. Creates a ZIP archive of the current sciunit -2. Fetches AWS credentials from a public endpoint -3. Uploads the archive to S3 bucket -4. Returns a CloudFront URL for downloading - -### Download (`sciunit open `) -1. Downloads the sciunit archive via CloudFront CDN -2. Extracts and opens the sciunit locally - -## Why CloudFront? - -We use CloudFront as a CDN layer on top of S3 for downloads because: - -| Feature | S3 Direct | CloudFront | -|---------|-----------|------------| -| First 1TB/month bandwidth | Paid (~$0.09/GB) | **Free** | -| Global edge locations | No | Yes | -| Caching | No | Yes | -| HTTPS | Yes | Yes | - -**Cost savings**: CloudFront offers 1TB of free data transfer per month, making it ideal for distributing sciunit packages without incurring bandwidth costs. - -## Configuration - -- **S3 Bucket**: `sciunit2-talha` -- **CloudFront Domain**: `https://d3okuktvxs1y4w.cloudfront.net` -- **Credentials Endpoint**: Fetched dynamically to support rotation - -## Usage - -```bash -# Upload a sciunit to S3 and get a shareable URL -sciunit copy -# Output: https://d3okuktvxs1y4w.cloudfront.net/projects/2024-01-07-12:00:00/myproject.zip - -# Open a sciunit from the URL -sciunit open https://d3okuktvxs1y4w.cloudfront.net/projects/2024-01-07-12:00:00/myproject.zip - -# Local copy only (no S3 upload) -sciunit copy -n -``` - -## Security - -- AWS credentials are stored securely and fetched at runtime -- Credentials have limited permissions (S3 read/write only) -- Credentials are rotated periodically From 6243216d1b07dc9781041987c5c1c8b387462423 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Wed, 25 Mar 2026 18:20:46 -0500 Subject: [PATCH 14/16] Patch: update bucket name --- sciunit2/s3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sciunit2/s3.py b/sciunit2/s3.py index 7bce856..5a897a3 100644 --- a/sciunit2/s3.py +++ b/sciunit2/s3.py @@ -11,7 +11,7 @@ CF_DOMAIN = "https://d3okuktvxs1y4w.cloudfront.net" -def live(fn, bucket="sciunit2-talha"): +def live(fn, bucket="sciunit-copy"): """ Uploads a file to S3 and returns a CF download URL. Fetches AWS credentials dynamically from endpoint to handle rotation. From 157fba041035b2d8391180c3498c3ea8f0000744 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Wed, 25 Mar 2026 20:15:29 -0500 Subject: [PATCH 15/16] Patch: remove legacy pkg_resources --- requirements.txt | 1 - sciunit2/cli.py | 4 ++-- sciunit2/command/exec_/__init__.py | 4 ++-- sciunit2/command/post_install/__init__.py | 6 +++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 21c2402..389cdc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -setuptools tzlocal utcdatetime zipfile2 diff --git a/sciunit2/cli.py b/sciunit2/cli.py index 3bca3d3..1e14df3 100644 --- a/sciunit2/cli.py +++ b/sciunit2/cli.py @@ -22,7 +22,7 @@ from getopt import getopt, GetoptError from io import StringIO import textwrap -import pkg_resources +from importlib.metadata import version as pkg_version import os import platform @@ -92,7 +92,7 @@ def _main(args): subcommand_usage(sys.stdout, [cls() for cls in __cmds__]) return elif op == '--version': - print(pkg_resources.require("sciunit2")[0]) + print(pkg_version("sciunit2")) return elif op == '--root': # pragma: no cover import sciunit2.workspace diff --git a/sciunit2/command/exec_/__init__.py b/sciunit2/command/exec_/__init__.py index 62cd41d..eb5e1e7 100644 --- a/sciunit2/command/exec_/__init__.py +++ b/sciunit2/command/exec_/__init__.py @@ -7,7 +7,7 @@ import sciunit2.workspace from getopt import getopt -from pkg_resources import resource_filename +from importlib.resources import files class ExecCommand(CommitMixin, AbstractCommand): @@ -28,7 +28,7 @@ def run(self, args): with emgr.exclusive(): rev = emgr.add(args) if optlist: - standin_fn = resource_filename(__name__, 'sciunit') + standin_fn = str(files(__name__).joinpath('sciunit')) sciunit2.core.shell(env=path_injection_for(standin_fn)) else: sciunit2.core.capture(args) diff --git a/sciunit2/command/post_install/__init__.py b/sciunit2/command/post_install/__init__.py index 68daee7..1564ecd 100644 --- a/sciunit2/command/post_install/__init__.py +++ b/sciunit2/command/post_install/__init__.py @@ -5,7 +5,7 @@ import os import pwd import tempfile -import pkg_resources +from importlib.resources import files from shutil import copyfileobj from contextlib import closing from humanfriendly import format_path @@ -43,7 +43,7 @@ def patch_shell_script(from_, to, rcfile): to_ = os.path.expanduser(to) with tempfile.NamedTemporaryFile(dir=os.path.dirname(to_), prefix='pip-tmp') as tmp: - script = pkg_resources.resource_stream(__name__, from_) + script = files(__name__).joinpath(from_).open("rb") try: with closing(script) as g, closing(open(to_, 'a+')) as f: f.seek(0) @@ -68,4 +68,4 @@ def patch_shell_script(from_, to, rcfile): print('Unable to patch %s. Please copy\n\n %s\n\n' 'to a subdirectory of your home directory ' 'and "source" it in %s.' % - (to, format_path(script.name), rcfile)) + (to, format_path(from_), rcfile)) From 37eda543b334997446120e5a1c61d312285bedb8 Mon Sep 17 00:00:00 2001 From: Talha Azaz Date: Wed, 25 Mar 2026 20:21:57 -0500 Subject: [PATCH 16/16] Patch: fix post install error --- sciunit2/command/post_install/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sciunit2/command/post_install/__init__.py b/sciunit2/command/post_install/__init__.py index 1564ecd..8fcbf29 100644 --- a/sciunit2/command/post_install/__init__.py +++ b/sciunit2/command/post_install/__init__.py @@ -43,7 +43,8 @@ def patch_shell_script(from_, to, rcfile): to_ = os.path.expanduser(to) with tempfile.NamedTemporaryFile(dir=os.path.dirname(to_), prefix='pip-tmp') as tmp: - script = files(__name__).joinpath(from_).open("rb") + script_path = files(__name__).joinpath(from_) + script = script_path.open("rb") try: with closing(script) as g, closing(open(to_, 'a+')) as f: f.seek(0) @@ -68,4 +69,4 @@ def patch_shell_script(from_, to, rcfile): print('Unable to patch %s. Please copy\n\n %s\n\n' 'to a subdirectory of your home directory ' 'and "source" it in %s.' % - (to, format_path(from_), rcfile)) + (to, format_path(str(script_path)), rcfile))