mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
feat(terraform): adopt live deployment roles safely
Add import-only dev and staging state with least-privilege HCP authentication and plan safety guards.
This commit is contained in:
parent
b605d5be02
commit
25c2e84e8f
51 changed files with 3039 additions and 17 deletions
50
.github/workflows/ci-terraform.yaml
vendored
Normal file
50
.github/workflows/ci-terraform.yaml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Terraform CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev, staging, main]
|
||||
paths:
|
||||
- "terraform/**"
|
||||
- "scripts/check-terraform-import-plan.py"
|
||||
- "scripts/test-terraform-import-plan-check.py"
|
||||
- ".github/workflows/ci-terraform.yaml"
|
||||
push:
|
||||
branches: [dev, staging, main]
|
||||
paths:
|
||||
- "terraform/**"
|
||||
- "scripts/check-terraform-import-plan.py"
|
||||
- "scripts/test-terraform-import-plan-check.py"
|
||||
- ".github/workflows/ci-terraform.yaml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
terraform:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dir:
|
||||
- terraform
|
||||
- terraform/bootstrap
|
||||
- terraform/live/dev
|
||||
- terraform/live/staging
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ matrix.dir }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||
with:
|
||||
terraform_version: "1.9.8"
|
||||
|
||||
- name: Terraform fmt
|
||||
run: terraform fmt -check -recursive
|
||||
|
||||
- name: Terraform init
|
||||
run: terraform init -backend=false
|
||||
|
||||
- name: Terraform validate
|
||||
run: terraform validate
|
||||
87
.github/workflows/deploy.yml
vendored
87
.github/workflows/deploy.yml
vendored
|
|
@ -1,10 +1,10 @@
|
|||
name: Validate and deploy dev
|
||||
name: Validate and deploy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
branches: [dev, staging, main]
|
||||
push:
|
||||
branches: [dev]
|
||||
branches: [dev, staging, main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
@ -32,6 +32,11 @@ jobs:
|
|||
cache: npm
|
||||
cache-dependency-path: infra/cdk/package-lock.json
|
||||
|
||||
- name: Set up Terraform
|
||||
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
|
||||
with:
|
||||
terraform_version: "1.9.8"
|
||||
|
||||
- name: Repository quality gate
|
||||
run: bash scripts/governance-check.sh
|
||||
|
||||
|
|
@ -40,6 +45,15 @@ jobs:
|
|||
npm ci --prefix infra/cdk
|
||||
npm run synth --prefix infra/cdk
|
||||
|
||||
- name: Terraform fmt and validate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for dir in terraform terraform/bootstrap; do
|
||||
terraform -chdir="$dir" fmt -check -recursive
|
||||
terraform -chdir="$dir" init -backend=false
|
||||
terraform -chdir="$dir" validate
|
||||
done
|
||||
|
||||
- name: Build Elastic Beanstalk source bundle
|
||||
run: bash scripts/package-elastic-beanstalk.sh
|
||||
|
||||
|
|
@ -66,22 +80,61 @@ jobs:
|
|||
.artifacts/elastic-beanstalk/webhook-config.txt
|
||||
|
||||
deploy:
|
||||
name: Deploy shoc-backend to Elastic Beanstalk dev
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev')
|
||||
name: Deploy shoc-backend to Elastic Beanstalk
|
||||
if: >
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
contains(fromJSON('["refs/heads/dev","refs/heads/staging","refs/heads/main"]'), github.ref))
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
environment:
|
||||
name: dev
|
||||
name: ${{ github.ref_name == 'main' && 'prod' || github.ref_name }}
|
||||
concurrency:
|
||||
group: deploy-dev
|
||||
group: deploy-${{ github.ref_name == 'main' && 'prod' || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Resolve deploy target
|
||||
id: target
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${GITHUB_REF_NAME}" in
|
||||
dev)
|
||||
application=shoc-backend
|
||||
environment=shoc-backend-dev
|
||||
smoke_url=https://api.dev.seahaven.com
|
||||
;;
|
||||
staging)
|
||||
application=shoc-backend
|
||||
environment=shoc-backend-staging
|
||||
smoke_url=https://api.staging.seahaven.com
|
||||
;;
|
||||
main)
|
||||
application=shoc-backend
|
||||
environment=shoc-backend-prod
|
||||
smoke_url=https://api.seahaven.com
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported ref ${GITHUB_REF_NAME}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
{
|
||||
echo "application=${application}"
|
||||
echo "environment=${environment}"
|
||||
echo "smoke_url=${smoke_url}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
{
|
||||
echo "EB_APPLICATION_NAME=${application}"
|
||||
echo "EB_ENVIRONMENT_NAME=${environment}"
|
||||
echo "SMOKE_URL=${smoke_url}"
|
||||
} >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
|
|
@ -101,7 +154,7 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
prev="$(aws elasticbeanstalk describe-environments \
|
||||
--environment-names shoc-backend-dev \
|
||||
--environment-names "${EB_ENVIRONMENT_NAME}" \
|
||||
--region us-east-1 \
|
||||
--query 'Environments[0].VersionLabel' \
|
||||
--output text)"
|
||||
|
|
@ -112,8 +165,8 @@ jobs:
|
|||
uses: aws-actions/aws-elasticbeanstalk-deploy@7883cdd454c162051bf6fc13389536b045149b4c # v1.0.8
|
||||
with:
|
||||
aws-region: us-east-1
|
||||
application-name: shoc-backend
|
||||
environment-name: shoc-backend-dev
|
||||
application-name: ${{ steps.target.outputs.application }}
|
||||
environment-name: ${{ steps.target.outputs.environment }}
|
||||
version-label: ${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
deployment-package-path: .artifacts/elastic-beanstalk/site.zip
|
||||
s3-bucket-name: elasticbeanstalk-us-east-1-396287094661
|
||||
|
|
@ -135,7 +188,7 @@ jobs:
|
|||
for _ in $(seq 1 80); do
|
||||
read -r status current health < <(
|
||||
aws elasticbeanstalk describe-environments \
|
||||
--environment-names shoc-backend-dev \
|
||||
--environment-names "${EB_ENVIRONMENT_NAME}" \
|
||||
--region us-east-1 \
|
||||
--query 'Environments[0].[Status,VersionLabel,Health]' \
|
||||
--output text
|
||||
|
|
@ -157,7 +210,7 @@ jobs:
|
|||
exit 1
|
||||
|
||||
- name: Post-deploy smoke
|
||||
run: bash scripts/smoke-elastic-beanstalk.sh https://api.dev.seahaven.com
|
||||
run: bash scripts/smoke-elastic-beanstalk.sh "${SMOKE_URL}"
|
||||
|
||||
- name: Verify webhook secret source is operational
|
||||
run: |
|
||||
|
|
@ -173,7 +226,7 @@ jobs:
|
|||
--header 'X-SH-Key-Id: deployment-smoke-invalid-key' \
|
||||
--header "X-SH-Signature: v1=$(printf '0%.0s' {1..64})" \
|
||||
--data '{}' \
|
||||
https://api.dev.seahaven.com/api/webhooks/work-orders)"
|
||||
"${SMOKE_URL}/api/webhooks/work-orders")"
|
||||
if [ "$status" != "401" ]; then
|
||||
echo "Expected enabled webhook with an operational secret source to reject the invalid probe with 401; received $status." >&2
|
||||
sed -n '1,20p' "$response_file" >&2
|
||||
|
|
@ -202,7 +255,7 @@ jobs:
|
|||
for _ in $(seq 1 80); do
|
||||
read -r status current health < <(
|
||||
aws elasticbeanstalk describe-environments \
|
||||
--environment-names shoc-backend-dev \
|
||||
--environment-names "${EB_ENVIRONMENT_NAME}" \
|
||||
--region us-east-1 \
|
||||
--query 'Environments[0].[Status,VersionLabel,Health]' \
|
||||
--output text
|
||||
|
|
@ -223,10 +276,10 @@ jobs:
|
|||
exit 0
|
||||
fi
|
||||
|
||||
echo "Restoring shoc-backend-dev application code to version label: $prev"
|
||||
echo "Restoring ${EB_ENVIRONMENT_NAME} application code to version label: $prev"
|
||||
echo "Database migrations are not reverted; deployable migrations must follow the expand/contract policy."
|
||||
aws elasticbeanstalk update-environment \
|
||||
--environment-name shoc-backend-dev \
|
||||
--environment-name "${EB_ENVIRONMENT_NAME}" \
|
||||
--version-label "$prev" \
|
||||
--region us-east-1
|
||||
|
||||
|
|
@ -234,7 +287,7 @@ jobs:
|
|||
for _ in $(seq 1 80); do
|
||||
read -r status current health < <(
|
||||
aws elasticbeanstalk describe-environments \
|
||||
--environment-names shoc-backend-dev \
|
||||
--environment-names "${EB_ENVIRONMENT_NAME}" \
|
||||
--region us-east-1 \
|
||||
--query 'Environments[0].[Status,VersionLabel,Health]' \
|
||||
--output text
|
||||
|
|
|
|||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -374,3 +374,14 @@ infra/cdk/.cdk.staging/
|
|||
|
||||
# Deployment packaging artifacts
|
||||
.artifacts/
|
||||
|
||||
# Terraform (HCP remote state; never commit tfvars with secrets)
|
||||
**/.terraform/
|
||||
*.tfvars
|
||||
!*.tfvars.example
|
||||
crash.log
|
||||
crash.*.log
|
||||
override.tf
|
||||
override.tf.json
|
||||
*_override.tf
|
||||
*_override.tf.json
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
| G7 | Cancellation forwarding | §6 | behavior tests on changed I/O paths + analyzer | review-enforced on changed paths |
|
||||
| G8 | Error disclosure | §5 | `SanitizedErrorsTests` (part of G5) | `ci` |
|
||||
| G9 | Board-backed regression | review framework | `REVIEW_AND_PR_FRAMEWORK.md` inventory | review-enforced |
|
||||
| G10 | Terraform import plan safety | live infrastructure adoption | `python scripts/test-terraform-import-plan-check.py` | `architecture-quality` → `governance-check.sh` |
|
||||
|
||||
## How to run locally
|
||||
|
||||
|
|
@ -45,6 +46,8 @@ The script:
|
|||
changed C# files it skips G3 with an explicit "skipped: no changed C#" line.
|
||||
4. builds the complete solution in Release with no restore (G4).
|
||||
5. runs the complete solution test suite in Release with no rebuild (G5).
|
||||
6. verifies that the Terraform plan guard rejects create, delete, replacement,
|
||||
and unapproved update actions (G10).
|
||||
|
||||
## Migration gates (G6)
|
||||
|
||||
|
|
|
|||
74
scripts/check-terraform-import-plan.py
Normal file
74
scripts/check-terraform-import-plan.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Reject unsafe actions in a live Terraform import plan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ALLOWED_MANAGED_TYPES = {"aws_iam_role", "aws_iam_role_policy"}
|
||||
UNSAFE_ACTIONS = {"create", "delete"}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("plan_json", type=Path)
|
||||
parser.add_argument(
|
||||
"--allow-update",
|
||||
action="store_true",
|
||||
help="Allow in-place updates after the initial no-op import is proven.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
plan = json.loads(args.plan_json.read_text(encoding="utf-8"))
|
||||
violations: list[str] = []
|
||||
managed = 0
|
||||
updates = 0
|
||||
|
||||
for resource in plan.get("resource_changes", []):
|
||||
if resource.get("mode", "managed") != "managed":
|
||||
continue
|
||||
|
||||
resource_type = resource.get("type", "")
|
||||
address = resource.get("address", "<unknown>")
|
||||
actions = set(resource.get("change", {}).get("actions", []))
|
||||
managed += 1
|
||||
|
||||
if resource_type not in ALLOWED_MANAGED_TYPES:
|
||||
violations.append(
|
||||
f"{address}: managed type {resource_type!r} is outside the live ownership boundary"
|
||||
)
|
||||
|
||||
unsafe = sorted(actions & UNSAFE_ACTIONS)
|
||||
if unsafe:
|
||||
violations.append(f"{address}: unsafe actions {unsafe}")
|
||||
|
||||
if "update" in actions:
|
||||
updates += 1
|
||||
if not args.allow_update:
|
||||
violations.append(
|
||||
f"{address}: update is forbidden during the initial no-op import"
|
||||
)
|
||||
|
||||
if violations:
|
||||
print("FAIL: live Terraform plan is not import-safe", file=sys.stderr)
|
||||
for violation in violations:
|
||||
print(f" - {violation}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
mode = "controlled update" if args.allow_update else "no-op import"
|
||||
print(
|
||||
f"PASS: {mode} plan has {managed} managed resources, "
|
||||
f"{updates} updates, and no create/delete/replace actions"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
113
scripts/deploy-api-tf.sh
Normal file
113
scripts/deploy-api-tf.sh
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# deploy-api-tf.sh — local Elastic Beanstalk publish for the Terraform POC.
|
||||
# Refuses live names. GitHub Actions is the real CD path once the branch is
|
||||
# pushed; this script exists only because GHA cannot run until then.
|
||||
#
|
||||
# Usage:
|
||||
# export AWS_PROFILE=seahaven-external-dev
|
||||
# bash scripts/deploy-api-tf.sh
|
||||
set -euo pipefail
|
||||
|
||||
REGION="${AWS_REGION:-us-east-1}"
|
||||
APPLICATION_NAME="shoc-backend-tf-poc"
|
||||
ENVIRONMENT_NAME="shoc-backend-tf-poc"
|
||||
SMOKE_URL="https://tf-poc.api.dev.seahaven.com"
|
||||
S3_BUCKET="elasticbeanstalk-us-east-1-396287094661"
|
||||
|
||||
log() { printf '\n\033[1m== %s ==\033[0m\n' "$1"; }
|
||||
die() { printf '\033[31mERR\033[0m %s\n' "$1" >&2; exit 1; }
|
||||
|
||||
[[ "${APPLICATION_NAME}" != "shoc-backend" ]] \
|
||||
|| die "refusing live Elastic Beanstalk application shoc-backend"
|
||||
[[ "${ENVIRONMENT_NAME}" != "shoc-backend-dev" ]] \
|
||||
|| die "refusing live Elastic Beanstalk environment shoc-backend-dev"
|
||||
[[ "${SMOKE_URL}" != "https://api.dev.seahaven.com" ]] \
|
||||
|| die "refusing live hostname api.dev.seahaven.com"
|
||||
|
||||
command -v aws >/dev/null 2>&1 || die "aws CLI is required"
|
||||
command -v curl >/dev/null 2>&1 || die "curl is required"
|
||||
|
||||
ACCOUNT="$(aws sts get-caller-identity --query Account --output text)"
|
||||
[[ "${ACCOUNT}" == "396287094661" ]] \
|
||||
|| die "refusing to deploy outside seahaven-external-dev (caller account ${ACCOUNT})"
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
log "package source bundle"
|
||||
bash scripts/package-elastic-beanstalk.sh
|
||||
|
||||
VERSION_LABEL="local-$(date -u +%Y%m%d%H%M%S)-${USER:-unknown}"
|
||||
BUNDLE=".artifacts/elastic-beanstalk/site.zip"
|
||||
KEY="shoc-backend-tf-poc/${VERSION_LABEL}.zip"
|
||||
|
||||
log "capture current environment version"
|
||||
prev="$(aws elasticbeanstalk describe-environments \
|
||||
--environment-names "${ENVIRONMENT_NAME}" \
|
||||
--region "${REGION}" \
|
||||
--query 'Environments[0].VersionLabel' \
|
||||
--output text)"
|
||||
echo "${prev}" > .artifacts/elastic-beanstalk/previous-version.txt
|
||||
echo "Previous version label: ${prev}"
|
||||
|
||||
log "upload bundle ${KEY}"
|
||||
aws s3 cp "${BUNDLE}" "s3://${S3_BUCKET}/${KEY}" --region "${REGION}"
|
||||
|
||||
log "create application version ${VERSION_LABEL}"
|
||||
aws elasticbeanstalk create-application-version \
|
||||
--application-name "${APPLICATION_NAME}" \
|
||||
--version-label "${VERSION_LABEL}" \
|
||||
--source-bundle "S3Bucket=${S3_BUCKET},S3Key=${KEY}" \
|
||||
--region "${REGION}"
|
||||
|
||||
log "update environment ${ENVIRONMENT_NAME}"
|
||||
aws elasticbeanstalk update-environment \
|
||||
--environment-name "${ENVIRONMENT_NAME}" \
|
||||
--version-label "${VERSION_LABEL}" \
|
||||
--region "${REGION}"
|
||||
|
||||
log "wait until expected version is Ready"
|
||||
status="Unknown"
|
||||
current="Unknown"
|
||||
health="Unknown"
|
||||
for _ in $(seq 1 80); do
|
||||
read -r status current health < <(
|
||||
aws elasticbeanstalk describe-environments \
|
||||
--environment-names "${ENVIRONMENT_NAME}" \
|
||||
--region "${REGION}" \
|
||||
--query 'Environments[0].[Status,VersionLabel,Health]' \
|
||||
--output text
|
||||
)
|
||||
echo "environment status: ${status}; version: ${current}; health: ${health}"
|
||||
if [ "${status}" = "Ready" ]; then
|
||||
if [ "${current}" = "${VERSION_LABEL}" ] && { [ "${health}" = "Green" ] || [ "${health}" = "Yellow" ]; }; then
|
||||
echo "Expected application version is Ready and healthy."
|
||||
break
|
||||
fi
|
||||
die "Environment became Ready without activating expected version ${VERSION_LABEL}."
|
||||
fi
|
||||
sleep 15
|
||||
done
|
||||
[[ "${status}" = "Ready" ]] || die "Expected application version did not become Ready."
|
||||
|
||||
log "smoke ${SMOKE_URL}"
|
||||
bash scripts/smoke-elastic-beanstalk.sh "${SMOKE_URL}"
|
||||
|
||||
log "webhook secret source"
|
||||
response_file="$(mktemp)"
|
||||
trap 'rm -f "$response_file"' EXIT
|
||||
status_code="$(curl --silent --show-error \
|
||||
--output "$response_file" \
|
||||
--write-out '%{http_code}' \
|
||||
--request POST \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header "X-SH-Timestamp: $(date +%s)" \
|
||||
--header 'X-SH-Key-Id: deployment-smoke-invalid-key' \
|
||||
--header "X-SH-Signature: v1=$(printf '0%.0s' {1..64})" \
|
||||
--data '{}' \
|
||||
"${SMOKE_URL}/api/webhooks/work-orders")"
|
||||
if [ "${status_code}" != "401" ]; then
|
||||
die "Expected enabled webhook to reject the invalid probe with 401; received ${status_code}."
|
||||
fi
|
||||
echo "webhook HTTP 401"
|
||||
|
|
@ -79,4 +79,8 @@ log "G5: full test suite"
|
|||
"$DOTNET" test "$SOLUTION" -c Release --no-build --nologo
|
||||
ok "G5: full test suite"
|
||||
|
||||
log "G10: Terraform import plan safety"
|
||||
python scripts/test-terraform-import-plan-check.py
|
||||
ok "G10: Terraform import plan safety"
|
||||
|
||||
log "governance-check: all required repository gates passed"
|
||||
|
|
|
|||
69
scripts/test-terraform-import-plan-check.py
Normal file
69
scripts/test-terraform-import-plan-check.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Small deterministic tests for check-terraform-import-plan.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).with_name("check-terraform-import-plan.py")
|
||||
|
||||
|
||||
def run_case(actions: list[str], *, allow_update: bool = False) -> subprocess.CompletedProcess[str]:
|
||||
plan = {
|
||||
"resource_changes": [
|
||||
{
|
||||
"address": "module.deploy_role.aws_iam_role.github_deploy",
|
||||
"mode": "managed",
|
||||
"type": "aws_iam_role",
|
||||
"change": {"actions": actions},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
plan_path = Path(directory) / "plan.json"
|
||||
plan_path.write_text(json.dumps(plan), encoding="utf-8")
|
||||
command = [sys.executable, str(SCRIPT), str(plan_path)]
|
||||
if allow_update:
|
||||
command.append("--allow-update")
|
||||
return subprocess.run(command, check=False, capture_output=True, text=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
cases = [
|
||||
("no-op import", run_case(["no-op"]), 0),
|
||||
("initial update", run_case(["update"]), 1),
|
||||
("controlled update", run_case(["update"], allow_update=True), 0),
|
||||
("create", run_case(["create"]), 1),
|
||||
("replacement", run_case(["delete", "create"]), 1),
|
||||
("destroy", run_case(["delete"]), 1),
|
||||
]
|
||||
failures = [
|
||||
(name, result, expected)
|
||||
for name, result, expected in cases
|
||||
if result.returncode != expected
|
||||
]
|
||||
if failures:
|
||||
print(
|
||||
"FAIL: plan-check cases failed: "
|
||||
+ ", ".join(name for name, _, _ in failures),
|
||||
file=sys.stderr,
|
||||
)
|
||||
for name, result, expected in failures:
|
||||
print(
|
||||
f"{name}: expected {expected}, got {result.returncode}\n"
|
||||
f"{result.stdout}{result.stderr}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
print("PASS: Terraform import plan safety checks")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
53
terraform/.terraform.lock.hcl
generated
Normal file
53
terraform/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "6.62.0"
|
||||
constraints = "~> 6.57"
|
||||
hashes = [
|
||||
"h1:4qcuRkosNKYxV2y69uJ6zAfTEO1Op04L4KUuWBrUvBo=",
|
||||
"h1:OthB9UeoBgmy348EpDjs5GDGk6p6UxAMQD5cXn7u9Ho=",
|
||||
"h1:lTKd2c1EunGxt2XROLgEeSXA2Jk+WiiG9BTcp+L/0xY=",
|
||||
"h1:nWSI/kgPk9aieiY01TEKOGXRX3+L889GSkEq0SMCL6E=",
|
||||
"zh:35a9e4bc6fd622c5a99561b882025f2745f1256bbf1a8da8d6b39319b75ae0b5",
|
||||
"zh:405927d470ff16201e40aa0fa2d0ab1de477360a0926d20719cd029179682ecd",
|
||||
"zh:4ab7866593a90bcf18f066b0092a209b9f42852acd783b504031ae74cb6f7010",
|
||||
"zh:5b477f313fc511648a4eed9f9085d0778414835896256ab14296d2345b7070e3",
|
||||
"zh:87de70bc99751f94262cec2260d972555a98f588aa3a613e417438f88a1182df",
|
||||
"zh:88f02a8ff07f00da4ffb3bee9e8ae25588e3a0a92633c625c1c2a63bac00a844",
|
||||
"zh:8d8596257453357c9f3fccaa7d2f04299e8d35b16f364adb8a2c829143a9c090",
|
||||
"zh:953c8e15fa9c12c081f17d66cf45246d032fa23bee33f264dc82242afdb98bc2",
|
||||
"zh:9a7dd903e5e9b2b0cc1317ad2d2692e0ddf05ae2a5aaee20ec5dd1db456711b7",
|
||||
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||
"zh:a859154c75c1088d098a481f1ebb259720c5a2ad87781364abf556a741e5adb7",
|
||||
"zh:b8d1e72ad39d5864118f64dd3273424ab637d34b3ff8dd3dfeb4aef9d458587f",
|
||||
"zh:c3666fcfc7b131f5282d4e7249fa68c3a21665757888aa02bbaf6be1cd036bba",
|
||||
"zh:c6b8ff94b3f49bf85fc087381cfe1b271c5b01cf74cf140d58aa500be7138913",
|
||||
"zh:d8143d790e9dd77b8e2f9168e4a33ad6d064dc4b082a0196636b182105aaed14",
|
||||
"zh:fa41eca042f377eb2741e95b36609c1de5b0cd675cd4e3e30c709497cb94db02",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/random" {
|
||||
version = "3.9.0"
|
||||
constraints = "~> 3.6"
|
||||
hashes = [
|
||||
"h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=",
|
||||
"h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=",
|
||||
"h1:o0s5Mk9NXMP60nlheO1r0LsDGGratFb3oL0t7bD2QnM=",
|
||||
"h1:q/uaUTBdKgAmZESrwsoeDQff9uUA/cI/N5ZKNgVwa9c=",
|
||||
"zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1",
|
||||
"zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea",
|
||||
"zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f",
|
||||
"zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0",
|
||||
"zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61",
|
||||
"zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc",
|
||||
"zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e",
|
||||
"zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef",
|
||||
"zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b",
|
||||
"zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257",
|
||||
"zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04",
|
||||
]
|
||||
}
|
||||
170
terraform/README.md
Normal file
170
terraform/README.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Terraform deployment infrastructure
|
||||
|
||||
The root module remains the isolated `tf-poc` Elastic Beanstalk and SQL Server
|
||||
stack in `seahaven-external-dev` (`396287094661`). It must never be reused for
|
||||
dev or staging state.
|
||||
|
||||
Live adoption is deliberately smaller. [`live/`](live/) imports only the
|
||||
existing GitHub Actions deploy roles and inline policies. All application,
|
||||
environment, database, certificate, DNS, network, runtime IAM, and secret
|
||||
resources remain external and are read only as inventory.
|
||||
|
||||
The Terraform roots are:
|
||||
|
||||
- `./`: isolated POC workload in `shoc-backend-tf-poc`.
|
||||
- `bootstrap/`: consolidated POC/dev/staging HCP role bootstrap in
|
||||
`shoc-backend-bootstrap`.
|
||||
- `live/dev/`: import-only dev deploy-role ownership in
|
||||
`shoc-backend-dev`.
|
||||
- `live/staging/`: import-only staging deploy-role ownership in
|
||||
`shoc-backend-staging`.
|
||||
|
||||
IAM lives in this repo, not org-baseline. App CD never runs a bootstrap root.
|
||||
Auto-apply stays off for every workspace.
|
||||
|
||||
The stabilized POC bootstrap deliberately leaves both POC HCP roles read only
|
||||
and removes `ViewOnlyAccess` plus the broad workload-mutation inline policy.
|
||||
The POC GitHub deploy role remains unchanged until a separately approved
|
||||
narrowing or teardown. Future POC teardown runs directly under the approved
|
||||
SSO administrator session, not the locked HCP apply role.
|
||||
|
||||
## Locked names
|
||||
|
||||
- Hostname: `tf-poc.api.dev.seahaven.com` (zone `Z07671212N75U4YLPWZR8`)
|
||||
- EB application / environment: `shoc-backend-tf-poc`
|
||||
- RDS identifier: `shoc-backend-tf-poc`
|
||||
- Catalog: `shoc_tf_poc` (create this database once after RDS is available)
|
||||
- Deploy role: `arn:aws:iam::396287094661:role/tf-managed/githubdeploy-shoc-backend-tf-poc`
|
||||
- GitHub Environment: `tf-poc` (OIDC `environment:tf-poc`)
|
||||
- HCP org `seahaven`, project `seahaven-external-dev`
|
||||
|
||||
## HCP layout
|
||||
|
||||
All SHOC backend environments live in AWS account `396287094661` and HCP
|
||||
|
||||
The POC workspaces remain isolated until teardown. The live bootstrap owns
|
||||
`hcptf-shoc-backend-{dev,staging}` and matching `-plan` roles. The live
|
||||
environment workspaces own only their existing GitHub deploy role and inline
|
||||
policy. See [`live/README.md`](live/README.md).
|
||||
|
||||
## Console setup (once)
|
||||
|
||||
1. In HCP Terraform, create project `seahaven-external-dev` if it does not exist.
|
||||
2. Create workspace `shoc-backend-bootstrap`:
|
||||
- VCS later, or CLI-driven until the branch is pushed
|
||||
- Working directory: `terraform/bootstrap`
|
||||
- Execution mode: **Local**
|
||||
- Auto-apply: off
|
||||
3. Create workspace `shoc-backend-tf-poc`:
|
||||
- Same branch
|
||||
- Working directory: `terraform`
|
||||
- Execution mode: **Remote**
|
||||
- Auto-apply: off
|
||||
- Speculative plans: on
|
||||
4. Do **not** use HCP "Quick setup AWS dynamic credentials".
|
||||
|
||||
## Discovery (before first workload apply)
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=seahaven-external-dev
|
||||
|
||||
aws elasticbeanstalk describe-environments \
|
||||
--environment-names shoc-backend-dev \
|
||||
--region us-east-1 \
|
||||
--query 'Environments[0].{Vpc:EndpointURL}'
|
||||
|
||||
aws elasticbeanstalk describe-configuration-settings \
|
||||
--application-name shoc-backend \
|
||||
--environment-name shoc-backend-dev \
|
||||
--region us-east-1 \
|
||||
--query "ConfigurationSettings[0].OptionSettings[?Namespace=='aws:ec2:vpc']"
|
||||
```
|
||||
|
||||
Copy `vpc-REPLACE_ME` and subnet lists into a local `terraform/terraform.tfvars`
|
||||
(gitignored). Confirm:
|
||||
|
||||
- `app.terraform.io` OIDC exists (`create_tfc_oidc_provider=false` in bootstrap).
|
||||
If the data source fails, set `create_tfc_oidc_provider=true`.
|
||||
- `external-dev-execution-boundary` exists.
|
||||
- `aws-elasticbeanstalk-service-role` exists.
|
||||
- GitHub OIDC provider `token.actions.githubusercontent.com` exists.
|
||||
|
||||
## Bootstrap apply (Adam)
|
||||
|
||||
SSO AdministratorAccess in this account is subject to the external-dev SCP, so
|
||||
both `hcptf-*` roles set `permissions_boundary` to
|
||||
`external-dev-execution-boundary`.
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=seahaven-external-dev
|
||||
cd terraform/bootstrap
|
||||
terraform login
|
||||
terraform init
|
||||
terraform apply
|
||||
```
|
||||
|
||||
Then on workspace `shoc-backend-tf-poc`, set workspace-scoped env vars:
|
||||
|
||||
- `TFC_AWS_PROVIDER_AUTH=true`
|
||||
- `TFC_AWS_PLAN_ROLE_ARN` = output `hcp_plan_role_arn`
|
||||
- `TFC_AWS_APPLY_ROLE_ARN` = output `hcp_apply_role_arn`
|
||||
|
||||
Never put those in a project variable set. Set `sendgrid_api_key` as a
|
||||
sensitive Terraform variable on that workspace when you want mail to work.
|
||||
|
||||
## Workload apply
|
||||
|
||||
HCP Manual apply on `shoc-backend-tf-poc`. Confirm `api.dev.seahaven.com` still
|
||||
serves live before and after.
|
||||
|
||||
After RDS is available, create the catalog once (SQL Server Express does not
|
||||
accept `db_name` on `aws_db_instance`):
|
||||
|
||||
```sql
|
||||
CREATE DATABASE [shoc_tf_poc];
|
||||
```
|
||||
|
||||
Then first app deploy can run migrations into that catalog.
|
||||
|
||||
## App deploy
|
||||
|
||||
GitHub Actions is the real CD path. `.github/workflows/deploy.yml` maps:
|
||||
|
||||
- `dev` → `dev`
|
||||
- `staging` → `staging`
|
||||
- `main` → `prod`
|
||||
|
||||
The live roots import the roles already referenced by the `dev` and `staging`
|
||||
GitHub Environment secrets. The role ARNs do not change during adoption.
|
||||
|
||||
The POC local fallback remains available until teardown:
|
||||
|
||||
```bash
|
||||
export AWS_PROFILE=seahaven-external-dev
|
||||
bash scripts/deploy-api-tf.sh
|
||||
```
|
||||
|
||||
The script refuses live names (`shoc-backend-dev`, `api.dev.seahaven.com`).
|
||||
|
||||
## After POC confirmation
|
||||
|
||||
1. Create `shoc-backend-dev` and `shoc-backend-staging` workspaces. Do not
|
||||
reuse POC state.
|
||||
2. Apply `bootstrap/` only after approval to create the four narrowly scoped
|
||||
live HCP roles.
|
||||
3. Follow the no-op import and controlled-update sequence in
|
||||
[`live/README.md`](live/README.md).
|
||||
4. Retire CDK deploy-role ownership only after both role imports are proven.
|
||||
5. Destroy the POC workload last. Bootstrap state remains.
|
||||
|
||||
Do not apply this module as `environment=dev` without imports.
|
||||
|
||||
## Local CI equivalent
|
||||
|
||||
```bash
|
||||
terraform -chdir=terraform fmt -check -recursive
|
||||
terraform -chdir=terraform init -backend=false && terraform -chdir=terraform validate
|
||||
terraform -chdir=terraform/bootstrap init -backend=false && terraform -chdir=terraform/bootstrap validate
|
||||
terraform -chdir=terraform/live/dev init -backend=false && terraform -chdir=terraform/live/dev validate
|
||||
terraform -chdir=terraform/live/staging init -backend=false && terraform -chdir=terraform/live/staging validate
|
||||
```
|
||||
37
terraform/acm.tf
Normal file
37
terraform/acm.tf
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
resource "aws_acm_certificate" "api" {
|
||||
domain_name = var.domain_name
|
||||
validation_method = "DNS"
|
||||
|
||||
tags = {
|
||||
Name = var.domain_name
|
||||
Project = "shoc-backend"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "acm_validation" {
|
||||
for_each = {
|
||||
for dvo in aws_acm_certificate.api.domain_validation_options : dvo.domain_name => {
|
||||
name = dvo.resource_record_name
|
||||
record = dvo.resource_record_value
|
||||
type = dvo.resource_record_type
|
||||
}
|
||||
}
|
||||
|
||||
allow_overwrite = true
|
||||
name = each.value.name
|
||||
records = [each.value.record]
|
||||
ttl = 60
|
||||
type = each.value.type
|
||||
zone_id = var.hosted_zone_id
|
||||
}
|
||||
|
||||
resource "aws_acm_certificate_validation" "api" {
|
||||
certificate_arn = aws_acm_certificate.api.arn
|
||||
validation_record_fqdns = [for record in aws_route53_record.acm_validation : record.fqdn]
|
||||
}
|
||||
29
terraform/bootstrap/.terraform.lock.hcl
generated
Normal file
29
terraform/bootstrap/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "6.62.0"
|
||||
constraints = "~> 6.57"
|
||||
hashes = [
|
||||
"h1:4qcuRkosNKYxV2y69uJ6zAfTEO1Op04L4KUuWBrUvBo=",
|
||||
"h1:OthB9UeoBgmy348EpDjs5GDGk6p6UxAMQD5cXn7u9Ho=",
|
||||
"h1:lTKd2c1EunGxt2XROLgEeSXA2Jk+WiiG9BTcp+L/0xY=",
|
||||
"h1:nWSI/kgPk9aieiY01TEKOGXRX3+L889GSkEq0SMCL6E=",
|
||||
"zh:35a9e4bc6fd622c5a99561b882025f2745f1256bbf1a8da8d6b39319b75ae0b5",
|
||||
"zh:405927d470ff16201e40aa0fa2d0ab1de477360a0926d20719cd029179682ecd",
|
||||
"zh:4ab7866593a90bcf18f066b0092a209b9f42852acd783b504031ae74cb6f7010",
|
||||
"zh:5b477f313fc511648a4eed9f9085d0778414835896256ab14296d2345b7070e3",
|
||||
"zh:87de70bc99751f94262cec2260d972555a98f588aa3a613e417438f88a1182df",
|
||||
"zh:88f02a8ff07f00da4ffb3bee9e8ae25588e3a0a92633c625c1c2a63bac00a844",
|
||||
"zh:8d8596257453357c9f3fccaa7d2f04299e8d35b16f364adb8a2c829143a9c090",
|
||||
"zh:953c8e15fa9c12c081f17d66cf45246d032fa23bee33f264dc82242afdb98bc2",
|
||||
"zh:9a7dd903e5e9b2b0cc1317ad2d2692e0ddf05ae2a5aaee20ec5dd1db456711b7",
|
||||
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||
"zh:a859154c75c1088d098a481f1ebb259720c5a2ad87781364abf556a741e5adb7",
|
||||
"zh:b8d1e72ad39d5864118f64dd3273424ab637d34b3ff8dd3dfeb4aef9d458587f",
|
||||
"zh:c3666fcfc7b131f5282d4e7249fa68c3a21665757888aa02bbaf6be1cd036bba",
|
||||
"zh:c6b8ff94b3f49bf85fc087381cfe1b271c5b01cf74cf140d58aa500be7138913",
|
||||
"zh:d8143d790e9dd77b8e2f9168e4a33ad6d064dc4b082a0196636b182105aaed14",
|
||||
"zh:fa41eca042f377eb2741e95b36609c1de5b0cd675cd4e3e30c709497cb94db02",
|
||||
]
|
||||
}
|
||||
228
terraform/bootstrap/iam.tf
Normal file
228
terraform/bootstrap/iam.tf
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
# Account-global HCP Terraform OIDC provider. An account may hold exactly one
|
||||
# provider per URL. Default is data-source because the frontend stack owns it.
|
||||
resource "aws_iam_openid_connect_provider" "terraform_cloud" {
|
||||
count = var.create_tfc_oidc_provider ? 1 : 0
|
||||
|
||||
url = "https://app.terraform.io"
|
||||
client_id_list = ["aws.workload.identity"]
|
||||
thumbprint_list = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"]
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "hcp_plan_assume" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRoleWithWebIdentity"]
|
||||
|
||||
principals {
|
||||
type = "Federated"
|
||||
identifiers = [local.tfc_oidc_arn]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:aud"
|
||||
values = ["aws.workload.identity"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:sub"
|
||||
values = ["${local.hcp_sub_prefix}:plan"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "hcp_apply_assume" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRoleWithWebIdentity"]
|
||||
|
||||
principals {
|
||||
type = "Federated"
|
||||
identifiers = [local.tfc_oidc_arn]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:aud"
|
||||
values = ["aws.workload.identity"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:sub"
|
||||
values = ["${local.hcp_sub_prefix}:apply"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "hcp_refresh" {
|
||||
statement {
|
||||
sid = "RefreshManagedIam"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:GetInstanceProfile",
|
||||
"iam:GetRole",
|
||||
"iam:GetRolePolicy",
|
||||
"iam:ListAttachedRolePolicies",
|
||||
"iam:ListInstanceProfilesForRole",
|
||||
"iam:ListRolePolicies",
|
||||
"iam:ListRoleTags",
|
||||
]
|
||||
resources = [
|
||||
local.github_deploy_role_arn,
|
||||
local.eb_ec2_role_arn,
|
||||
local.eb_service_role_arn,
|
||||
"arn:aws:iam::${local.account_id}:instance-profile/tf-managed/${var.eb_ec2_role_name}",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ListOidcProviders"
|
||||
effect = "Allow"
|
||||
actions = ["iam:ListOpenIDConnectProviders"]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ReadGithubOidcProvider"
|
||||
effect = "Allow"
|
||||
actions = ["iam:GetOpenIDConnectProvider"]
|
||||
resources = [local.github_oidc_arn]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshRds"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"rds:DescribeDBInstances",
|
||||
"rds:DescribeDBParameterGroups",
|
||||
"rds:DescribeDBSubnetGroups",
|
||||
"rds:ListTagsForResource",
|
||||
]
|
||||
# RDS describe APIs do not support resource-level permissions.
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshSecrets"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"secretsmanager:DescribeSecret",
|
||||
"secretsmanager:GetResourcePolicy",
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:ListSecretVersionIds",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:shoc-backend-tf-poc/jwt-JXLaUx",
|
||||
"arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:shoc-backend-tf-poc/webhook-hmac-eThZhu",
|
||||
"arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:rds!db-6e0e2e34-dea1-47b0-8e92-b90bde9cfe20-Mxeu3J",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ListCertificates"
|
||||
effect = "Allow"
|
||||
actions = ["acm:ListCertificates"]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshElasticBeanstalk"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"elasticbeanstalk:DescribeApplications",
|
||||
"elasticbeanstalk:DescribeConfigurationOptions",
|
||||
"elasticbeanstalk:DescribeConfigurationSettings",
|
||||
"elasticbeanstalk:DescribeEnvironmentResources",
|
||||
"elasticbeanstalk:DescribeEnvironments",
|
||||
"elasticbeanstalk:ListTagsForResource",
|
||||
]
|
||||
# Elastic Beanstalk describe APIs do not support resource-level permissions.
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshPocCertificate"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"acm:DescribeCertificate",
|
||||
"acm:GetCertificate",
|
||||
"acm:ListTagsForCertificate",
|
||||
]
|
||||
resources = ["arn:aws:acm:${var.aws_region}:${local.account_id}:certificate/4fcc2dff-bb11-4204-9107-86d6ce2b95a2"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshRoute53"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"route53:GetChange",
|
||||
"route53:GetHostedZone",
|
||||
"route53:ListResourceRecordSets",
|
||||
"route53:ListTagsForResource",
|
||||
]
|
||||
resources = [
|
||||
local.hosted_zone_arn,
|
||||
"arn:aws:route53:::change/*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "RefreshNetwork"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"ec2:DescribeSecurityGroupRules",
|
||||
"ec2:DescribeSecurityGroups",
|
||||
"ec2:DescribeSubnets",
|
||||
"ec2:DescribeTags",
|
||||
"ec2:DescribeVpcs",
|
||||
]
|
||||
# EC2 describe APIs do not support resource-level permissions.
|
||||
resources = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "hcp_plan" {
|
||||
name = "hcptf-shoc-backend-tf-poc-plan"
|
||||
description = "HCP Terraform PLAN role for shoc-backend-tf-poc"
|
||||
assume_role_policy = data.aws_iam_policy_document.hcp_plan_assume.json
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.execution_boundary_arn
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "hcp_plan_refresh" {
|
||||
name = "shoc-backend-tf-poc-plan-refresh"
|
||||
role = aws_iam_role.hcp_plan.id
|
||||
policy = data.aws_iam_policy_document.hcp_refresh.json
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "hcp_apply" {
|
||||
name = "hcptf-shoc-backend-tf-poc"
|
||||
description = "Read-only HCP Terraform APPLY role for the stabilized shoc-backend-tf-poc"
|
||||
assume_role_policy = data.aws_iam_policy_document.hcp_apply_assume.json
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.execution_boundary_arn
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "hcp_apply_iam" {
|
||||
name = "shoc-backend-tf-poc-iam"
|
||||
role = aws_iam_role.hcp_apply.id
|
||||
policy = data.aws_iam_policy_document.hcp_refresh.json
|
||||
}
|
||||
215
terraform/bootstrap/live_workspace_roles.tf
Normal file
215
terraform/bootstrap/live_workspace_roles.tf
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
locals {
|
||||
live_certificate_arn = "arn:aws:acm:${var.aws_region}:${local.account_id}:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"
|
||||
live_github_oidc_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com"
|
||||
live_rds_arn = "arn:aws:rds:${var.aws_region}:${local.account_id}:db:shoc-sqlserver-shared"
|
||||
|
||||
live_environments = {
|
||||
dev = {
|
||||
workspace = "shoc-backend-dev"
|
||||
deploy_role_name = "githubdeploy-shoc-backend-dev"
|
||||
runtime_role_name = "shoc-backend-dev"
|
||||
instance_profile_name = "shoc-backend-dev"
|
||||
hosted_zone_id = "Z07671212N75U4YLPWZR8"
|
||||
}
|
||||
staging = {
|
||||
workspace = "shoc-backend-staging"
|
||||
deploy_role_name = "githubdeploy-shoc-backend-staging"
|
||||
runtime_role_name = "shoc-backend-staging"
|
||||
instance_profile_name = "shoc-backend-staging"
|
||||
hosted_zone_id = "Z02602739VQWBWCAGXP4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "live_plan_assume" {
|
||||
for_each = local.live_environments
|
||||
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRoleWithWebIdentity"]
|
||||
|
||||
principals {
|
||||
type = "Federated"
|
||||
identifiers = [local.tfc_oidc_arn]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:aud"
|
||||
values = ["aws.workload.identity"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:sub"
|
||||
values = ["organization:${var.hcp_organization}:project:${var.hcp_project}:workspace:${each.value.workspace}:run_phase:plan"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "live_apply_assume" {
|
||||
for_each = local.live_environments
|
||||
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRoleWithWebIdentity"]
|
||||
|
||||
principals {
|
||||
type = "Federated"
|
||||
identifiers = [local.tfc_oidc_arn]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:aud"
|
||||
values = ["aws.workload.identity"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "app.terraform.io:sub"
|
||||
values = ["organization:${var.hcp_organization}:project:${var.hcp_project}:workspace:${each.value.workspace}:run_phase:apply"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "live_plan" {
|
||||
for_each = local.live_environments
|
||||
|
||||
statement {
|
||||
sid = "CallerIdentity"
|
||||
effect = "Allow"
|
||||
actions = ["sts:GetCallerIdentity"]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ReadExactIamResources"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:GetInstanceProfile",
|
||||
"iam:GetRole",
|
||||
"iam:GetRolePolicy",
|
||||
"iam:ListAttachedRolePolicies",
|
||||
"iam:ListInstanceProfileTags",
|
||||
"iam:ListInstanceProfilesForRole",
|
||||
"iam:ListRolePolicies",
|
||||
"iam:ListRoleTags",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:iam::${local.account_id}:role/${each.value.deploy_role_name}",
|
||||
"arn:aws:iam::${local.account_id}:role/${each.value.runtime_role_name}",
|
||||
"arn:aws:iam::${local.account_id}:instance-profile/${each.value.instance_profile_name}",
|
||||
local.eb_service_role_arn,
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ReadGithubOidc"
|
||||
effect = "Allow"
|
||||
actions = ["iam:GetOpenIDConnectProvider"]
|
||||
resources = [local.live_github_oidc_arn]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ReadSharedInventory"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"acm:ListCertificates",
|
||||
"ec2:DescribeSecurityGroups",
|
||||
"ec2:DescribeSubnets",
|
||||
"ec2:DescribeVpcs",
|
||||
"iam:ListOpenIDConnectProviders",
|
||||
"rds:DescribeDBInstances",
|
||||
"route53:ListHostedZonesByName",
|
||||
]
|
||||
# These AWS read APIs do not support resource-level permissions.
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ReadPinnedCertificate"
|
||||
effect = "Allow"
|
||||
actions = ["acm:DescribeCertificate", "acm:ListTagsForCertificate"]
|
||||
resources = [local.live_certificate_arn]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ReadSharedRdsTags"
|
||||
effect = "Allow"
|
||||
actions = ["rds:ListTagsForResource"]
|
||||
resources = [local.live_rds_arn]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "ReadPinnedHostedZone"
|
||||
effect = "Allow"
|
||||
actions = ["route53:GetHostedZone", "route53:ListResourceRecordSets", "route53:ListTagsForResource"]
|
||||
resources = ["arn:aws:route53:::hostedzone/${each.value.hosted_zone_id}"]
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "live_apply" {
|
||||
for_each = local.live_environments
|
||||
source_policy_documents = [data.aws_iam_policy_document.live_plan[each.key].json]
|
||||
|
||||
statement {
|
||||
sid = "UpdateImportedDeployRole"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"iam:PutRolePolicy",
|
||||
"iam:TagRole",
|
||||
"iam:UntagRole",
|
||||
"iam:UpdateRole",
|
||||
]
|
||||
resources = ["arn:aws:iam::${local.account_id}:role/${each.value.deploy_role_name}"]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "live_plan" {
|
||||
for_each = local.live_environments
|
||||
|
||||
name = "hcptf-shoc-backend-${each.key}-plan"
|
||||
description = "Import/read-only HCP Terraform plan role for shoc-backend ${each.key}."
|
||||
assume_role_policy = data.aws_iam_policy_document.live_plan_assume[each.key].json
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.execution_boundary_arn
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "live_plan" {
|
||||
for_each = local.live_environments
|
||||
|
||||
name = "shoc-backend-${each.key}-import-plan"
|
||||
role = aws_iam_role.live_plan[each.key].id
|
||||
policy = data.aws_iam_policy_document.live_plan[each.key].json
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "live_apply" {
|
||||
for_each = local.live_environments
|
||||
|
||||
name = "hcptf-shoc-backend-${each.key}"
|
||||
description = "Import/update-only HCP Terraform apply role for shoc-backend ${each.key}."
|
||||
assume_role_policy = data.aws_iam_policy_document.live_apply_assume[each.key].json
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.execution_boundary_arn
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "live_apply" {
|
||||
for_each = local.live_environments
|
||||
|
||||
name = "shoc-backend-${each.key}-import-apply"
|
||||
role = aws_iam_role.live_apply[each.key].id
|
||||
policy = data.aws_iam_policy_document.live_apply[each.key].json
|
||||
}
|
||||
43
terraform/bootstrap/locals.tf
Normal file
43
terraform/bootstrap/locals.tf
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
data "aws_caller_identity" "current" {}
|
||||
|
||||
data "aws_iam_openid_connect_provider" "terraform_cloud" {
|
||||
count = var.create_tfc_oidc_provider ? 0 : 1
|
||||
url = "https://app.terraform.io"
|
||||
}
|
||||
|
||||
locals {
|
||||
account_id = data.aws_caller_identity.current.account_id
|
||||
|
||||
hcp_sub_prefix = "organization:${var.hcp_organization}:project:${var.hcp_project}:workspace:${var.hcp_workload_workspace}:run_phase"
|
||||
|
||||
tfc_oidc_arn = var.create_tfc_oidc_provider ? aws_iam_openid_connect_provider.terraform_cloud[0].arn : data.aws_iam_openid_connect_provider.terraform_cloud[0].arn
|
||||
|
||||
github_deploy_role_arn = "arn:aws:iam::${local.account_id}:role/tf-managed/${var.github_deploy_role_name}"
|
||||
eb_ec2_role_arn = "arn:aws:iam::${local.account_id}:role/tf-managed/${var.eb_ec2_role_name}"
|
||||
github_oidc_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com"
|
||||
hosted_zone_arn = "arn:aws:route53:::hostedzone/${var.hosted_zone_id}"
|
||||
eb_service_role_arn = "arn:aws:iam::${local.account_id}:role/${var.eb_service_role_name}"
|
||||
|
||||
application_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${local.account_id}:application/${var.eb_application_name}"
|
||||
environment_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${local.account_id}:environment/${var.eb_application_name}/${var.eb_environment_name}"
|
||||
rds_arn = "arn:aws:rds:${var.aws_region}:${local.account_id}:db:${var.rds_identifier}"
|
||||
|
||||
live_protected_role_arns = [
|
||||
"arn:aws:iam::${local.account_id}:role/githubdeploy-shoc-backend-dev",
|
||||
"arn:aws:iam::${local.account_id}:role/cdk-hnb659fds-*",
|
||||
"arn:aws:iam::${local.account_id}:role/OrganizationAccountAccessRole",
|
||||
"arn:aws:iam::${local.account_id}:role/shoc-backend-dev",
|
||||
]
|
||||
|
||||
live_eb_environment_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${local.account_id}:environment/shoc-backend/shoc-backend-dev"
|
||||
live_cfn_stack_arn = "arn:aws:cloudformation:${var.aws_region}:${local.account_id}:stack/awseb-e-hehnrqjjrt-stack/*"
|
||||
}
|
||||
|
||||
resource "terraform_data" "account_guard" {
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = local.account_id == var.aws_account_id
|
||||
error_message = "Refuse to apply outside seahaven-external-dev (${var.aws_account_id}). Caller is ${local.account_id}."
|
||||
}
|
||||
}
|
||||
}
|
||||
29
terraform/bootstrap/outputs.tf
Normal file
29
terraform/bootstrap/outputs.tf
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
output "tfc_oidc_provider_arn" {
|
||||
description = "app.terraform.io OIDC provider ARN"
|
||||
value = local.tfc_oidc_arn
|
||||
}
|
||||
|
||||
output "hcp_plan_role_arn" {
|
||||
description = "Set TFC_AWS_PLAN_ROLE_ARN on workspace shoc-backend-tf-poc"
|
||||
value = aws_iam_role.hcp_plan.arn
|
||||
}
|
||||
|
||||
output "hcp_apply_role_arn" {
|
||||
description = "Set TFC_AWS_APPLY_ROLE_ARN on workspace shoc-backend-tf-poc"
|
||||
value = aws_iam_role.hcp_apply.arn
|
||||
}
|
||||
|
||||
output "create_tfc_oidc_provider" {
|
||||
description = "Whether this bootstrap created the OIDC provider (false means it was data-sourced)"
|
||||
value = var.create_tfc_oidc_provider
|
||||
}
|
||||
|
||||
output "live_workspace_roles" {
|
||||
description = "HCP Terraform dynamic credential roles for dev and staging."
|
||||
value = {
|
||||
for environment in keys(local.live_environments) : environment => {
|
||||
plan_role_arn = aws_iam_role.live_plan[environment].arn
|
||||
apply_role_arn = aws_iam_role.live_apply[environment].arn
|
||||
}
|
||||
}
|
||||
}
|
||||
11
terraform/bootstrap/providers.tf
Normal file
11
terraform/bootstrap/providers.tf
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
provider "aws" {
|
||||
region = var.aws_region
|
||||
|
||||
default_tags {
|
||||
tags = {
|
||||
Project = "shoc-backend"
|
||||
ManagedBy = "terraform"
|
||||
Workspace = "shoc-backend-bootstrap"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
terraform/bootstrap/terraform.tfvars.example
Normal file
15
terraform/bootstrap/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Copy to terraform.tfvars for local apply. Defaults already match
|
||||
# seahaven-external-dev; this file documents the locked names.
|
||||
aws_region = "us-east-1"
|
||||
aws_account_id = "396287094661"
|
||||
hcp_organization = "seahaven"
|
||||
hcp_project = "seahaven-external-dev"
|
||||
hcp_workload_workspace = "shoc-backend-tf-poc"
|
||||
create_tfc_oidc_provider = false
|
||||
github_deploy_role_name = "githubdeploy-shoc-backend-tf-poc"
|
||||
eb_ec2_role_name = "shoc-backend-tf-poc-ec2"
|
||||
eb_application_name = "shoc-backend-tf-poc"
|
||||
eb_environment_name = "shoc-backend-tf-poc"
|
||||
rds_identifier = "shoc-backend-tf-poc"
|
||||
hosted_zone_id = "Z07671212N75U4YLPWZR8"
|
||||
eb_service_role_name = "shoc-eb-service-role"
|
||||
88
terraform/bootstrap/variables.tf
Normal file
88
terraform/bootstrap/variables.tf
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
variable "aws_region" {
|
||||
type = string
|
||||
description = "AWS region for IAM (global) and any regional data sources"
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "aws_account_id" {
|
||||
type = string
|
||||
description = "seahaven-external-dev. Apply refuses any other account."
|
||||
default = "396287094661"
|
||||
|
||||
validation {
|
||||
condition = var.aws_account_id == "396287094661"
|
||||
error_message = "This bootstrap is only for seahaven-external-dev (396287094661)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "hcp_organization" {
|
||||
type = string
|
||||
description = "HCP Terraform organization name in OIDC trust subs"
|
||||
default = "seahaven"
|
||||
}
|
||||
|
||||
variable "hcp_project" {
|
||||
type = string
|
||||
description = "HCP Terraform project name in OIDC trust subs"
|
||||
default = "seahaven-external-dev"
|
||||
}
|
||||
|
||||
variable "hcp_workload_workspace" {
|
||||
type = string
|
||||
description = "Workload workspace the plan/apply roles trust (not this bootstrap workspace)"
|
||||
default = "shoc-backend-tf-poc"
|
||||
}
|
||||
|
||||
variable "create_tfc_oidc_provider" {
|
||||
type = bool
|
||||
description = "Create the account-global app.terraform.io OIDC provider. Default false: import the provider the frontend POC already created. Set true only if that data source fails."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "github_deploy_role_name" {
|
||||
type = string
|
||||
description = "GitHub OIDC deploy role name (path /tf-managed/ is fixed in IAM ARNs)"
|
||||
default = "githubdeploy-shoc-backend-tf-poc"
|
||||
}
|
||||
|
||||
variable "eb_ec2_role_name" {
|
||||
type = string
|
||||
description = "Elastic Beanstalk instance role name (path /tf-managed/)"
|
||||
default = "shoc-backend-tf-poc-ec2"
|
||||
}
|
||||
|
||||
variable "eb_application_name" {
|
||||
type = string
|
||||
description = "POC Elastic Beanstalk application the apply role may manage"
|
||||
default = "shoc-backend-tf-poc"
|
||||
}
|
||||
|
||||
variable "eb_environment_name" {
|
||||
type = string
|
||||
description = "POC Elastic Beanstalk environment the apply role may manage"
|
||||
default = "shoc-backend-tf-poc"
|
||||
}
|
||||
|
||||
variable "rds_identifier" {
|
||||
type = string
|
||||
description = "POC RDS instance identifier the apply role may manage"
|
||||
default = "shoc-backend-tf-poc"
|
||||
}
|
||||
|
||||
variable "hosted_zone_id" {
|
||||
type = string
|
||||
description = "dev.seahaven.com zone; apply may change records here (tf-poc + ACM validation only in workload TF)"
|
||||
default = "Z07671212N75U4YLPWZR8"
|
||||
}
|
||||
|
||||
variable "execution_boundary_arn" {
|
||||
type = string
|
||||
description = "SCP-required permissions boundary for CreateRole in this account"
|
||||
default = "arn:aws:iam::396287094661:policy/external-dev-execution-boundary"
|
||||
}
|
||||
|
||||
variable "eb_service_role_name" {
|
||||
type = string
|
||||
description = "Existing Elastic Beanstalk service role to PassRole (not created by this stack)"
|
||||
default = "shoc-eb-service-role"
|
||||
}
|
||||
18
terraform/bootstrap/versions.tf
Normal file
18
terraform/bootstrap/versions.tf
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
terraform {
|
||||
required_version = ">= 1.7.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.57"
|
||||
}
|
||||
}
|
||||
|
||||
cloud {
|
||||
organization = "seahaven"
|
||||
|
||||
workspaces {
|
||||
name = "shoc-backend-bootstrap"
|
||||
}
|
||||
}
|
||||
}
|
||||
234
terraform/elastic_beanstalk.tf
Normal file
234
terraform/elastic_beanstalk.tf
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
resource "aws_elastic_beanstalk_application" "api" {
|
||||
name = var.eb_application_name
|
||||
description = "SHOC API ${var.environment} (Terraform). Parallel to live shoc-backend during the POC."
|
||||
|
||||
tags = {
|
||||
Name = var.eb_application_name
|
||||
}
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_elastic_beanstalk_environment" "api" {
|
||||
name = var.eb_environment_name
|
||||
application = aws_elastic_beanstalk_application.api.name
|
||||
solution_stack_name = data.aws_elastic_beanstalk_solution_stack.dotnet.name
|
||||
tier = "WebServer"
|
||||
cname_prefix = var.eb_environment_name
|
||||
|
||||
wait_for_ready_timeout = "40m"
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:environment"
|
||||
name = "EnvironmentType"
|
||||
value = "LoadBalanced"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:environment"
|
||||
name = "LoadBalancerType"
|
||||
value = "application"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:environment"
|
||||
name = "ServiceRole"
|
||||
value = data.aws_iam_role.eb_service.arn
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:ec2:vpc"
|
||||
name = "VPCId"
|
||||
value = var.vpc_id
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:ec2:vpc"
|
||||
name = "Subnets"
|
||||
value = join(",", var.private_subnet_ids)
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:ec2:vpc"
|
||||
name = "ELBSubnets"
|
||||
value = join(",", var.public_subnet_ids)
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:ec2:vpc"
|
||||
name = "ELBScheme"
|
||||
value = "public"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:ec2:vpc"
|
||||
name = "AssociatePublicIpAddress"
|
||||
value = var.associate_public_ip ? "true" : "false"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:autoscaling:launchconfiguration"
|
||||
name = "IamInstanceProfile"
|
||||
value = aws_iam_instance_profile.eb_ec2.name
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:autoscaling:launchconfiguration"
|
||||
name = "InstanceType"
|
||||
value = var.eb_instance_type
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:autoscaling:launchconfiguration"
|
||||
name = "SecurityGroups"
|
||||
value = aws_security_group.eb.id
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:autoscaling:launchconfiguration"
|
||||
name = "DisableIMDSv1"
|
||||
value = "true"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:autoscaling:asg"
|
||||
name = "MinSize"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:autoscaling:asg"
|
||||
name = "MaxSize"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elbv2:loadbalancer"
|
||||
name = "SecurityGroups"
|
||||
value = aws_security_group.alb.id
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elbv2:loadbalancer"
|
||||
name = "ManagedSecurityGroup"
|
||||
value = aws_security_group.alb.id
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elbv2:listener:443"
|
||||
name = "Protocol"
|
||||
value = "HTTPS"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elbv2:listener:443"
|
||||
name = "SSLCertificateArns"
|
||||
value = aws_acm_certificate_validation.api.certificate_arn
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elbv2:listener:443"
|
||||
name = "SSLPolicy"
|
||||
value = "ELBSecurityPolicy-TLS13-1-2-2021-06"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elbv2:listener:80"
|
||||
name = "Protocol"
|
||||
value = "HTTP"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:environment:process:default"
|
||||
name = "HealthCheckPath"
|
||||
value = "/swagger/v1/swagger.json"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:environment:process:default"
|
||||
name = "Port"
|
||||
value = "80"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "ASPNETCORE_ENVIRONMENT"
|
||||
value = "Production"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "ConnectionStrings__DefaultConnection"
|
||||
value = local.connection_string
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "JWT__Secret"
|
||||
value = aws_secretsmanager_secret_version.jwt.secret_string
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "SendGrid__ApiKey"
|
||||
value = var.sendgrid_api_key
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "WorkOrderWebhook__Enabled"
|
||||
value = "true"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "WorkOrderWebhook__Region"
|
||||
value = var.aws_region
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "WorkOrderWebhook__SecretId"
|
||||
value = aws_secretsmanager_secret.webhook.arn
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "Sync__Enabled"
|
||||
value = "false"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:application:environment"
|
||||
name = "WorkOrderReconciliation__Enabled"
|
||||
value = "false"
|
||||
}
|
||||
|
||||
setting {
|
||||
namespace = "aws:elasticbeanstalk:command"
|
||||
name = "DeploymentPolicy"
|
||||
value = "AllAtOnce"
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = var.eb_environment_name
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_iam_role_policy_attachment.eb_web_tier,
|
||||
aws_iam_instance_profile.eb_ec2,
|
||||
aws_db_instance.poc,
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "api" {
|
||||
zone_id = var.hosted_zone_id
|
||||
name = var.domain_name
|
||||
type = "A"
|
||||
|
||||
alias {
|
||||
name = aws_elastic_beanstalk_environment.api.cname
|
||||
zone_id = data.aws_elastic_beanstalk_hosted_zone.current.id
|
||||
evaluate_target_health = true
|
||||
}
|
||||
}
|
||||
145
terraform/iam_github_deploy.tf
Normal file
145
terraform/iam_github_deploy.tf
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
data "aws_iam_policy_document" "github_deploy_assume" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRoleWithWebIdentity"]
|
||||
|
||||
principals {
|
||||
type = "Federated"
|
||||
identifiers = [data.aws_iam_openid_connect_provider.github.arn]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "token.actions.githubusercontent.com:aud"
|
||||
values = ["sts.amazonaws.com"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "token.actions.githubusercontent.com:sub"
|
||||
values = ["repo:${var.github_repo}:environment:${var.github_environment}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "github_deploy" {
|
||||
name = var.github_deploy_role_name
|
||||
path = local.content_role_path
|
||||
description = "GitHub Actions deploy role for ${var.github_repo} environment ${var.github_environment}"
|
||||
assume_role_policy = data.aws_iam_policy_document.github_deploy_assume.json
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.execution_boundary_arn
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [permissions_boundary]
|
||||
}
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
locals {
|
||||
poc_environment_id = aws_elastic_beanstalk_environment.api.id
|
||||
poc_environment_stack = "awseb-${aws_elastic_beanstalk_environment.api.id}-stack"
|
||||
poc_application_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${local.account_id}:application/${var.eb_application_name}"
|
||||
poc_environment_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${local.account_id}:environment/${var.eb_application_name}/${var.eb_environment_name}"
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "github_deploy" {
|
||||
statement {
|
||||
sid = "DescribeDiscovery"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"autoscaling:Describe*",
|
||||
"ec2:Describe*",
|
||||
"elasticbeanstalk:DescribeEnvironments",
|
||||
"elasticbeanstalk:DescribeApplicationVersions",
|
||||
"elasticbeanstalk:DescribeEvents",
|
||||
"elasticloadbalancing:Describe*",
|
||||
]
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "CreateApplicationVersion"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"elasticbeanstalk:CreateApplicationVersion",
|
||||
]
|
||||
resources = [
|
||||
local.poc_application_arn,
|
||||
"arn:aws:elasticbeanstalk:${var.aws_region}:${local.account_id}:applicationversion/${var.eb_application_name}/*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "UpdatePocEnvironment"
|
||||
effect = "Allow"
|
||||
actions = ["elasticbeanstalk:UpdateEnvironment"]
|
||||
resources = [local.poc_environment_arn]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "PocManagedCfn"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"cloudformation:DescribeStackEvents",
|
||||
"cloudformation:DescribeStackResource",
|
||||
"cloudformation:GetTemplate",
|
||||
"cloudformation:DescribeStackResources",
|
||||
"cloudformation:DescribeStacks",
|
||||
"cloudformation:ListStackResources",
|
||||
"cloudformation:CancelUpdateStack",
|
||||
"cloudformation:UpdateStack",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:cloudformation:${var.aws_region}:${local.account_id}:stack/${local.poc_environment_stack}/*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "PocAsgProcess"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"autoscaling:PutNotificationConfiguration",
|
||||
"autoscaling:ResumeProcesses",
|
||||
"autoscaling:SuspendProcesses",
|
||||
]
|
||||
resources = [
|
||||
"arn:aws:autoscaling:${var.aws_region}:${local.account_id}:autoScalingGroup:*:autoScalingGroupName/${local.poc_environment_stack}-*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "EbServiceObjects"
|
||||
effect = "Allow"
|
||||
actions = ["s3:PutObject"]
|
||||
resources = [
|
||||
"arn:aws:s3:::elasticbeanstalk-${var.aws_region}-${local.account_id}/${var.eb_application_name}/*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "EbServiceBuckets"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:ListBucket",
|
||||
]
|
||||
resources = ["arn:aws:s3:::elasticbeanstalk-${var.aws_region}-${local.account_id}"]
|
||||
}
|
||||
|
||||
statement {
|
||||
sid = "DenyLiveEnvironment"
|
||||
effect = "Deny"
|
||||
actions = ["elasticbeanstalk:*"]
|
||||
resources = [
|
||||
"arn:aws:elasticbeanstalk:${var.aws_region}:${local.account_id}:environment/shoc-backend/shoc-backend-dev",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "github_deploy" {
|
||||
name = "${var.github_deploy_role_name}-eb"
|
||||
role = aws_iam_role.github_deploy.id
|
||||
policy = data.aws_iam_policy_document.github_deploy.json
|
||||
}
|
||||
69
terraform/iam_runtime.tf
Normal file
69
terraform/iam_runtime.tf
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
data "aws_iam_policy_document" "eb_ec2_assume" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRole"]
|
||||
|
||||
principals {
|
||||
type = "Service"
|
||||
identifiers = ["ec2.amazonaws.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "eb_ec2" {
|
||||
name = var.eb_ec2_role_name
|
||||
path = local.content_role_path
|
||||
description = "Elastic Beanstalk instance role for ${var.eb_environment_name}"
|
||||
assume_role_policy = data.aws_iam_policy_document.eb_ec2_assume.json
|
||||
max_session_duration = 3600
|
||||
permissions_boundary = var.execution_boundary_arn
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [permissions_boundary]
|
||||
}
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "eb_web_tier" {
|
||||
role = aws_iam_role.eb_ec2.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier"
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "eb_worker_tier" {
|
||||
role = aws_iam_role.eb_ec2.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier"
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "eb_ssm" {
|
||||
role = aws_iam_role.eb_ec2.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "eb_ec2_secrets" {
|
||||
statement {
|
||||
sid = "PocSecrets"
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"secretsmanager:GetSecretValue",
|
||||
"secretsmanager:DescribeSecret",
|
||||
]
|
||||
resources = [
|
||||
aws_secretsmanager_secret.jwt.arn,
|
||||
aws_secretsmanager_secret.webhook.arn,
|
||||
aws_db_instance.poc.master_user_secret[0].secret_arn,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "eb_ec2_secrets" {
|
||||
name = "shoc-backend-tf-poc-secrets"
|
||||
role = aws_iam_role.eb_ec2.id
|
||||
policy = data.aws_iam_policy_document.eb_ec2_secrets.json
|
||||
}
|
||||
|
||||
resource "aws_iam_instance_profile" "eb_ec2" {
|
||||
name = var.eb_ec2_role_name
|
||||
path = local.content_role_path
|
||||
role = aws_iam_role.eb_ec2.name
|
||||
}
|
||||
73
terraform/live/README.md
Normal file
73
terraform/live/README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Live deploy-role adoption
|
||||
|
||||
These roots replace CDK ownership of the existing GitHub Actions deploy roles.
|
||||
They do not create or manage Elastic Beanstalk, RDS, ACM, Route 53, VPC,
|
||||
subnets, security groups, runtime roles, instance profiles, or secrets.
|
||||
|
||||
## Ownership
|
||||
|
||||
- `dev/` imports `githubdeploy-shoc-backend-dev` and its existing inline policy.
|
||||
- `staging/` imports `githubdeploy-shoc-backend-staging` and its existing inline
|
||||
policy.
|
||||
- `modules/environment-inventory/` reads and pins shared and environment
|
||||
resources without owning them.
|
||||
- `../bootstrap/` owns the four narrowly scoped live HCP Terraform plan/apply
|
||||
roles alongside the temporary POC role pair.
|
||||
|
||||
The shared `shoc-backend` Elastic Beanstalk application and
|
||||
`shoc-sqlserver-shared` RDS instance must never enter either environment state.
|
||||
|
||||
## Two-phase adoption
|
||||
|
||||
Each live root pins `adoption_complete=false` in reviewed code. It is not an
|
||||
HCP workspace variable.
|
||||
|
||||
1. Create the HCP workspace and configure dynamic credentials.
|
||||
2. Run the declarative imports.
|
||||
3. Export the HCP plan as JSON and run:
|
||||
|
||||
```bash
|
||||
python scripts/check-terraform-import-plan.py plan.json
|
||||
```
|
||||
|
||||
The first plan must be a no-op after import. The guard rejects updates,
|
||||
creates, deletes, replacements, and managed resource types outside the
|
||||
deploy role and inline policy.
|
||||
4. Apply the no-op import only after review.
|
||||
5. Change the environment root to `adoption_complete=true` in a reviewed code
|
||||
change, then review the controlled in-place role and policy update:
|
||||
|
||||
```bash
|
||||
python scripts/check-terraform-import-plan.py plan.json --allow-update
|
||||
```
|
||||
|
||||
6. Apply only when the plan contains updates to the imported deploy role and
|
||||
policy, with no create, delete, or replacement actions.
|
||||
|
||||
The reviewed `adoption_complete=true` change updates the ownership
|
||||
tag/description and narrows the dev role to the staging-style S3 bucket and
|
||||
application prefix. Read-only AWS APIs retain `Resource = "*"` only where AWS
|
||||
does not support resource-level permissions.
|
||||
|
||||
## Pinned live identities
|
||||
|
||||
- Dev: workspace `shoc-backend-dev`; EB environment `shoc-backend-dev`
|
||||
(`e-hehnrqjjrt`); .NET 8 AL2023 `3.11.3`; `api.dev.seahaven.com`.
|
||||
- Staging: workspace `shoc-backend-staging`; EB environment
|
||||
`shoc-backend-staging` (`e-6c9m4vb62z`); .NET 8 AL2023 `3.11.3`;
|
||||
`api.staging.seahaven.com`.
|
||||
|
||||
The environment roots are intentionally not general-purpose modules. Exact
|
||||
identifiers make accidental cross-environment reuse fail review and planning.
|
||||
|
||||
## Safety invariants
|
||||
|
||||
- Never reuse `shoc-backend-tf-poc` state.
|
||||
- Auto-apply remains off.
|
||||
- HCP apply roles have no IAM create/delete permissions and no service
|
||||
mutation permissions outside the exact imported deploy role.
|
||||
- Both managed resources have `prevent_destroy`.
|
||||
- Do not retire the CDK stack until the no-op import and controlled policy
|
||||
update have both succeeded.
|
||||
- Do not destroy the POC workload until dev and staging deployment smoke tests
|
||||
have stabilized.
|
||||
26
terraform/live/dev/.terraform.lock.hcl
generated
Normal file
26
terraform/live/dev/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "6.62.0"
|
||||
constraints = "~> 6.57"
|
||||
hashes = [
|
||||
"h1:OthB9UeoBgmy348EpDjs5GDGk6p6UxAMQD5cXn7u9Ho=",
|
||||
"zh:35a9e4bc6fd622c5a99561b882025f2745f1256bbf1a8da8d6b39319b75ae0b5",
|
||||
"zh:405927d470ff16201e40aa0fa2d0ab1de477360a0926d20719cd029179682ecd",
|
||||
"zh:4ab7866593a90bcf18f066b0092a209b9f42852acd783b504031ae74cb6f7010",
|
||||
"zh:5b477f313fc511648a4eed9f9085d0778414835896256ab14296d2345b7070e3",
|
||||
"zh:87de70bc99751f94262cec2260d972555a98f588aa3a613e417438f88a1182df",
|
||||
"zh:88f02a8ff07f00da4ffb3bee9e8ae25588e3a0a92633c625c1c2a63bac00a844",
|
||||
"zh:8d8596257453357c9f3fccaa7d2f04299e8d35b16f364adb8a2c829143a9c090",
|
||||
"zh:953c8e15fa9c12c081f17d66cf45246d032fa23bee33f264dc82242afdb98bc2",
|
||||
"zh:9a7dd903e5e9b2b0cc1317ad2d2692e0ddf05ae2a5aaee20ec5dd1db456711b7",
|
||||
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||
"zh:a859154c75c1088d098a481f1ebb259720c5a2ad87781364abf556a741e5adb7",
|
||||
"zh:b8d1e72ad39d5864118f64dd3273424ab637d34b3ff8dd3dfeb4aef9d458587f",
|
||||
"zh:c3666fcfc7b131f5282d4e7249fa68c3a21665757888aa02bbaf6be1cd036bba",
|
||||
"zh:c6b8ff94b3f49bf85fc087381cfe1b271c5b01cf74cf140d58aa500be7138913",
|
||||
"zh:d8143d790e9dd77b8e2f9168e4a33ad6d064dc4b082a0196636b182105aaed14",
|
||||
"zh:fa41eca042f377eb2741e95b36609c1de5b0cd675cd4e3e30c709497cb94db02",
|
||||
]
|
||||
}
|
||||
9
terraform/live/dev/imports.tf
Normal file
9
terraform/live/dev/imports.tf
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import {
|
||||
to = module.deploy_role.aws_iam_role.github_deploy
|
||||
id = "githubdeploy-shoc-backend-dev"
|
||||
}
|
||||
|
||||
import {
|
||||
to = module.deploy_role.aws_iam_role_policy.github_deploy
|
||||
id = "githubdeploy-shoc-backend-dev:GithubDeployRoleDefaultPolicyE8F540D1"
|
||||
}
|
||||
42
terraform/live/dev/main.tf
Normal file
42
terraform/live/dev/main.tf
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
locals {
|
||||
aws_account_id = "396287094661"
|
||||
aws_region = "us-east-1"
|
||||
|
||||
eb_application_name = "shoc-backend"
|
||||
eb_environment_name = "shoc-backend-dev"
|
||||
eb_environment_id = "e-hehnrqjjrt"
|
||||
eb_platform = "64bit Amazon Linux 2023 v3.11.3 running .NET 8"
|
||||
api_domain = "api.dev.seahaven.com"
|
||||
}
|
||||
|
||||
module "inventory" {
|
||||
source = "../modules/environment-inventory"
|
||||
|
||||
aws_account_id = local.aws_account_id
|
||||
vpc_id = "vpc-0d16336143f3da25e"
|
||||
subnet_ids = ["subnet-09eaf2bfa468d206f", "subnet-02946ccd6735742e9", "subnet-0bedaa6e0c750be4f"]
|
||||
rds_identifier = "shoc-sqlserver-shared"
|
||||
eb_service_role_name = "shoc-eb-service-role"
|
||||
runtime_role_name = "shoc-backend-dev"
|
||||
instance_profile_name = "shoc-backend-dev"
|
||||
security_group_ids = ["sg-0c8bb7cf2c193de57", "sg-050e5a737e98ba699"]
|
||||
certificate_domain = "*.seahaven.com"
|
||||
expected_certificate_arn = "arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"
|
||||
hosted_zone_name = "dev.seahaven.com"
|
||||
expected_hosted_zone_id = "Z07671212N75U4YLPWZR8"
|
||||
}
|
||||
|
||||
module "deploy_role" {
|
||||
source = "../modules/deploy-role"
|
||||
|
||||
aws_account_id = local.aws_account_id
|
||||
aws_region = local.aws_region
|
||||
github_repo = "Sea-Haven-Industries/shoc-backend"
|
||||
environment = "dev"
|
||||
eb_application_name = local.eb_application_name
|
||||
eb_environment_name = local.eb_environment_name
|
||||
eb_environment_id = local.eb_environment_id
|
||||
github_deploy_role_name = "githubdeploy-shoc-backend-dev"
|
||||
adoption_complete = false
|
||||
legacy_dev_s3_policy = true
|
||||
}
|
||||
20
terraform/live/dev/outputs.tf
Normal file
20
terraform/live/dev/outputs.tf
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
output "github_deploy_role_arn" {
|
||||
description = "Existing dev GitHub deploy role ARN."
|
||||
value = module.deploy_role.role_arn
|
||||
}
|
||||
|
||||
output "shared_rds_arn" {
|
||||
description = "Data-sourced shared RDS ARN."
|
||||
value = module.inventory.shared_rds_arn
|
||||
}
|
||||
|
||||
output "pinned_eb_environment" {
|
||||
description = "Pinned existing dev Elastic Beanstalk environment identity."
|
||||
value = {
|
||||
application = local.eb_application_name
|
||||
environment = local.eb_environment_name
|
||||
id = local.eb_environment_id
|
||||
platform = local.eb_platform
|
||||
api_domain = local.api_domain
|
||||
}
|
||||
}
|
||||
3
terraform/live/dev/providers.tf
Normal file
3
terraform/live/dev/providers.tf
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
19
terraform/live/dev/versions.tf
Normal file
19
terraform/live/dev/versions.tf
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
terraform {
|
||||
required_version = ">= 1.7.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.57"
|
||||
}
|
||||
}
|
||||
|
||||
cloud {
|
||||
organization = "seahaven"
|
||||
|
||||
workspaces {
|
||||
project = "seahaven-external-dev"
|
||||
name = "shoc-backend-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
173
terraform/live/modules/deploy-role/main.tf
Normal file
173
terraform/live/modules/deploy-role/main.tf
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
data "aws_iam_openid_connect_provider" "github" {
|
||||
url = "https://token.actions.githubusercontent.com"
|
||||
}
|
||||
|
||||
locals {
|
||||
application_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:application/${var.eb_application_name}"
|
||||
environment_arn = "arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:environment/${var.eb_application_name}/${var.eb_environment_name}"
|
||||
environment_stack_name = "awseb-${var.eb_environment_id}-stack"
|
||||
environment_stack_arn = "arn:aws:cloudformation:${var.aws_region}:${var.aws_account_id}:stack/${local.environment_stack_name}/*"
|
||||
environment_asg_arn = "arn:aws:autoscaling:${var.aws_region}:${var.aws_account_id}:autoScalingGroup:*:autoScalingGroupName/${local.environment_stack_name}-*"
|
||||
eb_bucket_name = "elasticbeanstalk-${var.aws_region}-${var.aws_account_id}"
|
||||
use_legacy_s3_policy = !var.adoption_complete && var.legacy_dev_s3_policy
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "assume" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["sts:AssumeRoleWithWebIdentity"]
|
||||
|
||||
principals {
|
||||
type = "Federated"
|
||||
identifiers = [data.aws_iam_openid_connect_provider.github.arn]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "token.actions.githubusercontent.com:aud"
|
||||
values = ["sts.amazonaws.com"]
|
||||
}
|
||||
|
||||
condition {
|
||||
test = "StringEquals"
|
||||
variable = "token.actions.githubusercontent.com:sub"
|
||||
values = ["repo:${var.github_repo}:environment:${var.environment}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "github_deploy" {
|
||||
name = var.github_deploy_role_name
|
||||
path = "/"
|
||||
description = var.adoption_complete ? (
|
||||
"Least-privilege GitHub OIDC deploy role for shoc-backend ${var.environment}. Terraform-owned; application/environment/S3 are owned by Elastic Beanstalk."
|
||||
) : (
|
||||
"Least-privilege GitHub OIDC deploy role for shoc-backend ${var.environment}. CDK-owned; application/environment/S3 are owned by Elastic Beanstalk."
|
||||
)
|
||||
assume_role_policy = data.aws_iam_policy_document.assume.json
|
||||
max_session_duration = 3600
|
||||
|
||||
tags = {
|
||||
Component = "deploy-role"
|
||||
Environment = var.environment
|
||||
ManagedBy = var.adoption_complete ? "terraform" : "cdk"
|
||||
Project = "shoc-backend"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_iam_policy_document" "deploy" {
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"autoscaling:Describe*",
|
||||
"ec2:Describe*",
|
||||
"elasticbeanstalk:DescribeApplicationVersions",
|
||||
"elasticbeanstalk:DescribeEnvironments",
|
||||
"elasticbeanstalk:DescribeEvents",
|
||||
"elasticloadbalancing:Describe*",
|
||||
]
|
||||
# These AWS read APIs do not support resource-level permissions.
|
||||
resources = ["*"]
|
||||
}
|
||||
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["elasticbeanstalk:CreateApplicationVersion"]
|
||||
resources = [
|
||||
local.application_arn,
|
||||
"arn:aws:elasticbeanstalk:${var.aws_region}:${var.aws_account_id}:applicationversion/${var.eb_application_name}/*",
|
||||
]
|
||||
}
|
||||
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = ["elasticbeanstalk:UpdateEnvironment"]
|
||||
resources = [local.environment_arn]
|
||||
}
|
||||
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"cloudformation:CancelUpdateStack",
|
||||
"cloudformation:DescribeStackEvents",
|
||||
"cloudformation:DescribeStackResource",
|
||||
"cloudformation:DescribeStackResources",
|
||||
"cloudformation:DescribeStacks",
|
||||
"cloudformation:GetTemplate",
|
||||
"cloudformation:ListStackResources",
|
||||
"cloudformation:UpdateStack",
|
||||
]
|
||||
resources = [local.environment_stack_arn]
|
||||
}
|
||||
|
||||
statement {
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"autoscaling:PutNotificationConfiguration",
|
||||
"autoscaling:ResumeProcesses",
|
||||
"autoscaling:SuspendProcesses",
|
||||
]
|
||||
resources = [local.environment_asg_arn]
|
||||
}
|
||||
|
||||
dynamic "statement" {
|
||||
for_each = local.use_legacy_s3_policy ? [1] : []
|
||||
content {
|
||||
effect = "Allow"
|
||||
actions = ["s3:Delete*", "s3:Get*", "s3:Put*"]
|
||||
resources = [
|
||||
"arn:aws:s3:::elasticbeanstalk-*/*",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "statement" {
|
||||
for_each = local.use_legacy_s3_policy ? [1] : []
|
||||
content {
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"s3:GetBucket*",
|
||||
"s3:ListBucket",
|
||||
"s3:PutBucketOwnershipControls",
|
||||
"s3:PutBucketPolicy",
|
||||
"s3:PutBucketPublicAccessBlock",
|
||||
]
|
||||
resources = ["arn:aws:s3:::elasticbeanstalk-*"]
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "statement" {
|
||||
for_each = local.use_legacy_s3_policy ? [] : [1]
|
||||
content {
|
||||
effect = "Allow"
|
||||
actions = ["s3:PutObject"]
|
||||
resources = ["arn:aws:s3:::${local.eb_bucket_name}/${var.eb_application_name}/*"]
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "statement" {
|
||||
for_each = local.use_legacy_s3_policy ? [] : [1]
|
||||
content {
|
||||
effect = "Allow"
|
||||
actions = [
|
||||
"s3:GetBucketLocation",
|
||||
"s3:ListBucket",
|
||||
]
|
||||
resources = ["arn:aws:s3:::${local.eb_bucket_name}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "github_deploy" {
|
||||
name = var.policy_name
|
||||
role = aws_iam_role.github_deploy.id
|
||||
policy = data.aws_iam_policy_document.deploy.json
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
14
terraform/live/modules/deploy-role/outputs.tf
Normal file
14
terraform/live/modules/deploy-role/outputs.tf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
output "role_arn" {
|
||||
description = "Existing GitHub deploy role ARN."
|
||||
value = aws_iam_role.github_deploy.arn
|
||||
}
|
||||
|
||||
output "environment_arn" {
|
||||
description = "Exact Elastic Beanstalk environment ARN the deploy role may update."
|
||||
value = local.environment_arn
|
||||
}
|
||||
|
||||
output "environment_stack_arn" {
|
||||
description = "Exact Elastic Beanstalk CloudFormation stack ARN pattern."
|
||||
value = local.environment_stack_arn
|
||||
}
|
||||
62
terraform/live/modules/deploy-role/variables.tf
Normal file
62
terraform/live/modules/deploy-role/variables.tf
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
variable "aws_account_id" {
|
||||
type = string
|
||||
description = "AWS account containing the existing deploy role."
|
||||
}
|
||||
|
||||
variable "aws_region" {
|
||||
type = string
|
||||
description = "AWS region containing the Elastic Beanstalk environment."
|
||||
}
|
||||
|
||||
variable "github_repo" {
|
||||
type = string
|
||||
description = "GitHub owner/repository allowed by the OIDC trust."
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
type = string
|
||||
description = "GitHub Environment and deployment environment."
|
||||
|
||||
validation {
|
||||
condition = contains(["dev", "staging"], var.environment)
|
||||
error_message = "environment must be dev or staging."
|
||||
}
|
||||
}
|
||||
|
||||
variable "eb_application_name" {
|
||||
type = string
|
||||
description = "Existing Elastic Beanstalk application name."
|
||||
}
|
||||
|
||||
variable "eb_environment_name" {
|
||||
type = string
|
||||
description = "Existing Elastic Beanstalk environment name."
|
||||
}
|
||||
|
||||
variable "eb_environment_id" {
|
||||
type = string
|
||||
description = "Existing Elastic Beanstalk environment ID used in generated resource names."
|
||||
}
|
||||
|
||||
variable "github_deploy_role_name" {
|
||||
type = string
|
||||
description = "Existing root-path GitHub OIDC deploy role name."
|
||||
}
|
||||
|
||||
variable "policy_name" {
|
||||
type = string
|
||||
description = "Existing inline policy name created by CDK."
|
||||
default = "GithubDeployRoleDefaultPolicyE8F540D1"
|
||||
}
|
||||
|
||||
variable "adoption_complete" {
|
||||
type = bool
|
||||
description = "False preserves the current role exactly for a no-op import. True records Terraform ownership and applies the targeted S3 policy."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "legacy_dev_s3_policy" {
|
||||
type = bool
|
||||
description = "Whether the current role has the legacy account-wide Elastic Beanstalk S3 permissions. Used only during the no-op import phase."
|
||||
default = false
|
||||
}
|
||||
59
terraform/live/modules/environment-inventory/main.tf
Normal file
59
terraform/live/modules/environment-inventory/main.tf
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
data "aws_caller_identity" "current" {}
|
||||
|
||||
data "aws_vpc" "selected" {
|
||||
id = var.vpc_id
|
||||
}
|
||||
|
||||
data "aws_subnet" "selected" {
|
||||
for_each = var.subnet_ids
|
||||
id = each.value
|
||||
}
|
||||
|
||||
data "aws_db_instance" "shared" {
|
||||
db_instance_identifier = var.rds_identifier
|
||||
}
|
||||
|
||||
data "aws_iam_role" "eb_service" {
|
||||
name = var.eb_service_role_name
|
||||
}
|
||||
|
||||
data "aws_iam_role" "runtime" {
|
||||
name = var.runtime_role_name
|
||||
}
|
||||
|
||||
data "aws_iam_instance_profile" "runtime" {
|
||||
name = var.instance_profile_name
|
||||
}
|
||||
|
||||
data "aws_security_group" "environment" {
|
||||
for_each = var.security_group_ids
|
||||
id = each.value
|
||||
}
|
||||
|
||||
data "aws_acm_certificate" "shared" {
|
||||
domain = var.certificate_domain
|
||||
statuses = ["ISSUED"]
|
||||
most_recent = true
|
||||
}
|
||||
|
||||
data "aws_route53_zone" "api" {
|
||||
name = var.hosted_zone_name
|
||||
private_zone = false
|
||||
}
|
||||
|
||||
check "identity" {
|
||||
assert {
|
||||
condition = data.aws_caller_identity.current.account_id == var.aws_account_id
|
||||
error_message = "Refusing to inspect resources outside the expected AWS account."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = data.aws_acm_certificate.shared.arn == var.expected_certificate_arn
|
||||
error_message = "The resolved ACM certificate does not match the pinned live certificate."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = data.aws_route53_zone.api.zone_id == var.expected_hosted_zone_id
|
||||
error_message = "The resolved Route 53 zone does not match the pinned live zone."
|
||||
}
|
||||
}
|
||||
24
terraform/live/modules/environment-inventory/outputs.tf
Normal file
24
terraform/live/modules/environment-inventory/outputs.tf
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
output "shared_rds_arn" {
|
||||
description = "Existing shared RDS ARN. The live environment states never manage it."
|
||||
value = data.aws_db_instance.shared.db_instance_arn
|
||||
}
|
||||
|
||||
output "runtime_role_arn" {
|
||||
description = "Existing environment-specific runtime role ARN."
|
||||
value = data.aws_iam_role.runtime.arn
|
||||
}
|
||||
|
||||
output "instance_profile_arn" {
|
||||
description = "Existing environment-specific instance-profile ARN."
|
||||
value = data.aws_iam_instance_profile.runtime.arn
|
||||
}
|
||||
|
||||
output "certificate_arn" {
|
||||
description = "Pinned existing shared ACM certificate ARN."
|
||||
value = data.aws_acm_certificate.shared.arn
|
||||
}
|
||||
|
||||
output "hosted_zone_id" {
|
||||
description = "Pinned existing Route 53 hosted-zone ID."
|
||||
value = data.aws_route53_zone.api.zone_id
|
||||
}
|
||||
59
terraform/live/modules/environment-inventory/variables.tf
Normal file
59
terraform/live/modules/environment-inventory/variables.tf
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
variable "aws_account_id" {
|
||||
type = string
|
||||
description = "Expected AWS account ID."
|
||||
}
|
||||
|
||||
variable "vpc_id" {
|
||||
type = string
|
||||
description = "Existing VPC ID."
|
||||
}
|
||||
|
||||
variable "subnet_ids" {
|
||||
type = set(string)
|
||||
description = "Existing Elastic Beanstalk subnet IDs."
|
||||
}
|
||||
|
||||
variable "rds_identifier" {
|
||||
type = string
|
||||
description = "Existing shared RDS instance identifier."
|
||||
}
|
||||
|
||||
variable "eb_service_role_name" {
|
||||
type = string
|
||||
description = "Existing shared Elastic Beanstalk service role."
|
||||
}
|
||||
|
||||
variable "runtime_role_name" {
|
||||
type = string
|
||||
description = "Existing environment-specific EC2 role."
|
||||
}
|
||||
|
||||
variable "instance_profile_name" {
|
||||
type = string
|
||||
description = "Existing environment-specific EC2 instance profile."
|
||||
}
|
||||
|
||||
variable "security_group_ids" {
|
||||
type = set(string)
|
||||
description = "Existing environment-specific Elastic Beanstalk and load-balancer security groups."
|
||||
}
|
||||
|
||||
variable "certificate_domain" {
|
||||
type = string
|
||||
description = "Primary domain on the existing shared ACM certificate."
|
||||
}
|
||||
|
||||
variable "hosted_zone_name" {
|
||||
type = string
|
||||
description = "Existing Route 53 hosted-zone name."
|
||||
}
|
||||
|
||||
variable "expected_certificate_arn" {
|
||||
type = string
|
||||
description = "Exact existing ACM certificate ARN."
|
||||
}
|
||||
|
||||
variable "expected_hosted_zone_id" {
|
||||
type = string
|
||||
description = "Exact existing Route 53 hosted-zone ID."
|
||||
}
|
||||
26
terraform/live/staging/.terraform.lock.hcl
generated
Normal file
26
terraform/live/staging/.terraform.lock.hcl
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "6.62.0"
|
||||
constraints = "~> 6.57"
|
||||
hashes = [
|
||||
"h1:OthB9UeoBgmy348EpDjs5GDGk6p6UxAMQD5cXn7u9Ho=",
|
||||
"zh:35a9e4bc6fd622c5a99561b882025f2745f1256bbf1a8da8d6b39319b75ae0b5",
|
||||
"zh:405927d470ff16201e40aa0fa2d0ab1de477360a0926d20719cd029179682ecd",
|
||||
"zh:4ab7866593a90bcf18f066b0092a209b9f42852acd783b504031ae74cb6f7010",
|
||||
"zh:5b477f313fc511648a4eed9f9085d0778414835896256ab14296d2345b7070e3",
|
||||
"zh:87de70bc99751f94262cec2260d972555a98f588aa3a613e417438f88a1182df",
|
||||
"zh:88f02a8ff07f00da4ffb3bee9e8ae25588e3a0a92633c625c1c2a63bac00a844",
|
||||
"zh:8d8596257453357c9f3fccaa7d2f04299e8d35b16f364adb8a2c829143a9c090",
|
||||
"zh:953c8e15fa9c12c081f17d66cf45246d032fa23bee33f264dc82242afdb98bc2",
|
||||
"zh:9a7dd903e5e9b2b0cc1317ad2d2692e0ddf05ae2a5aaee20ec5dd1db456711b7",
|
||||
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||
"zh:a859154c75c1088d098a481f1ebb259720c5a2ad87781364abf556a741e5adb7",
|
||||
"zh:b8d1e72ad39d5864118f64dd3273424ab637d34b3ff8dd3dfeb4aef9d458587f",
|
||||
"zh:c3666fcfc7b131f5282d4e7249fa68c3a21665757888aa02bbaf6be1cd036bba",
|
||||
"zh:c6b8ff94b3f49bf85fc087381cfe1b271c5b01cf74cf140d58aa500be7138913",
|
||||
"zh:d8143d790e9dd77b8e2f9168e4a33ad6d064dc4b082a0196636b182105aaed14",
|
||||
"zh:fa41eca042f377eb2741e95b36609c1de5b0cd675cd4e3e30c709497cb94db02",
|
||||
]
|
||||
}
|
||||
9
terraform/live/staging/imports.tf
Normal file
9
terraform/live/staging/imports.tf
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import {
|
||||
to = module.deploy_role.aws_iam_role.github_deploy
|
||||
id = "githubdeploy-shoc-backend-staging"
|
||||
}
|
||||
|
||||
import {
|
||||
to = module.deploy_role.aws_iam_role_policy.github_deploy
|
||||
id = "githubdeploy-shoc-backend-staging:GithubDeployRoleDefaultPolicyE8F540D1"
|
||||
}
|
||||
42
terraform/live/staging/main.tf
Normal file
42
terraform/live/staging/main.tf
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
locals {
|
||||
aws_account_id = "396287094661"
|
||||
aws_region = "us-east-1"
|
||||
|
||||
eb_application_name = "shoc-backend"
|
||||
eb_environment_name = "shoc-backend-staging"
|
||||
eb_environment_id = "e-6c9m4vb62z"
|
||||
eb_platform = "64bit Amazon Linux 2023 v3.11.3 running .NET 8"
|
||||
api_domain = "api.staging.seahaven.com"
|
||||
}
|
||||
|
||||
module "inventory" {
|
||||
source = "../modules/environment-inventory"
|
||||
|
||||
aws_account_id = local.aws_account_id
|
||||
vpc_id = "vpc-0d16336143f3da25e"
|
||||
subnet_ids = ["subnet-09eaf2bfa468d206f", "subnet-02946ccd6735742e9", "subnet-0bedaa6e0c750be4f"]
|
||||
rds_identifier = "shoc-sqlserver-shared"
|
||||
eb_service_role_name = "shoc-eb-service-role"
|
||||
runtime_role_name = "shoc-backend-staging"
|
||||
instance_profile_name = "shoc-backend-staging"
|
||||
security_group_ids = ["sg-02ea36a6719217fa2", "sg-0517062b0deef982d"]
|
||||
certificate_domain = "*.seahaven.com"
|
||||
expected_certificate_arn = "arn:aws:acm:us-east-1:396287094661:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"
|
||||
hosted_zone_name = "staging.seahaven.com"
|
||||
expected_hosted_zone_id = "Z02602739VQWBWCAGXP4"
|
||||
}
|
||||
|
||||
module "deploy_role" {
|
||||
source = "../modules/deploy-role"
|
||||
|
||||
aws_account_id = local.aws_account_id
|
||||
aws_region = local.aws_region
|
||||
github_repo = "Sea-Haven-Industries/shoc-backend"
|
||||
environment = "staging"
|
||||
eb_application_name = local.eb_application_name
|
||||
eb_environment_name = local.eb_environment_name
|
||||
eb_environment_id = local.eb_environment_id
|
||||
github_deploy_role_name = "githubdeploy-shoc-backend-staging"
|
||||
adoption_complete = false
|
||||
legacy_dev_s3_policy = false
|
||||
}
|
||||
20
terraform/live/staging/outputs.tf
Normal file
20
terraform/live/staging/outputs.tf
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
output "github_deploy_role_arn" {
|
||||
description = "Existing staging GitHub deploy role ARN."
|
||||
value = module.deploy_role.role_arn
|
||||
}
|
||||
|
||||
output "shared_rds_arn" {
|
||||
description = "Data-sourced shared RDS ARN."
|
||||
value = module.inventory.shared_rds_arn
|
||||
}
|
||||
|
||||
output "pinned_eb_environment" {
|
||||
description = "Pinned existing staging Elastic Beanstalk environment identity."
|
||||
value = {
|
||||
application = local.eb_application_name
|
||||
environment = local.eb_environment_name
|
||||
id = local.eb_environment_id
|
||||
platform = local.eb_platform
|
||||
api_domain = local.api_domain
|
||||
}
|
||||
}
|
||||
3
terraform/live/staging/providers.tf
Normal file
3
terraform/live/staging/providers.tf
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
provider "aws" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
19
terraform/live/staging/versions.tf
Normal file
19
terraform/live/staging/versions.tf
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
terraform {
|
||||
required_version = ">= 1.7.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.57"
|
||||
}
|
||||
}
|
||||
|
||||
cloud {
|
||||
organization = "seahaven"
|
||||
|
||||
workspaces {
|
||||
project = "seahaven-external-dev"
|
||||
name = "shoc-backend-staging"
|
||||
}
|
||||
}
|
||||
}
|
||||
48
terraform/locals.tf
Normal file
48
terraform/locals.tf
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
data "aws_caller_identity" "current" {}
|
||||
|
||||
data "aws_iam_openid_connect_provider" "github" {
|
||||
url = "https://token.actions.githubusercontent.com"
|
||||
}
|
||||
|
||||
data "aws_iam_role" "eb_service" {
|
||||
name = var.eb_service_role_name
|
||||
}
|
||||
|
||||
data "aws_elastic_beanstalk_solution_stack" "dotnet" {
|
||||
most_recent = true
|
||||
name_regex = "^64bit Amazon Linux 2023 .* running .NET 8$"
|
||||
}
|
||||
|
||||
data "aws_elastic_beanstalk_hosted_zone" "current" {}
|
||||
|
||||
locals {
|
||||
account_id = data.aws_caller_identity.current.account_id
|
||||
content_role_path = "/tf-managed/"
|
||||
|
||||
webhook_secret_name = "shoc-backend-tf-poc/webhook-hmac"
|
||||
jwt_secret_name = "shoc-backend-tf-poc/jwt"
|
||||
}
|
||||
|
||||
resource "terraform_data" "account_guard" {
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = local.account_id == var.aws_account_id
|
||||
error_message = "Refuse to apply outside seahaven-external-dev (${var.aws_account_id}). Caller is ${local.account_id}."
|
||||
}
|
||||
|
||||
precondition {
|
||||
condition = can(regex("^vpc-", var.vpc_id))
|
||||
error_message = "Set vpc_id from live Elastic Beanstalk discovery before apply."
|
||||
}
|
||||
|
||||
precondition {
|
||||
condition = length(var.private_subnet_ids) >= 2
|
||||
error_message = "Set at least two private_subnet_ids before apply."
|
||||
}
|
||||
|
||||
precondition {
|
||||
condition = length(var.public_subnet_ids) >= 2
|
||||
error_message = "Set at least two public_subnet_ids before apply."
|
||||
}
|
||||
}
|
||||
}
|
||||
49
terraform/outputs.tf
Normal file
49
terraform/outputs.tf
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
output "api_url" {
|
||||
description = "Public URL of the POC API"
|
||||
value = "https://${var.domain_name}"
|
||||
}
|
||||
|
||||
output "eb_application_name" {
|
||||
description = "Elastic Beanstalk application name"
|
||||
value = aws_elastic_beanstalk_application.api.name
|
||||
}
|
||||
|
||||
output "eb_environment_name" {
|
||||
description = "Elastic Beanstalk environment name"
|
||||
value = aws_elastic_beanstalk_environment.api.name
|
||||
}
|
||||
|
||||
output "eb_environment_id" {
|
||||
description = "Elastic Beanstalk environment id (e-xxxxxxxx)"
|
||||
value = aws_elastic_beanstalk_environment.api.id
|
||||
}
|
||||
|
||||
output "eb_cname" {
|
||||
description = "Elastic Beanstalk environment CNAME"
|
||||
value = aws_elastic_beanstalk_environment.api.cname
|
||||
}
|
||||
|
||||
output "rds_endpoint" {
|
||||
description = "RDS SQL Server address"
|
||||
value = aws_db_instance.poc.address
|
||||
}
|
||||
|
||||
output "database_name" {
|
||||
description = "Catalog to CREATE DATABASE on the RDS instance before the first app deploy"
|
||||
value = var.database_name
|
||||
}
|
||||
|
||||
output "webhook_secret_arn" {
|
||||
description = "Secrets Manager ARN for the POC webhook HMAC keyset"
|
||||
value = aws_secretsmanager_secret.webhook.arn
|
||||
}
|
||||
|
||||
output "github_deploy_role_arn" {
|
||||
description = "Set GitHub Environment tf-poc secret AWS_DEPLOY_ROLE_ARN to this value"
|
||||
value = aws_iam_role.github_deploy.arn
|
||||
}
|
||||
|
||||
output "acm_certificate_arn" {
|
||||
description = "ACM certificate ARN for the POC hostname"
|
||||
value = aws_acm_certificate.api.arn
|
||||
}
|
||||
12
terraform/providers.tf
Normal file
12
terraform/providers.tf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
provider "aws" {
|
||||
region = var.aws_region
|
||||
|
||||
default_tags {
|
||||
tags = {
|
||||
Project = "shoc-backend"
|
||||
ManagedBy = "terraform"
|
||||
Workspace = "shoc-backend-tf-poc"
|
||||
Environment = var.environment
|
||||
}
|
||||
}
|
||||
}
|
||||
66
terraform/rds.tf
Normal file
66
terraform/rds.tf
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
resource "aws_db_subnet_group" "poc" {
|
||||
name = "shoc-backend-tf-poc"
|
||||
subnet_ids = var.private_subnet_ids
|
||||
|
||||
tags = {
|
||||
Name = "shoc-backend-tf-poc"
|
||||
}
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "poc" {
|
||||
identifier = var.rds_identifier
|
||||
engine = "sqlserver-ex"
|
||||
instance_class = var.rds_instance_class
|
||||
license_model = "license-included"
|
||||
|
||||
allocated_storage = var.rds_allocated_storage
|
||||
max_allocated_storage = var.rds_allocated_storage
|
||||
storage_type = "gp3"
|
||||
storage_encrypted = true
|
||||
|
||||
username = var.rds_master_username
|
||||
manage_master_user_password = true
|
||||
|
||||
db_subnet_group_name = aws_db_subnet_group.poc.name
|
||||
vpc_security_group_ids = [aws_security_group.rds.id]
|
||||
publicly_accessible = false
|
||||
multi_az = false
|
||||
port = 1433
|
||||
|
||||
backup_retention_period = 1
|
||||
deletion_protection = false
|
||||
skip_final_snapshot = true
|
||||
apply_immediately = true
|
||||
copy_tags_to_snapshot = true
|
||||
|
||||
tags = {
|
||||
Name = var.rds_identifier
|
||||
}
|
||||
|
||||
timeouts {
|
||||
create = "90m"
|
||||
update = "90m"
|
||||
delete = "90m"
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_secretsmanager_secret_version" "rds_master" {
|
||||
secret_id = aws_db_instance.poc.master_user_secret[0].secret_arn
|
||||
depends_on = [aws_db_instance.poc]
|
||||
}
|
||||
|
||||
locals {
|
||||
rds_master = jsondecode(data.aws_secretsmanager_secret_version.rds_master.secret_string)
|
||||
|
||||
connection_string = join(";", [
|
||||
"Server=${aws_db_instance.poc.address},${aws_db_instance.poc.port}",
|
||||
"Initial Catalog=${var.database_name}",
|
||||
"User Id=${local.rds_master["username"]}",
|
||||
"Password=${local.rds_master["password"]}",
|
||||
"Encrypt=True",
|
||||
"TrustServerCertificate=True",
|
||||
"MultipleActiveResultSets=true",
|
||||
])
|
||||
}
|
||||
49
terraform/secrets.tf
Normal file
49
terraform/secrets.tf
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
resource "random_password" "jwt" {
|
||||
length = 64
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "random_id" "webhook_secret" {
|
||||
byte_length = 32
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "jwt" {
|
||||
name = local.jwt_secret_name
|
||||
description = "JWT signing secret for shoc-backend-tf-poc. Rotate the secret version out of band; Terraform ignores later value changes."
|
||||
recovery_window_in_days = 0
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_version" "jwt" {
|
||||
secret_id = aws_secretsmanager_secret.jwt.id
|
||||
secret_string = random_password.jwt.result
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [secret_string]
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "webhook" {
|
||||
name = local.webhook_secret_name
|
||||
description = "Work-order webhook HMAC keyset for shoc-backend-tf-poc. Do not use the prod ingest ARN."
|
||||
recovery_window_in_days = 0
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_version" "webhook" {
|
||||
secret_id = aws_secretsmanager_secret.webhook.id
|
||||
secret_string = jsonencode({
|
||||
keys = [
|
||||
{
|
||||
kid = "tf-poc"
|
||||
secret = random_id.webhook_secret.hex
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [secret_string]
|
||||
}
|
||||
}
|
||||
93
terraform/security_groups.tf
Normal file
93
terraform/security_groups.tf
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
resource "aws_security_group" "eb" {
|
||||
name = "shoc-backend-tf-poc-eb"
|
||||
description = "Elastic Beanstalk instances for shoc-backend-tf-poc"
|
||||
vpc_id = var.vpc_id
|
||||
|
||||
tags = {
|
||||
Name = "shoc-backend-tf-poc-eb"
|
||||
Project = "shoc-backend"
|
||||
}
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_egress_rule" "eb_all" {
|
||||
security_group_id = aws_security_group.eb.id
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
ip_protocol = "-1"
|
||||
description = "Instances need outbound for Secrets Manager, Windows Update-style platform, and HTTPS."
|
||||
}
|
||||
|
||||
resource "aws_security_group" "alb" {
|
||||
name = "shoc-backend-tf-poc-alb"
|
||||
description = "Application load balancer for shoc-backend-tf-poc"
|
||||
vpc_id = var.vpc_id
|
||||
|
||||
tags = {
|
||||
Name = "shoc-backend-tf-poc-alb"
|
||||
Project = "shoc-backend"
|
||||
}
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "alb_http" {
|
||||
security_group_id = aws_security_group.alb.id
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
from_port = 80
|
||||
to_port = 80
|
||||
ip_protocol = "tcp"
|
||||
description = "HTTP (redirected to HTTPS by the load balancer)"
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "alb_https" {
|
||||
security_group_id = aws_security_group.alb.id
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
ip_protocol = "tcp"
|
||||
description = "HTTPS"
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_egress_rule" "alb_all" {
|
||||
security_group_id = aws_security_group.alb.id
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
ip_protocol = "-1"
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "eb_from_alb" {
|
||||
security_group_id = aws_security_group.eb.id
|
||||
referenced_security_group_id = aws_security_group.alb.id
|
||||
from_port = 80
|
||||
to_port = 80
|
||||
ip_protocol = "tcp"
|
||||
description = "ALB to instance HTTP"
|
||||
}
|
||||
|
||||
resource "aws_security_group" "rds" {
|
||||
name = "shoc-backend-tf-poc-rds"
|
||||
description = "SQL Server for shoc-backend-tf-poc"
|
||||
vpc_id = var.vpc_id
|
||||
|
||||
tags = {
|
||||
Name = "shoc-backend-tf-poc-rds"
|
||||
Project = "shoc-backend"
|
||||
}
|
||||
|
||||
depends_on = [terraform_data.account_guard]
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_ingress_rule" "rds_from_eb" {
|
||||
security_group_id = aws_security_group.rds.id
|
||||
referenced_security_group_id = aws_security_group.eb.id
|
||||
from_port = 1433
|
||||
to_port = 1433
|
||||
ip_protocol = "tcp"
|
||||
description = "EB instances to SQL Server"
|
||||
}
|
||||
|
||||
resource "aws_vpc_security_group_egress_rule" "rds_all" {
|
||||
security_group_id = aws_security_group.rds.id
|
||||
cidr_ipv4 = "0.0.0.0/0"
|
||||
ip_protocol = "-1"
|
||||
}
|
||||
25
terraform/terraform.tfvars.example
Normal file
25
terraform/terraform.tfvars.example
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
aws_region = "us-east-1"
|
||||
aws_account_id = "396287094661"
|
||||
environment = "tf-poc"
|
||||
domain_name = "tf-poc.api.dev.seahaven.com"
|
||||
hosted_zone_id = "Z07671212N75U4YLPWZR8"
|
||||
hosted_zone_name = "dev.seahaven.com"
|
||||
github_repo = "Sea-Haven-Industries/shoc-backend"
|
||||
github_environment = "tf-poc"
|
||||
github_deploy_role_name = "githubdeploy-shoc-backend-tf-poc"
|
||||
eb_application_name = "shoc-backend-tf-poc"
|
||||
eb_environment_name = "shoc-backend-tf-poc"
|
||||
eb_ec2_role_name = "shoc-backend-tf-poc-ec2"
|
||||
eb_service_role_name = "shoc-eb-service-role"
|
||||
eb_instance_type = "t3.small"
|
||||
rds_identifier = "shoc-backend-tf-poc"
|
||||
rds_instance_class = "db.t3.small"
|
||||
rds_allocated_storage = 20
|
||||
rds_master_username = "shoc_admin"
|
||||
database_name = "shoc_tf_poc"
|
||||
|
||||
# Copied from live shoc-backend-dev. Same public subnets for instances and ALB.
|
||||
vpc_id = "vpc-0d16336143f3da25e"
|
||||
private_subnet_ids = ["subnet-09eaf2bfa468d206f", "subnet-02946ccd6735742e9", "subnet-0bedaa6e0c750be4f"]
|
||||
public_subnet_ids = ["subnet-09eaf2bfa468d206f", "subnet-02946ccd6735742e9", "subnet-0bedaa6e0c750be4f"]
|
||||
associate_public_ip = true
|
||||
195
terraform/variables.tf
Normal file
195
terraform/variables.tf
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
variable "aws_region" {
|
||||
type = string
|
||||
description = "AWS region for the API, RDS, ACM, and Elastic Beanstalk."
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "aws_account_id" {
|
||||
type = string
|
||||
description = "seahaven-external-dev. Apply refuses any other account."
|
||||
default = "396287094661"
|
||||
|
||||
validation {
|
||||
condition = var.aws_account_id == "396287094661"
|
||||
error_message = "This stack is only for seahaven-external-dev (396287094661)."
|
||||
}
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
type = string
|
||||
description = "Logical environment. POC apply is tf-poc only. dev/staging/prod are accepted so promotion does not rewrite the module."
|
||||
default = "tf-poc"
|
||||
|
||||
validation {
|
||||
condition = contains(["tf-poc", "dev", "staging", "prod"], var.environment)
|
||||
error_message = "environment must be tf-poc, dev, staging, or prod."
|
||||
}
|
||||
}
|
||||
|
||||
variable "domain_name" {
|
||||
type = string
|
||||
description = "Public API hostname."
|
||||
default = "tf-poc.api.dev.seahaven.com"
|
||||
|
||||
validation {
|
||||
condition = var.environment != "tf-poc" || var.domain_name == "tf-poc.api.dev.seahaven.com"
|
||||
error_message = "tf-poc hostname must be tf-poc.api.dev.seahaven.com."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = var.environment != "dev" || var.domain_name == "api.dev.seahaven.com"
|
||||
error_message = "dev hostname must be api.dev.seahaven.com."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = var.domain_name != "api.tf-poc.dev.seahaven.com"
|
||||
error_message = "Refuse api.tf-poc.dev.seahaven.com; use tf-poc.api.dev.seahaven.com."
|
||||
}
|
||||
}
|
||||
|
||||
variable "hosted_zone_id" {
|
||||
type = string
|
||||
description = "Route 53 zone for the hostname + ACM DNS validation."
|
||||
default = "Z07671212N75U4YLPWZR8"
|
||||
}
|
||||
|
||||
variable "hosted_zone_name" {
|
||||
type = string
|
||||
description = "Zone name; used only for documentation and FQDN checks."
|
||||
default = "dev.seahaven.com"
|
||||
}
|
||||
|
||||
variable "github_repo" {
|
||||
type = string
|
||||
description = "GitHub owner/name for the deploy OIDC trust"
|
||||
default = "Sea-Haven-Industries/shoc-backend"
|
||||
}
|
||||
|
||||
variable "github_environment" {
|
||||
type = string
|
||||
description = "GitHub Actions environment name used in the OIDC sub"
|
||||
default = "tf-poc"
|
||||
}
|
||||
|
||||
variable "github_deploy_role_name" {
|
||||
type = string
|
||||
description = "IAM role name at path /tf-managed/"
|
||||
default = "githubdeploy-shoc-backend-tf-poc"
|
||||
}
|
||||
|
||||
variable "eb_application_name" {
|
||||
type = string
|
||||
description = "Elastic Beanstalk application name. POC uses a separate app from live shoc-backend."
|
||||
default = "shoc-backend-tf-poc"
|
||||
|
||||
validation {
|
||||
condition = var.environment != "tf-poc" || var.eb_application_name == "shoc-backend-tf-poc"
|
||||
error_message = "tf-poc Elastic Beanstalk application must be shoc-backend-tf-poc."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = var.environment != "dev" || var.eb_application_name == "shoc-backend"
|
||||
error_message = "dev Elastic Beanstalk application must be shoc-backend."
|
||||
}
|
||||
}
|
||||
|
||||
variable "eb_environment_name" {
|
||||
type = string
|
||||
description = "Elastic Beanstalk environment name."
|
||||
default = "shoc-backend-tf-poc"
|
||||
|
||||
validation {
|
||||
condition = var.environment != "tf-poc" || var.eb_environment_name == "shoc-backend-tf-poc"
|
||||
error_message = "tf-poc Elastic Beanstalk environment must be shoc-backend-tf-poc."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = var.environment != "dev" || var.eb_environment_name == "shoc-backend-dev"
|
||||
error_message = "dev Elastic Beanstalk environment must be shoc-backend-dev."
|
||||
}
|
||||
}
|
||||
|
||||
variable "eb_ec2_role_name" {
|
||||
type = string
|
||||
description = "Instance role / instance-profile name at path /tf-managed/"
|
||||
default = "shoc-backend-tf-poc-ec2"
|
||||
}
|
||||
|
||||
variable "eb_service_role_name" {
|
||||
type = string
|
||||
description = "Existing Elastic Beanstalk service role (data-sourced, not created)"
|
||||
default = "shoc-eb-service-role"
|
||||
}
|
||||
|
||||
variable "eb_instance_type" {
|
||||
type = string
|
||||
description = "EC2 instance type for the POC environment"
|
||||
default = "t3.small"
|
||||
}
|
||||
|
||||
variable "vpc_id" {
|
||||
type = string
|
||||
description = "Existing VPC that hosts live Elastic Beanstalk. Required at apply; discover before first apply."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "private_subnet_ids" {
|
||||
type = list(string)
|
||||
description = "Private subnets for RDS and EB instances (at least two AZs)."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "public_subnet_ids" {
|
||||
type = list(string)
|
||||
description = "Public subnets for the EB application load balancer (at least two AZs)."
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "associate_public_ip" {
|
||||
type = bool
|
||||
description = "Associate a public IP on EB instances. Live shoc-backend-dev uses true on public subnets."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "rds_identifier" {
|
||||
type = string
|
||||
description = "RDS instance identifier"
|
||||
default = "shoc-backend-tf-poc"
|
||||
}
|
||||
|
||||
variable "rds_instance_class" {
|
||||
type = string
|
||||
description = "RDS SQL Server Express instance class"
|
||||
default = "db.t3.small"
|
||||
}
|
||||
|
||||
variable "rds_allocated_storage" {
|
||||
type = number
|
||||
description = "RDS allocated storage in GiB. SQL Server minimum is 20. Express database size remains 10 GiB."
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "rds_master_username" {
|
||||
type = string
|
||||
description = "RDS master username. Cannot be sa/admin/root."
|
||||
default = "shoc_admin"
|
||||
}
|
||||
|
||||
variable "database_name" {
|
||||
type = string
|
||||
description = "SQL Server catalog the API uses. Create this database once after RDS is available (RDS Express does not accept db_name)."
|
||||
default = "shoc_tf_poc"
|
||||
}
|
||||
|
||||
variable "sendgrid_api_key" {
|
||||
type = string
|
||||
description = "SendGrid API key. Set as a sensitive HCP workspace variable before smoke that sends mail. Empty is allowed for first apply."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "execution_boundary_arn" {
|
||||
type = string
|
||||
description = "SCP-required permissions boundary on CreateRole"
|
||||
default = "arn:aws:iam::396287094661:policy/external-dev-execution-boundary"
|
||||
}
|
||||
22
terraform/versions.tf
Normal file
22
terraform/versions.tf
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
terraform {
|
||||
required_version = ">= 1.7.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 6.57"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = "~> 3.6"
|
||||
}
|
||||
}
|
||||
|
||||
cloud {
|
||||
organization = "seahaven"
|
||||
|
||||
workspaces {
|
||||
name = "shoc-backend-tf-poc"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue