# Technical documentation — ear_lambdas

Evidence date: 2026-10-16.

Source: `git@bitbucket.org:overdrive/ear_lambdas.git`, `master` at `1539c1d` (2023-09-27, Ettiene). Single branch `master`. Database password values are not copied into this document.

## Architecture

This repository is a flat export of twelve Lambda functions. Each function is one SAM template and one zip. There is no shared library directory, no `package.json`, and no `serverless.yml`.

```text
*.yaml                         AWS SAM, one function each, CodeUri: .
*-<uuid>.zip                   the deployed code for that function
bitbucket-pipelines.yml        SonarQube on master
sonar-project.properties       project key only
.gitignore                     Bitbucket default template
```

Commit `cc029a4` ("exports of all cape town lambdas") added eleven pairs. Commit `1539c1d` ("added on put function") added `VideoDurationOnPut`.

Region is not a template parameter. Evidence that the export is `af-south-1`: the commit message, the RDS hostname, the layer ARN, and `AWS.config.update({ region: 'af-south-1' })` inside `sort_files.js` and `transcode.js`. The upload function's S3 client uses `eu-west-1` for `the-ear-prod-video-upload`.

Account on every role and on the layer: `055752874982`.

Three zips (`create.js`, `sort_files.js`, `transcode.js`) are Webpack bundles. Source-map `sources` entries point at `C:/wamp64/www/theear_transcode/region_south_africa/`. `aws-sdk` is external. The application module is the last concatenated module in each file.

## Repository layout

Root only. No `src/`, `test/`, or `README`.

| Pair | Zip entry that the handler names | Zip size (bytes, git) |
|---|---|---|
| `phpCronEveryDay.yaml` | `index.js` | 675 |
| `phpCronEveryDay2am.yaml` | `index.js` | 428 |
| `phpCronEveryDay6am.yaml` | `index.js` | 443 |
| `phpCronEveryDay8am.yaml` | `index.js` | 419 |
| `phpCronTrigger.yaml` | `index.js` | 457 |
| `the-ear-resources-to-upload.yaml` | `index.js` | 488 |
| `theear-resources-production-create.yaml` | `create.js` (plus `create.js.map`) | 83478 |
| `theear-resources-production-sort_files.yaml` | `sort_files.js` (plus map) | 436439 |
| `theear-resources-production-transcode.yaml` | `transcode.js` (plus map) | 436704 |
| `theear-resources-production-custom-resource-existing-s3.yaml` | `s3/handler.js` and `s3/lib/*`, `utils.js` | 10540 |
| `videoDuration.yaml` | `lambda_function.py` | 885 |
| `VideoDurationOnPut.yaml` | `lambda_function.py`, and a copy of the yaml | 2111 |

`CodeUri: .` means "the directory of the template". The handler files are inside the zip, not beside the yaml. The zip file name is not referenced by the yaml. Provenance: **Inferred** that these pairs are Lambda console downloads, not a SAM project laid out for `sam build`.

`sonarqube_scan.bat` was listed when the onboarding task was created. It is not in this revision.

## Runtimes, memory, timeout, events

Shared on every function: `x86_64`, ephemeral storage 512, event max age 21600 seconds, max retry attempts 2, SnapStart off, runtime management `Auto`, package type Zip.

| Function | Runtime | MB | Timeout (s) | Event in the template | VPC |
|---|---|---|---|---|---|
| `phpCronEveryDay` | nodejs12.x | 128 | 3 | `cron(0 6 * * ? *)` | no |
| `phpCronEveryDay2am` | nodejs14.x | 128 | 3 | none | no |
| `phpCronEveryDay6am` | nodejs14.x | 128 | 3 | none | no |
| `phpCronEveryDay8am` | nodejs16.x | 128 | 3 | none | no |
| `phpCronTrigger` | nodejs12.x | 128 | 3 | `rate(1 hour)` | no |
| `theearresourcestoupload` | nodejs12.x | 128 | 3 | none | yes, resources VPC |
| `theearresourcesproductioncreate` | nodejs12.x | 512 | 6 | API `POST /create` | yes, resources VPC |
| `theearresourcesproductionsortfiles` | nodejs12.x | 512 | 6 | API `GET /sort_files` | yes, resources VPC |
| `theearresourcesproductiontranscode` | nodejs12.x | 512 | 6 | S3 `ObjectCreated:*` on `Bucket1` | yes, resources VPC |
| `theearresourcesproductioncustomresourceexistings3` | nodejs12.x | 1024 | 180 | none | yes, resources VPC |
| `videoDuration` | python3.9 | 128 | 600 | `cron(0 18 * * ? *)` | yes, duration VPC |
| `VideoDurationOnPut` | python3.9 | 128 | 3 | S3 `ObjectCreated:*` on `Bucket1` | yes, duration VPC |

Resources VPC, from the templates:

- Security group `sg-04a9c8db07f607e2d`
- Subnets `subnet-0c90a693b84fa59b9`, `subnet-073bdac49e88c7daa`

Duration VPC:

- Security group `sg-0b1438e5765fd1440`
- Subnets `subnet-0d3f1cc6598f5d416`, `subnet-02d6fd15bd0519011`

The same subnet and security-group ids are also copied into environment variables `SUBNETIDS1`, `SUBNETIDS2`, and `SECURITYGROUPIDS` on create, sort_files, and transcode. The handlers do not read those variables. The `VpcConfig` block is what attaches the function.

Tags:

- `lambda-console:blueprint: hello-world` on `phpCronEveryDay`, `phpCronTrigger`, and `theearresourcestoupload`.
- `STAGE: production` on create, sort_files, transcode, and the custom resource.
- No tags on the 2am, 6am, and 8am templates or on either Python template.

## Handler map

| Template logical id | Handler | Role |
|---|---|---|
| `phpCronEveryDay` | `index.handler` | `service-role/phpCronEveryDay-role-o7rgabdc` |
| `phpCronEveryDay2am` | `index.handler` | `service-role/phpCronEveryDay2am-role-zgmtnn1u` |
| `phpCronEveryDay6am` | `index.handler` | `service-role/phpCronEveryDay6am-role-ac2w27q6` |
| `phpCronEveryDay8am` | `index.handler` | `service-role/phpCronEveryDay-role-o7rgabdc` (same as the daily function) |
| `phpCronTrigger` | `index.handler` | `service-role/phpCronTrigger-role-u96vukc8` |
| `theearresourcestoupload` | `index.handler` | `theear-resources-production-af-south-1-lambdaRole` |
| `theearresourcesproductioncreate` | `create.main` | same resources role |
| `theearresourcesproductionsortfiles` | `sort_files.main` | same resources role |
| `theearresourcesproductiontranscode` | `transcode.main` | same resources role |
| `theearresourcesproductioncustomresourceexistings3` | `s3/handler.handler` | same resources role |
| `videoDuration` | `lambda_function.lambda_handler` | `service-role/videoDuration-role-ezd8bp5t` |
| `VideoDurationOnPut` | `lambda_function.lambda_handler` | same duration role |

Role policies are not in the repository.

## Data stores

### MySQL

Host (from env, not hardcoded in the Python or Node handlers): `the-ear-prod-db-a.cluster-coaufliysybm.af-south-1.rds.amazonaws.com`.

Database: `the_ear_prod_db_a`.

User name in the templates: `admin`.

Node client: the bundle calls a `mysql` config factory with `host`, `database`, `user`, `password` from `databaseHost`, `databaseName`, `databaseUser`, `databasePass`. That matches `serverless-mysql` usage described for `theear_transcode`. Charset is not set in the Node call.

Python client: `pymysql.connect`, `autocommit=True`, `charset=utf8mb4`, `DictCursor`.

Columns the SQL text uses on `resources`:

| Column | Who writes it | Notes |
|---|---|---|
| `name` | catalog insert, backfill insert | file stem |
| `url` | catalog insert, backfill insert | `s3://` URL |
| `type` | catalog insert, backfill insert | suffix map. Backfill stores `.m3u8` as `mxf` |
| `duration` | insert as `"0"`; Python updates it | schedule uses a placeholder; on-put concatenates |
| `created` | inserts | UTC timestamp string |
| `status` | neither Node handler | Python filters `status = 'active'`. Who sets `active` is **Unknown** |
| `id` | neither insert lists it | Python updates `WHERE id = ...`. **Inferred** auto-increment or similar. DDL is not in git |

There is no migration folder.

### DynamoDB

`create.js` uses `AWS.DynamoDB.DocumentClient` with no region override. `TableName` is `process.env.tableName`. The create, sort_files, and transcode templates set `tableName` to `loging`. Only `create.js` reads it. Item attributes: `userId`, `noteId`, `content`, `attachment`, `createdAt`. No `KeySchema` is in the repo.

### S3 buckets named in code

| Name | Where | Region the client sets |
|---|---|---|
| `the-ear-prod-resources` | catalog URL prefix; upload copy source | catalog does not call S3. Upload client is `eu-west-1` but the source bucket is only a copy-source string |
| `the-ear-prod-video-upload` | upload destination. Commented out inside `transcode.js` | `eu-west-1` in the upload function |
| `the-ear-staging-resources` | `sort_files.js` `listObjects` and URL prefix | `af-south-1` |

`Bucket1` in `theear-resources-production-transcode.yaml` and `VideoDurationOnPut.yaml` is `Type: AWS::S3::Bucket` with no `BucketName`, no notification, and no `DeletionPolicy`. Deploying the template as written would create a new bucket and attach the event to that new bucket. Provenance: **Inferred** console-export placeholder. The production bucket name for those two events is **Unknown / Requires Confirmation**.

### CloudFront

Both Python files request `https://d1ogvodmq23438.cloudfront.net` + the path from the stored or event URL. Distribution configuration is not in git.

## Environment variables

On `theear-resources-production-create`, `sort_files`, and `transcode` (same keys and the same non-secret values):

| Key | Value in the template | Read by the handler |
|---|---|---|
| `databaseHost`, `DBHOST` | RDS hostname above | Node catalog and backfill read `databaseHost` only |
| `databaseName`, `DBNAME` | `the_ear_prod_db_a` | `databaseName` only |
| `databaseUser`, `DBUSERNAME` | `admin` | `databaseUser` only |
| `databasePass`, `DBPASSWORD` | a password (omitted here) | `databasePass` only |
| `SUBNETIDS1`, `SUBNETIDS2`, `SECURITYGROUPIDS` | the resources VPC ids | not read |
| `tableName` | `loging` | `create.js` only |

On `videoDuration` and `VideoDurationOnPut`:

| Key | Value |
|---|---|
| `db_host` | same RDS hostname |
| `db_database` | `the_ear_prod_db_a` |
| `db_user` | `admin` |
| `db_password` | a password (omitted here) |

The PHP-cron functions and the upload function set no environment variables. The custom resource sets none.

### Security note

`databasePass`, `DBPASSWORD`, and `db_password` are plaintext properties in:

- `theear-resources-production-create.yaml`
- `theear-resources-production-sort_files.yaml`
- `theear-resources-production-transcode.yaml`
- `videoDuration.yaml`
- `VideoDurationOnPut.yaml`

The value is the same key across those files. It has been in git since `cc029a4` (the Python and Node templates) and `1539c1d` (the on-put template). This document does not repeat it. **Requires Confirmation** and a human action: rotate the database user, stop publishing the secret in templates, and remove it from history if that is the team's practice. IAM role policies that might also allow use of the secret are not in this repo.

The cron functions do not embed a bearer token. They call public HTTPS URLs with no headers.

`VideoDurationOnPut` and the Node insert/select strings concatenate event or file-name data into SQL. The scheduled `videoDuration` update uses `%s` placeholders.

## Custom resource package

Zip root `README.md` is the sentence: "This directory contains the Lambda functions for the Serverless Custom CloudFormation Resources."

Configured handler `s3/handler.js`:

- `Create` calls `addPermission` then `updateConfiguration`.
- `Update` calls `updateConfiguration` only.
- `Delete` calls `removePermission` then `removeConfiguration`.
- Anything else throws `Unhandled RequestType`.

`s3/lib/bucket.js` reads the bucket notification configuration, drops Lambda configurations whose id starts with the function name, appends one configuration per `BucketConfigs` entry (event, optional key filter, id = function name plus an MD5 of the config), and writes the configuration back.

`utils.js` wraps the call with a CloudFormation custom-resource response. The log label is `CustomResourceExistingS3`.

`cognitoUserPool/`, `eventBridge/`, and `apiGatewayCloudWatchRole/` are in the zip and are not the handler string in the yaml.

## Python layer

Both Python templates:

```text
arn:aws:lambda:af-south-1:055752874982:layer:packages:2
```

The zip contains only `lambda_function.py` (and, for OnPut, a yaml copy). Imports are `m3u8`, `pymysql`, `boto3`, and the standard library. `boto3` is provided by the Lambda Python runtime. `m3u8` and `pymysql` are not in the zip. Provenance: **Inferred** that layer version 2 supplies them. Layer contents were not downloaded. `boto3` is imported in `VideoDurationOnPut` and the `s3` client is created. The handler never calls it. `videoDuration` does not import `boto3`.

`VideoDurationOnPut` imports `io` and `timedelta` and does not use them.

## Auth

No Cognito user pool, no JWT check, and no API authorizer property are in these templates. `create.js` reads `event.requestContext.identity.cognitoIdentityId` and the comment calls it a Cognito Identity Pool id. If the field is missing, the handler throws. `POST /create` in the sibling `theear_transcode` source uses `authorizer: aws_iam`. This export's API event is only `Path: /create` and `Method: POST`.

`GET /sort_files` is the same: path and method only.

PHP cron calls are unauthenticated HTTPS from the Lambda's point of view.

## Background work

There is no queue, no worker process, and no Step Functions definition.

Scheduled invocations that are in the templates:

| Function | Schedule |
|---|---|
| `phpCronEveryDay` | `cron(0 6 * * ? *)` |
| `phpCronTrigger` | `rate(1 hour)` |
| `videoDuration` | `cron(0 18 * * ? *)` |

The cron handlers fire every `https.get` inside a `for` loop and finish the Lambda from the first callback. They do not use `async`/`await` or `Promise.all`. With a 3 second timeout, later URLs can still be in flight when the runtime freezes the process. Provenance: **Inferred** from the control flow. It was not executed here.

Lambda asynchronous retry (2 attempts, 6 hour max age) applies to all twelve functions via `EventInvokeConfig`. There is no destination or DLQ in the templates.

## Deployment and CI

`bitbucket-pipelines.yml`:

- Image `maven:3.3.9`.
- Clone depth `full`.
- Branch pattern `{master}` only.
- One step, "SonarQube analysis", pipe `sonarsource/sonarqube-scan:2.0.1`.
- `SONAR_HOST_URL` and `SONAR_TOKEN` from repository or workspace variables. The token is not in git.
- Cache `~/.sonar`.

`sonar-project.properties` is one line:

```text
sonar.projectKey=overdrive_ear_lambdas_AYqOelPiuNwRc2P2S9Q4
```

No `sonar.sources`, no exclusions, no coverage. No build, test, or deploy step. Nothing in the repo runs `sam deploy` or `serverless deploy`.

`.gitignore` is the Bitbucket example (node_modules, jars, IDE files, large video extensions). It does not ignore `*.zip` or `*.yaml`. The function zips are committed.

## Development and testing

No test directory, no Jest config, no `pytest` file, no mock event. The `# TODO implement` comment in `videoDuration` sits above code that queries MySQL and returns 200.

Local reproduction would require unpacking a zip and invoking the handler with a fabricated event. That procedure is not documented in the repo and was not run for this write-up. Behaviour above is from reading the source.

Node 12, 14, and 16 appear only as `Runtime:` strings. Whether those runtimes still accept invocations was not checked against AWS documentation on the evidence date.

## Monitoring

No CloudWatch alarm, no log subscription, and no X-Ray setting appear in the templates. Handlers `console.log` or `print` the URL, the SQL text, row counts, and errors. The scheduled duration function logs less than the on-put function. The on-put function prints the SQL, including the duration value it is about to write.

## Relationship to `theear_transcode`

| This export | Source-map or behavioural link |
|---|---|
| `create.js` | `region_south_africa/create.js` |
| `sort_files.js` | `region_south_africa/sort_files.js` |
| `transcode.js` | `region_south_africa/transcode.js` |
| custom-resource zip | Serverless Framework resource, not an application file in the sibling PRD |
| PHP cron, both Python duration functions, resources-to-upload | Not functions in the `theear_transcode` Silicon Ark set |

The sibling documents the Serverless project at `fda49df` (2022-05-04). This export is dated 2023-09-14 and 2023-09-27. Timeouts and the API authorizer differ. See the discrepancy table in `prd.md`.

## Unknown / requires confirmation

- Live enablement and drift since 2023-09-27.
- Schedules for the 2am, 6am, and 8am functions.
- Bucket names behind both `Bucket1` resources and behind `theearresourcestoupload`.
- API Gateway id, stage, and authorizer for `/create` and `/sort_files`.
- DynamoDB region and whether table `loging` exists.
- IAM policy contents.
- Layer `packages:2` contents.
- Contents and protection of CloudFront `d1ogvodmq23438`.
- Whether the database password in git has already been rotated in AWS while the files stayed as they are.
