mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
dev deploys have failed since PR #1231 (2026-06-15) with InvalidParameterValueException: Unzipped size must be smaller than 262144000 bytes. The lambda zip is built from the whole repo root (deployment/terraform/modules/lambda_with_api_gateway) and only excluded tests/deployment/pycache — so .git (206MB packed), the per-schema corpus.jsonl fixtures under backend/epc_api/json_samples/ (~50MB), and the sap10_calculator PDF specs (~13MB) were all being zipped up even though none of them are read at runtime. Also add scripts/check_lambda_zip_size.py, which mirrors the terraform module's pip install + zip-with-excludes behaviour to report the projected unzipped size without mutating the repo, and wire it into deploy_terraform.yml as a fast_api_lambda_zip_size_check job that gates the real deploy so this fails fast with a clear message instead of surfacing as an opaque Terraform/AWS error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
149 lines
4.6 KiB
Python
Executable file
149 lines
4.6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Estimate the unzipped size of a Terraform-packaged Lambda before deploying.
|
|
|
|
Mirrors what `deployment/terraform/modules/lambda_with_api_gateway/main.tf` does:
|
|
`pip install -r <requirements> -t <source_dir> ...` followed by zipping
|
|
`source_dir` minus `zip_excludes`. AWS Lambda's hard limit is 262144000 bytes
|
|
(250 MiB) unzipped. We install the dependencies into a scratch directory
|
|
instead of the real source tree so this can run locally or in CI without
|
|
mutating the repo.
|
|
|
|
Usage:
|
|
python3 scripts/check_lambda_zip_size.py \
|
|
--source-dir . \
|
|
--requirements backend/app/requirements/requirements.txt \
|
|
--exclude "**/tests/**" --exclude "**/.git/**" ...
|
|
|
|
Exits non-zero if the projected unzipped size exceeds --limit (or exceeds
|
|
--warn-pct of the limit, when --strict is not passed the warning is just
|
|
printed).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import fnmatch
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
LAMBDA_UNZIPPED_LIMIT_BYTES = 262_144_000
|
|
|
|
|
|
def doublestar_match(pattern_parts: list[str], path_parts: list[str]) -> bool:
|
|
if not pattern_parts:
|
|
return not path_parts
|
|
head, rest = pattern_parts[0], pattern_parts[1:]
|
|
if head == "**":
|
|
if doublestar_match(rest, path_parts):
|
|
return True
|
|
if path_parts and doublestar_match(pattern_parts, path_parts[1:]):
|
|
return True
|
|
return False
|
|
if not path_parts:
|
|
return False
|
|
if fnmatch.fnmatchcase(path_parts[0], head):
|
|
return doublestar_match(rest, path_parts[1:])
|
|
return False
|
|
|
|
|
|
def is_excluded(rel_path: Path, excludes: list[str]) -> bool:
|
|
parts = rel_path.as_posix().split("/")
|
|
return any(doublestar_match(pattern.split("/"), parts) for pattern in excludes)
|
|
|
|
|
|
def sum_dir_size(root: Path, excludes: list[str] | None = None) -> int:
|
|
total = 0
|
|
for path in root.rglob("*"):
|
|
if path.is_dir():
|
|
continue
|
|
rel = path.relative_to(root)
|
|
if excludes and is_excluded(rel, excludes):
|
|
continue
|
|
total += path.stat().st_size
|
|
return total
|
|
|
|
|
|
def install_dependencies(requirements: Path, target: Path) -> int:
|
|
subprocess.run(
|
|
[
|
|
"pip",
|
|
"install",
|
|
"-r",
|
|
str(requirements),
|
|
"-t",
|
|
str(target),
|
|
"--platform",
|
|
"manylinux2014_x86_64",
|
|
"--implementation",
|
|
"cp",
|
|
"--python-version",
|
|
"3.11",
|
|
"--only-binary=:all:",
|
|
],
|
|
check=True,
|
|
)
|
|
return sum_dir_size(target)
|
|
|
|
|
|
def human(n: int) -> str:
|
|
return f"{n:,} bytes ({n / 1024 / 1024:.1f} MiB)"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--source-dir", type=Path, default=Path("."))
|
|
parser.add_argument("--requirements", type=Path, default=None)
|
|
parser.add_argument("--exclude", action="append", default=[], dest="excludes")
|
|
parser.add_argument("--limit", type=int, default=LAMBDA_UNZIPPED_LIMIT_BYTES)
|
|
parser.add_argument(
|
|
"--warn-pct",
|
|
type=float,
|
|
default=85.0,
|
|
help="Print a warning once projected size crosses this %% of the limit.",
|
|
)
|
|
parser.add_argument(
|
|
"--strict",
|
|
action="store_true",
|
|
help="Exit non-zero on the warning threshold too, not just the hard limit.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
source_size = sum_dir_size(args.source_dir, args.excludes)
|
|
|
|
deps_size = 0
|
|
if args.requirements is not None:
|
|
with tempfile.TemporaryDirectory(prefix="lambda-deps-") as tmp:
|
|
deps_size = install_dependencies(args.requirements, Path(tmp))
|
|
|
|
total = source_size + deps_size
|
|
pct = total / args.limit * 100
|
|
|
|
print(f"source files (post-exclude): {human(source_size)}")
|
|
if args.requirements is not None:
|
|
print(f"pip dependencies: {human(deps_size)}")
|
|
print(f"projected unzipped total: {human(total)} [{pct:.1f}% of limit]")
|
|
print(f"AWS limit: {human(args.limit)}")
|
|
|
|
if total > args.limit:
|
|
print(
|
|
f"\nFAIL: projected unzipped size exceeds the Lambda limit by "
|
|
f"{human(total - args.limit)}.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
if pct >= args.warn_pct:
|
|
msg = (
|
|
f"\nWARNING: projected unzipped size is at {pct:.1f}% of the "
|
|
f"{human(args.limit)} limit."
|
|
)
|
|
print(msg, file=sys.stderr)
|
|
if args.strict:
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|