mirror of
https://github.com/Sea-Haven-Industries/front-integrations.git
synced 2026-09-30 04:53:11 +00:00
feat(terraform): migrate front-integrations to HCP Terraform (PLAT-72) (#41)
* feat(terraform): migrate front-integrations to HCP Terraform Freeze SAM CD and add Terraform for seahaven-prod (Lambdas, DynamoDB, EventBridge, alarms) so HCP becomes the sole deploy path. Include prod secret ARNs in the tfvars example for workspace wiring. * fix(terraform): reject symlink sources in Lambda package build
This commit is contained in:
parent
b30ed47321
commit
7bbdbabe3a
21 changed files with 832 additions and 112 deletions
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
|
|
@ -33,3 +33,14 @@ updates:
|
||||||
update-types:
|
update-types:
|
||||||
- "minor"
|
- "minor"
|
||||||
- "patch"
|
- "patch"
|
||||||
|
- package-ecosystem: "terraform"
|
||||||
|
directory: "/terraform"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore(deps)"
|
||||||
|
groups:
|
||||||
|
minor-and-patch:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
|
|
||||||
35
.github/workflows/ci-terraform.yaml
vendored
Normal file
35
.github/workflows/ci-terraform.yaml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
name: Terraform CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "terraform/**"
|
||||||
|
- ".github/workflows/ci-terraform.yaml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
terraform:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: terraform
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||||
|
with:
|
||||||
|
terraform_version: "1.9.8"
|
||||||
|
|
||||||
|
- name: Package Lambda zips
|
||||||
|
run: ./build_packages.sh
|
||||||
|
|
||||||
|
- name: Terraform fmt
|
||||||
|
run: terraform fmt -check -recursive
|
||||||
|
|
||||||
|
- name: Terraform init
|
||||||
|
run: terraform init -backend=false
|
||||||
|
|
||||||
|
- name: Terraform validate
|
||||||
|
run: terraform validate
|
||||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
|
|
@ -11,3 +11,5 @@ jobs:
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@3f746774229d41770727e2e4fd63ed5f5555a8b3 # v1.0.3
|
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@3f746774229d41770727e2e4fd63ed5f5555a8b3 # v1.0.3
|
||||||
with:
|
with:
|
||||||
source-dirs: "src"
|
source-dirs: "src"
|
||||||
|
# Deploy path is HCP Terraform; reusable name still covers Python lint/tests.
|
||||||
|
run-sam-validate: false
|
||||||
|
|
|
||||||
22
.github/workflows/deploy.yaml
vendored
22
.github/workflows/deploy.yaml
vendored
|
|
@ -1,22 +0,0 @@
|
||||||
name: Deploy
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: deploy
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@3f746774229d41770727e2e4fd63ed5f5555a8b3 # v1.0.3
|
|
||||||
with:
|
|
||||||
stack-name: front-integrations
|
|
||||||
cfn-role-arn: arn:aws:iam::328440206208:role/github-cfn-execution-role
|
|
||||||
secrets:
|
|
||||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
||||||
parameter-overrides: ${{ secrets.SAM_PARAMETER_OVERRIDES }}
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -3,3 +3,7 @@ __pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.env
|
.env
|
||||||
samconfig.toml
|
samconfig.toml
|
||||||
|
terraform/.terraform/
|
||||||
|
terraform/build/
|
||||||
|
terraform/*.tfvars
|
||||||
|
!terraform/terraform.tfvars.example
|
||||||
|
|
|
||||||
122
README.md
122
README.md
|
|
@ -1,11 +1,11 @@
|
||||||
# front-integrations
|
# front-integrations
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
Front platform integrations for Sea Haven Industries. Two scheduled Lambdas:
|
Front platform integrations for Sea Haven Industries. Two scheduled Lambdas deployed to **seahaven-prod** via HCP Terraform workspace `front-integrations-prod` (PLAT-72).
|
||||||
|
|
||||||
1. **SLA Monitor** — checks Front conversations for SLA breaches and sends tiered Slack alerts
|
1. **SLA Monitor** — checks Front conversations for SLA breaches and sends tiered Slack alerts
|
||||||
2. **User Sync** — pulls Google Workspace user profiles and syncs job title + phone to Front teammate custom fields
|
2. **User Sync** — pulls Google Workspace user profiles and syncs job title + phone to Front teammate custom fields
|
||||||
|
|
@ -56,8 +56,10 @@ Business time counts weekday hours only (Mon-Fri, Eastern time). Alerts are only
|
||||||
|
|
||||||
```
|
```
|
||||||
front-integrations/
|
front-integrations/
|
||||||
├── template.yaml # AWS SAM template — all infrastructure (see below)
|
├── terraform/ # HCP Terraform config (sole deploy path)
|
||||||
├── samconfig.toml.example # Deploy config template (copy to samconfig.toml, gitignored)
|
│ ├── build_packages.sh # Pip-install user_sync deps for arm64 Lambda zips
|
||||||
|
│ └── *.tf
|
||||||
|
├── template.yaml # Legacy SAM template (retained until post-cutover hygiene)
|
||||||
├── slack-app-manifest.yaml # Slack app manifest for the SLA Monitor bot
|
├── slack-app-manifest.yaml # Slack app manifest for the SLA Monitor bot
|
||||||
└── src/
|
└── src/
|
||||||
├── sla_monitor/ # front-sla-monitor Lambda
|
├── sla_monitor/ # front-sla-monitor Lambda
|
||||||
|
|
@ -70,70 +72,18 @@ front-integrations/
|
||||||
|
|
||||||
## AWS Resources
|
## AWS Resources
|
||||||
|
|
||||||
- **Stack:** `front-integrations` (SAM, us-east-1)
|
- **Account / region:** seahaven-prod `011934824531`, `us-east-1`
|
||||||
|
- **IaC:** Terraform under `terraform/` (HCP remote apply, Manual until sealed)
|
||||||
- **Lambda:** `front-sla-monitor` — Python 3.12, arm64, 256 MB, 300s timeout, 60-day log retention
|
- **Lambda:** `front-sla-monitor` — Python 3.12, arm64, 256 MB, 300s timeout, 60-day log retention
|
||||||
- **Lambda:** `front-user-sync` — Python 3.12, arm64, 256 MB, 300s timeout, 60-day log retention
|
- **Lambda:** `front-user-sync` — Python 3.12, arm64, 256 MB, 300s timeout, 60-day log retention
|
||||||
- **DynamoDB:** `front-sla-alerts` — alert history per conversation + monitor state, 7-day TTL
|
- **DynamoDB:** `front-sla-alerts` — alert history per conversation + monitor state, 7-day TTL
|
||||||
- **EventBridge:** SLA check every 15 min during business hours; user sync daily at 6 AM ET weekdays
|
- **EventBridge:** SLA check every 15 min during business hours; user sync daily at 6 AM ET weekdays
|
||||||
|
- **IAM:** Execution roles under path `/tf-managed/` with `seahaven-lambda-execution-boundary`
|
||||||
## Infrastructure (`template.yaml`)
|
- **Secrets:** ARNs as Terraform variables; values never in state
|
||||||
|
|
||||||
All of the resources above are declared as code in a single AWS SAM template,
|
|
||||||
[`template.yaml`](template.yaml) at the repository root (`Transform: AWS::Serverless-2016-10-31`).
|
|
||||||
It is the only source of infrastructure truth — there are no console-created or
|
|
||||||
CDK resources in this stack. `sam build` / `sam deploy` render it to a
|
|
||||||
CloudFormation stack named `front-integrations` in `us-east-1`.
|
|
||||||
|
|
||||||
### Global defaults
|
|
||||||
|
|
||||||
`Globals.Function` applies to every Lambda unless overridden per-function:
|
|
||||||
|
|
||||||
- Runtime `python3.12`, architecture `arm64`
|
|
||||||
- 256 MB memory, 300s timeout
|
|
||||||
- Permissions boundary `arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary`
|
|
||||||
|
|
||||||
### Resources declared
|
|
||||||
|
|
||||||
| Logical ID | Type | Notes |
|
|
||||||
|---|---|---|
|
|
||||||
| `SlaMonitorFunction` | `AWS::Serverless::Function` | `front-sla-monitor`; `Schedule` event `cron(0/15 12-22 ? * MON-FRI *)` (UTC) |
|
|
||||||
| `UserSyncFunction` | `AWS::Serverless::Function` | `front-user-sync`; `Schedule` event `cron(0 11 ? * MON-FRI *)` (UTC) |
|
|
||||||
| `AlertsTable` | `AWS::DynamoDB::Table` | `front-sla-alerts`; PAY_PER_REQUEST, `conversationId` hash key, `ttl` TTL attribute |
|
|
||||||
| `SlaMonitorLogGroup`, `UserSyncLogGroup` | `AWS::Logs::LogGroup` | 60-day retention; each function `DependsOn` its group |
|
|
||||||
| 8 alarms | `AWS::CloudWatch::Alarm` | 3 per Lambda + 2 on the table — see [Monitoring](#monitoring) |
|
|
||||||
|
|
||||||
Each function carries a least-privilege inline IAM policy: `secretsmanager:GetSecretValue`
|
|
||||||
scoped to only the secret ARNs it reads. `SlaMonitorFunction` additionally gets a
|
|
||||||
`DynamoDBCrudPolicy` scoped to `AlertsTable`.
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Secret ARNs and the alert channel are passed in at deploy time (via `parameter_overrides`
|
|
||||||
in `samconfig.toml` — see [`samconfig.toml.example`](samconfig.toml.example) — or the
|
|
||||||
`SAM_PARAMETER_OVERRIDES` Actions secret). They have no defaults and must be supplied:
|
|
||||||
|
|
||||||
| Parameter | Description |
|
|
||||||
|---|---|
|
|
||||||
| `FrontApiTokenSecretArn` | ARN of the Front API token secret |
|
|
||||||
| `SlackBotTokenSecretArn` | ARN of the Slack bot token secret |
|
|
||||||
| `GoogleServiceAccountSecretArn` | ARN of the Google service account key secret |
|
|
||||||
| `SlackAlertChannel` | Slack channel ID for `#front-sla-alerts` |
|
|
||||||
|
|
||||||
The remaining parameters (`AckSlaMinutes`, `ActionSlaMinutes`, `MonitorInboxes`,
|
|
||||||
`GoogleOrgUnits`, etc.) tune behavior and ship with sensible defaults — see
|
|
||||||
[Configuration](#configuration).
|
|
||||||
|
|
||||||
### Outputs
|
|
||||||
|
|
||||||
| Output | Value |
|
|
||||||
|---|---|
|
|
||||||
| `SlaMonitorFunctionArn` | `front-sla-monitor` Lambda ARN |
|
|
||||||
| `UserSyncFunctionArn` | `front-user-sync` Lambda ARN |
|
|
||||||
| `AlertsTableName` | `front-sla-alerts` table name |
|
|
||||||
|
|
||||||
## Monitoring
|
## Monitoring
|
||||||
|
|
||||||
CloudWatch alarms publish to the shared `site-alerts` SNS topic (`arn:aws:sns:us-east-1:328440206208:site-alerts`). All alarms are ALARM-only (no OK/recovery notification) and treat missing data as `notBreaching`.
|
CloudWatch alarms publish to the shared `site-alerts` SNS topic in seahaven-prod. All alarms are ALARM-only (no OK/recovery notification) and treat missing data as `notBreaching`.
|
||||||
|
|
||||||
| Alarm | Metric | Trigger |
|
| Alarm | Metric | Trigger |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|
@ -187,64 +137,56 @@ The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This pr
|
||||||
1. Settings > Custom Fields > Teammates tab
|
1. Settings > Custom Fields > Teammates tab
|
||||||
2. Create: **Job Title** (String) and **Phone** (String)
|
2. Create: **Job Title** (String) and **Phone** (String)
|
||||||
|
|
||||||
### 5. Store Secrets in AWS
|
### 5. Store Secrets in seahaven-prod
|
||||||
|
|
||||||
|
Create secrets in seahaven-prod (exact ARNs are wired into the Lambda boundary and Terraform variables). Strip trailing newlines before `put-secret-value`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
aws secretsmanager create-secret \
|
aws secretsmanager create-secret \
|
||||||
--name "front-integrations/front-api-token" \
|
--name "front-integrations/front-api-token" \
|
||||||
--secret-string "YOUR_FRONT_API_TOKEN" \
|
--secret-string "YOUR_FRONT_API_TOKEN" \
|
||||||
--region us-east-1
|
--region us-east-1 --profile seahaven-prod
|
||||||
|
|
||||||
aws secretsmanager create-secret \
|
aws secretsmanager create-secret \
|
||||||
--name "front-integrations/slack-bot-token" \
|
--name "front-integrations/slack-bot-token" \
|
||||||
--secret-string "xoxb-YOUR-SLACK-BOT-TOKEN" \
|
--secret-string "xoxb-YOUR-SLACK-BOT-TOKEN" \
|
||||||
--region us-east-1
|
--region us-east-1 --profile seahaven-prod
|
||||||
|
|
||||||
aws secretsmanager create-secret \
|
aws secretsmanager create-secret \
|
||||||
--name "front-integrations/google-service-account" \
|
--name "front-integrations/google-service-account" \
|
||||||
--secret-string file://path-to-service-account-key.json \
|
--secret-string file://path-to-service-account-key.json \
|
||||||
--region us-east-1
|
--region us-east-1 --profile seahaven-prod
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Deploy
|
### 6. Deploy
|
||||||
|
|
||||||
```bash
|
1. Set workspace variables in HCP (`front-integrations-prod`) from `terraform/terraform.tfvars.example`.
|
||||||
sam build
|
2. Keep `schedules_enabled=false` until DynamoDB row copy and cutover.
|
||||||
sam deploy --guided
|
3. Apply from the HCP workspace (Manual apply until the stack is sealed). Do not use local `terraform apply` against prod.
|
||||||
```
|
|
||||||
|
|
||||||
Or push to `main` to trigger the GitHub Actions deploy workflow.
|
|
||||||
|
|
||||||
### 7. GitHub Actions Secrets
|
|
||||||
|
|
||||||
| Secret | Value |
|
|
||||||
|---|---|
|
|
||||||
| `AWS_DEPLOY_ROLE_ARN` | Org-wide OIDC deploy role (set after OIDC role is added) |
|
|
||||||
| `SAM_PARAMETER_OVERRIDES` | `FrontApiTokenSecretArn=arn:... SlackBotTokenSecretArn=arn:... GoogleServiceAccountSecretArn=arn:... SlackAlertChannel=CXXXXXXXXXX` |
|
|
||||||
|
|
||||||
## Manual Testing
|
## Manual Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
aws lambda invoke --function-name front-sla-monitor --payload '{}' /dev/stdout --region us-east-1
|
aws lambda invoke --function-name front-sla-monitor --payload '{}' /dev/stdout --region us-east-1 --profile seahaven-prod
|
||||||
aws lambda invoke --function-name front-user-sync --payload '{}' /dev/stdout --region us-east-1
|
aws lambda invoke --function-name front-user-sync --payload '{}' /dev/stdout --region us-east-1 --profile seahaven-prod
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### SLA Monitor
|
### SLA Monitor
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Variable | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `AckSlaMinutes` | 60 | Business minutes before Tier 1 alert |
|
| `ack_sla_minutes` | 60 | Business minutes before Tier 1 alert |
|
||||||
| `ActionSlaMinutes` | 1440 | Business minutes before Tier 2 alert |
|
| `action_sla_minutes` | 1440 | Business minutes before Tier 2 alert |
|
||||||
| `AdamEmail` | adam@seahavenind.com | Tier 2 escalation recipient |
|
| `adam_email` | adam@seahavenind.com | Tier 2 escalation recipient |
|
||||||
| `SlackAlertChannel` | — | Channel ID for broadcast alerts |
|
| `slack_alert_channel` | — | Channel ID for broadcast alerts |
|
||||||
| `MonitorInboxes` | Triage,California,... | Inbox names to monitor (empty = all shared) |
|
| `monitor_inboxes` | Triage,California,... | Inbox names to monitor (empty = all shared) |
|
||||||
| `SlaMonitorStartDate` | 2026-05-14 | Date monitoring begins |
|
| `sla_monitor_start_date` | 2026-05-14 | Date monitoring begins |
|
||||||
|
|
||||||
### User Sync
|
### User Sync
|
||||||
|
|
||||||
| Parameter | Default | Description |
|
| Variable | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GoogleAdminEmail` | adam@seahavenind.com | Google Workspace admin to impersonate |
|
| `google_admin_email` | adam@seahavenind.com | Google Workspace admin to impersonate |
|
||||||
| `GoogleOrgUnits` | /Office/Scheduling,/Office/Operations | Org unit paths to sync |
|
| `google_org_units` | /Office/Scheduling,/Office/Operations | Org unit paths to sync |
|
||||||
|
|
|
||||||
74
terraform/.terraform.lock.hcl
generated
Normal file
74
terraform/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# This file is maintained automatically by "terraform init".
|
||||||
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/archive" {
|
||||||
|
version = "2.8.0"
|
||||||
|
constraints = "~> 2.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:WB6H5ksIZiyq1lQlD/PWeh+tn4FLsbSjVnRW3+4xe2Y=",
|
||||||
|
"h1:cMBtvdHEgvTmglbioVehZCaOIPocumu9+vlGw5Dsaro=",
|
||||||
|
"h1:jdmKm+xl6ZcQrijxapnZ94RVuz/G4vk7hsIa1N0VT5Q=",
|
||||||
|
"h1:oRWx6ZDIdlNBF90L8TzV9X7pQ+P403hoCDiBfmUfhC0=",
|
||||||
|
"zh:0d14713fdc259fb377d0b899ad3c650a34194bd52194c863303ef22a65a580e2",
|
||||||
|
"zh:369b56040c7a8085d04e7e8ffac1e2b321a3170e502f788819bc34b868ec016f",
|
||||||
|
"zh:4d1a3b983ed6af5a52bfe12794674ae55cbadfa6021b37106ade68b433ad216a",
|
||||||
|
"zh:5c547549e26e083573c78a966ca68ce6d7df6bb8f3948f66a575f07da46b74ea",
|
||||||
|
"zh:6de093e62a975eb19a5e3017ce38e6e3cb639c17b79648d2000e0a8348f0e997",
|
||||||
|
"zh:7267936c2cdbc448efeb594d73e6b56a53d6a7ae14fe88cdd2a4133adc3302f0",
|
||||||
|
"zh:7482f023050ed426b4b45116e1761643bc33b1fd4ce4a6fab207ae2571f35940",
|
||||||
|
"zh:76bbd93b234e5a2927d98b511d86565700f549b570871a194c35f944b96cefb7",
|
||||||
|
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||||
|
"zh:c6afc4bc1f002bac9c173007dd4da05fde788cd14c2916089f958c33fedb0dfa",
|
||||||
|
"zh:d3ba40bd806a3a08e9237dece679193c99afb2085de6b45d7f5d1f673cfcd368",
|
||||||
|
"zh:e1ad7ded53ecd6f0e5b473a3b44eae2b2e885653a56050ab583d387332be02e4",
|
||||||
|
"zh:e93e78575ce82be6084cc153c24ba8f385dc8d6880888ee66e918460c870953d",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/aws" {
|
||||||
|
version = "6.58.0"
|
||||||
|
constraints = "~> 6.57"
|
||||||
|
hashes = [
|
||||||
|
"h1:1im6ypdeXLXq3sHElserTE62qmIqNiHLAyC3R5EnFFw=",
|
||||||
|
"h1:2kpake4zZKRX5437QVIRU3qFYH6Bjw/QE1fgVCPOrUg=",
|
||||||
|
"h1:OWl47Bo8Vzlf5srTUCmA6v4kvQGfah/P1joRtIYUUMc=",
|
||||||
|
"h1:UFot9S97tuAPvjKvoxm08sDG/gKYdDK+lMwsZKtLieY=",
|
||||||
|
"zh:1221253beee5629fb503d79cebc9bc661279cbc4be5d01db9ab4c1b702108250",
|
||||||
|
"zh:132bd0925bdc4b72446ac750b7ccb1e19b9ba8fbb6df57b2c1423314d2195d4f",
|
||||||
|
"zh:18cda250b9e82b753808715893c8927f132273c00ffae7a697d65ac1cb577e48",
|
||||||
|
"zh:204c944f1fb7f440a335bb2083c9691a9d1f677aea9701025dd5816aee41f0ba",
|
||||||
|
"zh:2dc41df289f2b10a01e650cdd73699955f0ab0645d09cfb114a8cd0f4cc4ede7",
|
||||||
|
"zh:345633dfa9a234659d52aadd126e6dce658518c3ab5cbf6d871221287ed5ec56",
|
||||||
|
"zh:4dadcced73e742903158bc9838936d911f3fa4c2c37b5591c1a28f8f2a1902a6",
|
||||||
|
"zh:5bc60cc2b8c093da98b211d9f6c21c9ecec0f21b944d9ecbe3961fba33086e80",
|
||||||
|
"zh:6cc8f084938b0033a9c0c910989919dad6b1683e76e0afa1a5c604e39f398a75",
|
||||||
|
"zh:7db214647f79de9a033b5dfd6cbfaa42d53c4d056b32cb3acc7ad99306dd548a",
|
||||||
|
"zh:9078589ec881cee7ed9403af262c98ff257fb3e1baae72ff6429a398b1c730af",
|
||||||
|
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||||
|
"zh:bd5bce6aec4d4922b1127b8575688bd4bc4279670ee28d198ede404709826c7c",
|
||||||
|
"zh:cd900ecf56d21023873898b06e40234f3f4d350f2343b7d9b980d6c5cb604fae",
|
||||||
|
"zh:dbe93b276a84421026b956c3c5b4eb8897da6cbb54b93cb89f5d6ebbd30805ca",
|
||||||
|
"zh:f6b6c7bb2dbf04ee085e5c22f7a65b3ccaebf368ed95584dc0dfee8a22771056",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/external" {
|
||||||
|
version = "2.4.0"
|
||||||
|
constraints = "~> 2.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:AmY6ZeIvqoTT5ZjzD+P49PeQH6Va1QLMkX+7MUQfYoA=",
|
||||||
|
"zh:0772afb42b658468ac5e15df33bf2080456f8f0b8ab163bfe9c50d2b2ea02135",
|
||||||
|
"zh:0ac31a9aaa43dfcff5944b791596cdc94e153348e4bb4642282d034dff548134",
|
||||||
|
"zh:32d8492b1bdcc956ca3c6d00c6392d0a83942ff11d4820c7ee63ca6796e06950",
|
||||||
|
"zh:3c0482e894429f528ce6655a76ab0d8a9f7c0dacc6c828865e1515d4a7dbb852",
|
||||||
|
"zh:61e68100b4db2f930b31491f23c602126382fd5e51252be1b551f0e17f8ddbee",
|
||||||
|
"zh:6d60f615a0ad85eb962c9eb94f25e3eba7a72684ce276ba5dfb23f36b295a8f8",
|
||||||
|
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||||
|
"zh:9ced2745eb5f1346203027d2dd7bf856ad1d279a25730ff7dbc6eec187aaca0c",
|
||||||
|
"zh:a8378558a177d43f55aa0d79d4fae91a704695122a1b109668c1daa8fb76f09d",
|
||||||
|
"zh:aadd98086133d3ebea67437d56512fdcc6dfb3bd34dfc23f276c0db9272e27b4",
|
||||||
|
"zh:beff701b653841e70441978137768f54e7dc6c27e7bf12a4589087f01f5bbcee",
|
||||||
|
"zh:c91c2223b29fdbc0044d20e1936ccc051d010727a13f2ff1e75e51f09bff33a3",
|
||||||
|
"zh:d491f9c2d32a39dc4031628469ae7c8aec0074312a7c1f0286b173cdcf854a54",
|
||||||
|
]
|
||||||
|
}
|
||||||
143
terraform/alarms.tf
Normal file
143
terraform/alarms.tf
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
resource "aws_cloudwatch_metric_alarm" "sla_monitor_errors" {
|
||||||
|
alarm_name = "front-sla-monitor-errors"
|
||||||
|
alarm_description = "front-sla-monitor invocation errors"
|
||||||
|
namespace = "AWS/Lambda"
|
||||||
|
metric_name = "Errors"
|
||||||
|
statistic = "Sum"
|
||||||
|
period = 300
|
||||||
|
evaluation_periods = 1
|
||||||
|
threshold = 0
|
||||||
|
comparison_operator = "GreaterThanThreshold"
|
||||||
|
treat_missing_data = "notBreaching"
|
||||||
|
alarm_actions = [var.alarm_sns_topic_arn]
|
||||||
|
|
||||||
|
dimensions = {
|
||||||
|
FunctionName = aws_lambda_function.sla_monitor.function_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_metric_alarm" "sla_monitor_throttles" {
|
||||||
|
alarm_name = "front-sla-monitor-throttles"
|
||||||
|
alarm_description = "front-sla-monitor invocations throttled"
|
||||||
|
namespace = "AWS/Lambda"
|
||||||
|
metric_name = "Throttles"
|
||||||
|
statistic = "Sum"
|
||||||
|
period = 300
|
||||||
|
evaluation_periods = 1
|
||||||
|
threshold = 0
|
||||||
|
comparison_operator = "GreaterThanThreshold"
|
||||||
|
treat_missing_data = "notBreaching"
|
||||||
|
alarm_actions = [var.alarm_sns_topic_arn]
|
||||||
|
|
||||||
|
dimensions = {
|
||||||
|
FunctionName = aws_lambda_function.sla_monitor.function_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_metric_alarm" "sla_monitor_duration" {
|
||||||
|
alarm_name = "front-sla-monitor-duration"
|
||||||
|
alarm_description = "front-sla-monitor approaching its 300s timeout (>90%)"
|
||||||
|
namespace = "AWS/Lambda"
|
||||||
|
metric_name = "Duration"
|
||||||
|
statistic = "Maximum"
|
||||||
|
period = 300
|
||||||
|
evaluation_periods = 1
|
||||||
|
threshold = 270000
|
||||||
|
comparison_operator = "GreaterThanThreshold"
|
||||||
|
treat_missing_data = "notBreaching"
|
||||||
|
alarm_actions = [var.alarm_sns_topic_arn]
|
||||||
|
|
||||||
|
dimensions = {
|
||||||
|
FunctionName = aws_lambda_function.sla_monitor.function_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_metric_alarm" "user_sync_errors" {
|
||||||
|
alarm_name = "front-user-sync-errors"
|
||||||
|
alarm_description = "front-user-sync invocation errors"
|
||||||
|
namespace = "AWS/Lambda"
|
||||||
|
metric_name = "Errors"
|
||||||
|
statistic = "Sum"
|
||||||
|
period = 300
|
||||||
|
evaluation_periods = 1
|
||||||
|
threshold = 0
|
||||||
|
comparison_operator = "GreaterThanThreshold"
|
||||||
|
treat_missing_data = "notBreaching"
|
||||||
|
alarm_actions = [var.alarm_sns_topic_arn]
|
||||||
|
|
||||||
|
dimensions = {
|
||||||
|
FunctionName = aws_lambda_function.user_sync.function_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_metric_alarm" "user_sync_throttles" {
|
||||||
|
alarm_name = "front-user-sync-throttles"
|
||||||
|
alarm_description = "front-user-sync invocations throttled"
|
||||||
|
namespace = "AWS/Lambda"
|
||||||
|
metric_name = "Throttles"
|
||||||
|
statistic = "Sum"
|
||||||
|
period = 300
|
||||||
|
evaluation_periods = 1
|
||||||
|
threshold = 0
|
||||||
|
comparison_operator = "GreaterThanThreshold"
|
||||||
|
treat_missing_data = "notBreaching"
|
||||||
|
alarm_actions = [var.alarm_sns_topic_arn]
|
||||||
|
|
||||||
|
dimensions = {
|
||||||
|
FunctionName = aws_lambda_function.user_sync.function_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_metric_alarm" "user_sync_duration" {
|
||||||
|
alarm_name = "front-user-sync-duration"
|
||||||
|
alarm_description = "front-user-sync approaching its 300s timeout (>90%)"
|
||||||
|
namespace = "AWS/Lambda"
|
||||||
|
metric_name = "Duration"
|
||||||
|
statistic = "Maximum"
|
||||||
|
period = 300
|
||||||
|
evaluation_periods = 1
|
||||||
|
threshold = 270000
|
||||||
|
comparison_operator = "GreaterThanThreshold"
|
||||||
|
treat_missing_data = "notBreaching"
|
||||||
|
alarm_actions = [var.alarm_sns_topic_arn]
|
||||||
|
|
||||||
|
dimensions = {
|
||||||
|
FunctionName = aws_lambda_function.user_sync.function_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_metric_alarm" "alerts_read_throttle" {
|
||||||
|
alarm_name = "front-sla-alerts-read-throttle"
|
||||||
|
alarm_description = "front-sla-alerts DynamoDB table had one or more read throttle events"
|
||||||
|
namespace = "AWS/DynamoDB"
|
||||||
|
metric_name = "ReadThrottleEvents"
|
||||||
|
statistic = "Sum"
|
||||||
|
period = 300
|
||||||
|
evaluation_periods = 1
|
||||||
|
threshold = 0
|
||||||
|
comparison_operator = "GreaterThanThreshold"
|
||||||
|
treat_missing_data = "notBreaching"
|
||||||
|
alarm_actions = [var.alarm_sns_topic_arn]
|
||||||
|
|
||||||
|
dimensions = {
|
||||||
|
TableName = aws_dynamodb_table.alerts.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_metric_alarm" "alerts_write_throttle" {
|
||||||
|
alarm_name = "front-sla-alerts-write-throttle"
|
||||||
|
alarm_description = "front-sla-alerts DynamoDB table had one or more write throttle events"
|
||||||
|
namespace = "AWS/DynamoDB"
|
||||||
|
metric_name = "WriteThrottleEvents"
|
||||||
|
statistic = "Sum"
|
||||||
|
period = 300
|
||||||
|
evaluation_periods = 1
|
||||||
|
threshold = 0
|
||||||
|
comparison_operator = "GreaterThanThreshold"
|
||||||
|
treat_missing_data = "notBreaching"
|
||||||
|
alarm_actions = [var.alarm_sns_topic_arn]
|
||||||
|
|
||||||
|
dimensions = {
|
||||||
|
TableName = aws_dynamodb_table.alerts.name
|
||||||
|
}
|
||||||
|
}
|
||||||
53
terraform/artifacts.tf
Normal file
53
terraform/artifacts.tf
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# HCP plan and apply run on separate workers. archive_file paths from plan are
|
||||||
|
# not on the apply worker, so zip bytes are carried in the plan via
|
||||||
|
# content_base64 and uploaded to S3 at apply time for Lambda to consume.
|
||||||
|
#
|
||||||
|
# Package build runs during plan via external data (local-exec provisioners
|
||||||
|
# only run on apply; archive_file needs build/ present at plan time).
|
||||||
|
|
||||||
|
data "external" "package_build" {
|
||||||
|
program = ["bash", "${path.module}/build_packages_external.sh"]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_s3_bucket" "artifacts" {
|
||||||
|
bucket = "front-integrations-artifacts-${local.account_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_s3_bucket_public_access_block" "artifacts" {
|
||||||
|
bucket = aws_s3_bucket.artifacts.id
|
||||||
|
|
||||||
|
block_public_acls = true
|
||||||
|
block_public_policy = true
|
||||||
|
ignore_public_acls = true
|
||||||
|
restrict_public_buckets = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data "archive_file" "sla_monitor" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "${path.module}/build/sla_monitor"
|
||||||
|
output_path = "${path.module}/build/front-sla-monitor.zip"
|
||||||
|
|
||||||
|
depends_on = [data.external.package_build]
|
||||||
|
}
|
||||||
|
|
||||||
|
data "archive_file" "user_sync" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "${path.module}/build/user_sync"
|
||||||
|
output_path = "${path.module}/build/front-user-sync.zip"
|
||||||
|
|
||||||
|
depends_on = [data.external.package_build]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_s3_object" "sla_monitor" {
|
||||||
|
bucket = aws_s3_bucket.artifacts.id
|
||||||
|
key = "front-sla-monitor.zip"
|
||||||
|
content_base64 = filebase64(data.archive_file.sla_monitor.output_path)
|
||||||
|
source_hash = data.archive_file.sla_monitor.output_base64sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_s3_object" "user_sync" {
|
||||||
|
bucket = aws_s3_bucket.artifacts.id
|
||||||
|
key = "front-user-sync.zip"
|
||||||
|
content_base64 = filebase64(data.archive_file.user_sync.output_path)
|
||||||
|
source_hash = data.archive_file.user_sync.output_base64sha256
|
||||||
|
}
|
||||||
61
terraform/build_packages.sh
Executable file
61
terraform/build_packages.sh
Executable file
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Package Lambda zips for HCP plan/apply. Runs on the Terraform worker.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
BUILD="${ROOT}/build"
|
||||||
|
SRC="$(cd "${ROOT}/../src" && pwd)"
|
||||||
|
|
||||||
|
# Copy only regular files that resolve inside SRC (no symlink escape).
|
||||||
|
copy_src_file() {
|
||||||
|
local rel="$1"
|
||||||
|
local dest="$2"
|
||||||
|
local src_path="${SRC}/${rel}"
|
||||||
|
|
||||||
|
if [[ -L "${src_path}" ]]; then
|
||||||
|
echo "error: refusing symlink source: ${src_path}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -f "${src_path}" ]]; then
|
||||||
|
echo "error: missing regular file: ${src_path}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local resolved
|
||||||
|
resolved="$(cd "$(dirname "${src_path}")" && pwd)/$(basename "${src_path}")"
|
||||||
|
case "${resolved}" in
|
||||||
|
"${SRC}"/*) ;;
|
||||||
|
*)
|
||||||
|
echo "error: path escapes src tree: ${resolved}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${dest}")"
|
||||||
|
# -P: never follow symlinks if the destination path is replaced mid-run.
|
||||||
|
cp -P "${src_path}" "${dest}"
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -rf "${BUILD}"
|
||||||
|
mkdir -p "${BUILD}/sla_monitor" "${BUILD}/user_sync"
|
||||||
|
|
||||||
|
copy_src_file "sla_monitor/app.py" "${BUILD}/sla_monitor/app.py"
|
||||||
|
copy_src_file "user_sync/app.py" "${BUILD}/user_sync/app.py"
|
||||||
|
copy_src_file "user_sync/requirements.txt" "${BUILD}/user_sync/requirements.txt"
|
||||||
|
|
||||||
|
python3 -m pip install \
|
||||||
|
--quiet \
|
||||||
|
--disable-pip-version-check \
|
||||||
|
-r "${BUILD}/user_sync/requirements.txt" \
|
||||||
|
-t "${BUILD}/user_sync/" \
|
||||||
|
--platform manylinux2014_aarch64 \
|
||||||
|
--implementation cp \
|
||||||
|
--python-version 3.12 \
|
||||||
|
--only-binary=:all: \
|
||||||
|
--upgrade
|
||||||
|
|
||||||
|
# Runtime provides boto3; drop the copy to keep the zip smaller.
|
||||||
|
rm -rf "${BUILD}/user_sync/boto3" "${BUILD}/user_sync/botocore" \
|
||||||
|
"${BUILD}/user_sync/s3transfer" "${BUILD}/user_sync/jmespath" \
|
||||||
|
"${BUILD}/user_sync/"*.dist-info 2>/dev/null || true
|
||||||
|
rm -f "${BUILD}/user_sync/requirements.txt"
|
||||||
17
terraform/build_packages_external.sh
Executable file
17
terraform/build_packages_external.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Terraform external data source entrypoint. Stdout must be JSON only.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
"${ROOT}/build_packages.sh" >&2
|
||||||
|
|
||||||
|
hash="$(
|
||||||
|
{
|
||||||
|
# -P: do not follow symlinks; only hash regular files under build/.
|
||||||
|
find -P "${ROOT}/build/sla_monitor" "${ROOT}/build/user_sync" -type f -print0 2>/dev/null \
|
||||||
|
| sort -z \
|
||||||
|
| xargs -0 sha256sum
|
||||||
|
} | sha256sum | awk '{print $1}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '{"status":"ok","hash":"%s"}\n' "${hash}"
|
||||||
15
terraform/dynamodb.tf
Normal file
15
terraform/dynamodb.tf
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
resource "aws_dynamodb_table" "alerts" {
|
||||||
|
name = "front-sla-alerts"
|
||||||
|
billing_mode = "PAY_PER_REQUEST"
|
||||||
|
hash_key = "conversationId"
|
||||||
|
|
||||||
|
attribute {
|
||||||
|
name = "conversationId"
|
||||||
|
type = "S"
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl {
|
||||||
|
attribute_name = "ttl"
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
41
terraform/events.tf
Normal file
41
terraform/events.tf
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
resource "aws_cloudwatch_event_rule" "sla_monitor" {
|
||||||
|
name = "front-sla-monitor"
|
||||||
|
description = "Check Front conversations for SLA breaches every 15 min during business hours"
|
||||||
|
schedule_expression = var.sla_monitor_schedule
|
||||||
|
state = local.schedule_state
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_event_target" "sla_monitor" {
|
||||||
|
rule = aws_cloudwatch_event_rule.sla_monitor.name
|
||||||
|
target_id = "front-sla-monitor"
|
||||||
|
arn = aws_lambda_function.sla_monitor.arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_lambda_permission" "sla_monitor_events" {
|
||||||
|
statement_id = "AllowExecutionFromEventBridge"
|
||||||
|
action = "lambda:InvokeFunction"
|
||||||
|
function_name = aws_lambda_function.sla_monitor.function_name
|
||||||
|
principal = "events.amazonaws.com"
|
||||||
|
source_arn = aws_cloudwatch_event_rule.sla_monitor.arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_event_rule" "user_sync" {
|
||||||
|
name = "front-user-sync"
|
||||||
|
description = "Sync Google Workspace user profiles to Front daily at 6 AM ET"
|
||||||
|
schedule_expression = var.user_sync_schedule
|
||||||
|
state = local.schedule_state
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_event_target" "user_sync" {
|
||||||
|
rule = aws_cloudwatch_event_rule.user_sync.name
|
||||||
|
target_id = "front-user-sync"
|
||||||
|
arn = aws_lambda_function.user_sync.arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_lambda_permission" "user_sync_events" {
|
||||||
|
statement_id = "AllowExecutionFromEventBridge"
|
||||||
|
action = "lambda:InvokeFunction"
|
||||||
|
function_name = aws_lambda_function.user_sync.function_name
|
||||||
|
principal = "events.amazonaws.com"
|
||||||
|
source_arn = aws_cloudwatch_event_rule.user_sync.arn
|
||||||
|
}
|
||||||
53
terraform/iam.tf
Normal file
53
terraform/iam.tf
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
data "aws_iam_policy_document" "lambda_assume" {
|
||||||
|
statement {
|
||||||
|
effect = "Allow"
|
||||||
|
actions = ["sts:AssumeRole"]
|
||||||
|
|
||||||
|
principals {
|
||||||
|
type = "Service"
|
||||||
|
identifiers = ["lambda.amazonaws.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role" "sla_monitor" {
|
||||||
|
name = "front-sla-monitor"
|
||||||
|
path = "/tf-managed/"
|
||||||
|
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
|
||||||
|
permissions_boundary = local.boundary_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role_policy_attachment" "sla_monitor_basic" {
|
||||||
|
role = aws_iam_role.sla_monitor.name
|
||||||
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role_policy" "sla_monitor_secrets" {
|
||||||
|
name = "secretsmanager-get"
|
||||||
|
role = aws_iam_role.sla_monitor.id
|
||||||
|
policy = local.sla_monitor_secrets_policy_json
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role_policy" "sla_monitor_ddb" {
|
||||||
|
name = "dynamodb-crud"
|
||||||
|
role = aws_iam_role.sla_monitor.id
|
||||||
|
policy = local.sla_monitor_ddb_policy_json
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role" "user_sync" {
|
||||||
|
name = "front-user-sync"
|
||||||
|
path = "/tf-managed/"
|
||||||
|
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
|
||||||
|
permissions_boundary = local.boundary_arn
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role_policy_attachment" "user_sync_basic" {
|
||||||
|
role = aws_iam_role.user_sync.name
|
||||||
|
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role_policy" "user_sync_secrets" {
|
||||||
|
name = "secretsmanager-get"
|
||||||
|
role = aws_iam_role.user_sync.id
|
||||||
|
policy = local.user_sync_secrets_policy_json
|
||||||
|
}
|
||||||
60
terraform/lambda.tf
Normal file
60
terraform/lambda.tf
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
resource "aws_cloudwatch_log_group" "sla_monitor" {
|
||||||
|
name = "/aws/lambda/front-sla-monitor"
|
||||||
|
retention_in_days = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_log_group" "user_sync" {
|
||||||
|
name = "/aws/lambda/front-user-sync"
|
||||||
|
retention_in_days = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_lambda_function" "sla_monitor" {
|
||||||
|
function_name = "front-sla-monitor"
|
||||||
|
role = aws_iam_role.sla_monitor.arn
|
||||||
|
handler = "app.handler"
|
||||||
|
runtime = "python3.12"
|
||||||
|
architectures = ["arm64"]
|
||||||
|
memory_size = 256
|
||||||
|
timeout = 300
|
||||||
|
|
||||||
|
s3_bucket = aws_s3_bucket.artifacts.id
|
||||||
|
s3_key = aws_s3_object.sla_monitor.key
|
||||||
|
source_code_hash = data.archive_file.sla_monitor.output_base64sha256
|
||||||
|
|
||||||
|
environment {
|
||||||
|
variables = local.sla_monitor_env
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
aws_s3_object.sla_monitor,
|
||||||
|
aws_cloudwatch_log_group.sla_monitor,
|
||||||
|
aws_iam_role_policy_attachment.sla_monitor_basic,
|
||||||
|
aws_iam_role_policy.sla_monitor_secrets,
|
||||||
|
aws_iam_role_policy.sla_monitor_ddb,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_lambda_function" "user_sync" {
|
||||||
|
function_name = "front-user-sync"
|
||||||
|
role = aws_iam_role.user_sync.arn
|
||||||
|
handler = "app.handler"
|
||||||
|
runtime = "python3.12"
|
||||||
|
architectures = ["arm64"]
|
||||||
|
memory_size = 256
|
||||||
|
timeout = 300
|
||||||
|
|
||||||
|
s3_bucket = aws_s3_bucket.artifacts.id
|
||||||
|
s3_key = aws_s3_object.user_sync.key
|
||||||
|
source_code_hash = data.archive_file.user_sync.output_base64sha256
|
||||||
|
|
||||||
|
environment {
|
||||||
|
variables = local.user_sync_env
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
aws_s3_object.user_sync,
|
||||||
|
aws_cloudwatch_log_group.user_sync,
|
||||||
|
aws_iam_role_policy_attachment.user_sync_basic,
|
||||||
|
aws_iam_role_policy.user_sync_secrets,
|
||||||
|
]
|
||||||
|
}
|
||||||
77
terraform/locals.tf
Normal file
77
terraform/locals.tf
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
locals {
|
||||||
|
account_id = "011934824531"
|
||||||
|
boundary_arn = "arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary"
|
||||||
|
|
||||||
|
schedule_state = var.schedules_enabled ? "ENABLED" : "DISABLED"
|
||||||
|
|
||||||
|
sla_monitor_env = {
|
||||||
|
FRONT_SECRET_NAME = var.front_api_token_secret_arn
|
||||||
|
SLACK_SECRET_NAME = var.slack_bot_token_secret_arn
|
||||||
|
SLACK_ALERT_CHANNEL = var.slack_alert_channel
|
||||||
|
ADAM_EMAIL = var.adam_email
|
||||||
|
TABLE_NAME = aws_dynamodb_table.alerts.name
|
||||||
|
ACK_SLA_MINUTES = tostring(var.ack_sla_minutes)
|
||||||
|
ACTION_SLA_MINUTES = tostring(var.action_sla_minutes)
|
||||||
|
MONITOR_INBOXES = var.monitor_inboxes
|
||||||
|
START_DATE = var.sla_monitor_start_date
|
||||||
|
}
|
||||||
|
|
||||||
|
user_sync_env = {
|
||||||
|
GOOGLE_SECRET_NAME = var.google_service_account_secret_arn
|
||||||
|
FRONT_SECRET_NAME = var.front_api_token_secret_arn
|
||||||
|
GOOGLE_ADMIN_EMAIL = var.google_admin_email
|
||||||
|
GOOGLE_OUS = var.google_org_units
|
||||||
|
}
|
||||||
|
|
||||||
|
sla_monitor_secrets_policy_json = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = ["secretsmanager:GetSecretValue"]
|
||||||
|
Resource = [
|
||||||
|
var.front_api_token_secret_arn,
|
||||||
|
var.slack_bot_token_secret_arn,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
user_sync_secrets_policy_json = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = ["secretsmanager:GetSecretValue"]
|
||||||
|
Resource = [
|
||||||
|
var.google_service_account_secret_arn,
|
||||||
|
var.front_api_token_secret_arn,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
sla_monitor_ddb_policy_json = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"dynamodb:GetItem",
|
||||||
|
"dynamodb:PutItem",
|
||||||
|
"dynamodb:UpdateItem",
|
||||||
|
"dynamodb:DeleteItem",
|
||||||
|
"dynamodb:Query",
|
||||||
|
"dynamodb:Scan",
|
||||||
|
"dynamodb:BatchGetItem",
|
||||||
|
"dynamodb:BatchWriteItem",
|
||||||
|
"dynamodb:DescribeTable",
|
||||||
|
]
|
||||||
|
Resource = [
|
||||||
|
aws_dynamodb_table.alerts.arn,
|
||||||
|
"${aws_dynamodb_table.alerts.arn}/index/*",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
19
terraform/outputs.tf
Normal file
19
terraform/outputs.tf
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
output "sla_monitor_function_arn" {
|
||||||
|
description = "Front SLA Monitor Lambda ARN"
|
||||||
|
value = aws_lambda_function.sla_monitor.arn
|
||||||
|
}
|
||||||
|
|
||||||
|
output "user_sync_function_arn" {
|
||||||
|
description = "Front User Sync Lambda ARN"
|
||||||
|
value = aws_lambda_function.user_sync.arn
|
||||||
|
}
|
||||||
|
|
||||||
|
output "alerts_table_name" {
|
||||||
|
description = "DynamoDB alerts table name"
|
||||||
|
value = aws_dynamodb_table.alerts.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "alerts_table_arn" {
|
||||||
|
description = "DynamoDB alerts table ARN"
|
||||||
|
value = aws_dynamodb_table.alerts.arn
|
||||||
|
}
|
||||||
11
terraform/providers.tf
Normal file
11
terraform/providers.tf
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
provider "aws" {
|
||||||
|
region = var.aws_region
|
||||||
|
|
||||||
|
default_tags {
|
||||||
|
tags = {
|
||||||
|
Project = "front-integrations"
|
||||||
|
ManagedBy = "terraform"
|
||||||
|
Workspace = "front-integrations-prod"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
terraform/terraform.tfvars.example
Normal file
7
terraform/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Wire these as HCP workspace Terraform variables (never commit real .tfvars).
|
||||||
|
# Prod ARNs created 2026-08-05 (PLAT-72):
|
||||||
|
front_api_token_secret_arn = "arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/front-api-token-UXKv0U"
|
||||||
|
slack_bot_token_secret_arn = "arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/slack-bot-token-giGfA7"
|
||||||
|
google_service_account_secret_arn = "arn:aws:secretsmanager:us-east-1:011934824531:secret:front-integrations/google-service-account-dqv3Bo"
|
||||||
|
slack_alert_channel = "C0B2XGSV63V"
|
||||||
|
# schedules_enabled = false until DDB copy + cutover (default)
|
||||||
91
terraform/variables.tf
Normal file
91
terraform/variables.tf
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
variable "aws_region" {
|
||||||
|
type = string
|
||||||
|
description = "AWS region for all resources"
|
||||||
|
default = "us-east-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "front_api_token_secret_arn" {
|
||||||
|
type = string
|
||||||
|
description = "Secrets Manager ARN for the Front API token (exact ARN, including suffix)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "slack_bot_token_secret_arn" {
|
||||||
|
type = string
|
||||||
|
description = "Secrets Manager ARN for the Slack bot token (exact ARN, including suffix)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "google_service_account_secret_arn" {
|
||||||
|
type = string
|
||||||
|
description = "Secrets Manager ARN for the Google service account JSON (exact ARN, including suffix)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "slack_alert_channel" {
|
||||||
|
type = string
|
||||||
|
description = "Slack channel ID for #front-sla-alerts"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "adam_email" {
|
||||||
|
type = string
|
||||||
|
description = "Email address for Tier 2 escalation"
|
||||||
|
default = "adam@seahavenind.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ack_sla_minutes" {
|
||||||
|
type = number
|
||||||
|
description = "Business minutes before Tier 1 alert"
|
||||||
|
default = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "action_sla_minutes" {
|
||||||
|
type = number
|
||||||
|
description = "Business minutes before Tier 2 alert"
|
||||||
|
default = 1440
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "monitor_inboxes" {
|
||||||
|
type = string
|
||||||
|
description = "Comma-separated inbox names to monitor (empty = all shared)"
|
||||||
|
default = "Triage,California,West Coast,Central,East Coast,Vendors"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sla_monitor_start_date" {
|
||||||
|
type = string
|
||||||
|
description = "Date when SLA monitoring begins (YYYY-MM-DD, Eastern)"
|
||||||
|
default = "2026-05-14"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "google_admin_email" {
|
||||||
|
type = string
|
||||||
|
description = "Google Workspace admin email to impersonate for Directory API"
|
||||||
|
default = "adam@seahavenind.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "google_org_units" {
|
||||||
|
type = string
|
||||||
|
description = "Comma-separated Google Workspace org unit paths to sync"
|
||||||
|
default = "/Office/Scheduling,/Office/Operations"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sla_monitor_schedule" {
|
||||||
|
type = string
|
||||||
|
description = "EventBridge schedule for SLA monitor (UTC)"
|
||||||
|
default = "cron(0/15 12-22 ? * MON-FRI *)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "user_sync_schedule" {
|
||||||
|
type = string
|
||||||
|
description = "EventBridge schedule for user sync (UTC)"
|
||||||
|
default = "cron(0 11 ? * MON-FRI *)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "schedules_enabled" {
|
||||||
|
type = bool
|
||||||
|
description = "Whether EventBridge schedules are ENABLED (false until DDB copy + cutover)"
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "alarm_sns_topic_arn" {
|
||||||
|
type = string
|
||||||
|
description = "SNS topic ARN for CloudWatch alarms (site-alerts)"
|
||||||
|
default = "arn:aws:sns:us-east-1:011934824531:site-alerts"
|
||||||
|
}
|
||||||
26
terraform/versions.tf
Normal file
26
terraform/versions.tf
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
terraform {
|
||||||
|
required_version = ">= 1.7.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "~> 6.57"
|
||||||
|
}
|
||||||
|
archive = {
|
||||||
|
source = "hashicorp/archive"
|
||||||
|
version = "~> 2.0"
|
||||||
|
}
|
||||||
|
external = {
|
||||||
|
source = "hashicorp/external"
|
||||||
|
version = "~> 2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cloud {
|
||||||
|
organization = "seahaven"
|
||||||
|
|
||||||
|
workspaces {
|
||||||
|
name = "front-integrations-prod"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue