Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Predefined environment variables are injected into each backend service automati
| `POSTGRES_NAME` | PostgreSQL name | *(empty)* | AWS Aurora database |
| `POSTGRES_USER` | PostgreSQL username | *(empty)* | AWS Aurora username |
| `POSTGRES_PASS` | PostgreSQL password | *(empty)* | AWS Aurora password |
| `MONGO_HOST` | MongoDB hostname | `host.docker.internal` | AWS DocumentDB endpoint |
| `MONGO_HOST` | MongoDB hostname | `172.17.0.1` (Linux) / `host.docker.internal` (Mac/Windows) | AWS DocumentDB endpoint |
| `MONGO_PORT`. | MongoDB port | `27017` | `27017` |
| `MONGO_NAME` | MongoDB db name | *(empty)* | AWS DocumentDB database |
| `MONGO_USER` | MongoDB username | *(empty)* | AWS DocumentDB username |
Expand All @@ -50,17 +50,35 @@ coding-workshop-participant/
│ │ │ ├── eslint.config.js # ESLint JS tool configuration
│ │ │ ├── index.js # Business logic using NodeJS
│ │ │ ├── mongo-service.js # MongoDB connectivity service
│ │ │ ├── package.js # NodeJS configuration and dependencies
│ │ │ └── postgres-service.json # PostgreSQL connectivity service
│ │ │ ├── package.json # NodeJS configuration and dependencies
│ │ │ └── postgres-service.js # PostgreSQL connectivity service
│ │ └── python-service/ # Backend service example for Python developers
│ │ ├── function.py # Business logic using Python
│ │ ├── mongo_service.js # MongoDB connectivity service
│ │ ├── postgres_service.json # PostgreSQL connectivity service
│ │ ├── mongo_service.py # MongoDB connectivity service
│ │ ├── postgres_service.py # PostgreSQL connectivity service
│ │ └── requirements.txt # Python configuration and dependencies
│ └── README.md # Backend guide (YOU ARE HERE)
├── ...
```

## Adding a New Service

Place your service folder **directly under `backend/`** — one level deep:

```
backend/
├── my-service/ ✓ will be deployed
│ └── function.py
├── _examples/
│ └── my-service/ ✗ will NOT be deployed (underscore prefix)
│ └── function.py
└── group/
└── my-service/ ✗ will NOT be deployed (too deep)
└── function.py
```

Terraform auto-discovers services by looking for `function.py` (Python), `package.json` (Node.js), or `pom.xml` (Java) one level under `backend/`. Any folder prefixed with `_` is ignored.

## Usage

### Local Development
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,33 @@ private String getPostgresVersion() {

/**
* Retrieves the MongoDB version using the MongoService.
* Returns null when MONGO_HOST is not set (e.g. DocumentDB disabled on AWS).
*
* @return the MongoDB version string, or "unknown" if retrieval fails
* @return the MongoDB version string, null if not configured, or "unknown" on error
*/
private String getMongoVersion() {
String host = System.getenv("MONGO_HOST");
if (host == null || host.isEmpty()) {
return null;
}
try {
String host = getEnvOrDefault("MONGO_HOST", "localhost");
int port = Integer.parseInt(getEnvOrDefault("MONGO_PORT", "27017"));
String user = getEnvOrDefault("MONGO_USER", "test");
String password = getEnvOrDefault("MONGO_PASS", "test");
String database = getEnvOrDefault("MONGO_NAME", "test");

String user = System.getenv("MONGO_USER");
String password = System.getenv("MONGO_PASS");
String database = getEnvOrDefault("MONGO_NAME", "admin");
boolean isLocal = "true".equals(System.getenv("IS_LOCAL"));

// Include credentials only when provided
String auth = (user != null && !user.isEmpty())
? user + ":" + password + "@"
: "";
// TLS required for DocumentDB on AWS, disabled locally
String tls = isLocal
? ""
: "?tls=true&tlsAllowInvalidCertificates=true&retryWrites=false";
String connectionString = String.format(
"mongodb://%s:%s@%s:%d/%s",
user, password, host, port, database
"mongodb://%s%s:%d/%s%s",
auth, host, port, database, tls
);

MongoService service = new MongoService(connectionString, database);
Expand Down
27 changes: 14 additions & 13 deletions backend/_examples/nodejs-service/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@ const pgConfig = {
};

/**
* MongoDB configuration loaded from environment variables with sensible defaults.
* MongoDB configuration loaded from environment variables.
* Null when MONGO_HOST is not set (e.g. DocumentDB disabled on AWS).
*
* @type {Object}
* @type {Object|null}
*/
const mongoConfig = {
host: process.env.MONGO_HOST || "localhost",
const _mongoHost = process.env.MONGO_HOST || "";
const mongoConfig = _mongoHost ? {
host: _mongoHost,
port: parseInt(process.env.MONGO_PORT || "27017"),
user: process.env.MONGO_USER || "test",
password: process.env.MONGO_PASS || "test",
database: process.env.MONGO_NAME || "test",
user: process.env.MONGO_USER || "",
password: process.env.MONGO_PASS || "",
database: process.env.MONGO_NAME || "",
isLocal: process.env.IS_LOCAL === "true",
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
};
} : null;

/**
* Sample code: Hello World with PostgreSQL and MongoDB connectivity.
Expand All @@ -46,11 +49,9 @@ exports.handler = async (event, context) => {
console.log("Received context: ", context)

try {
// Retrieve versions in parallel for better performance
const [pgVersion, mongoVersion] = await Promise.all([
getPostgresVersion(pgConfig),
getMongoVersion(mongoConfig),
]);
// Retrieve versions — MongoDB skipped if not configured
const pgVersion = await getPostgresVersion(pgConfig);
const mongoVersion = mongoConfig ? await getMongoVersion(mongoConfig) : null;

// Log retrieved versions for debugging
console.log("PostgreSQL Version: ", pgVersion);
Expand Down
6 changes: 4 additions & 2 deletions backend/_examples/nodejs-service/mongo-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ let mongoClient = null;
*/
async function getMongoVersion(config) {
try {
// Build MongoDB connection URI with credentials
const uri = `mongodb://${config.user}:${config.password}@${config.host}:${config.port}`;
// Build MongoDB connection URI — include credentials only when provided
const auth = config.user ? `${config.user}:${config.password}@` : "";
const uri = `mongodb://${auth}${config.host}:${config.port}`;

// Create client if not already pooled
if (!mongoClient) {
mongoClient = new MongoClient(uri, {
serverSelectionTimeoutMS: config.serverSelectionTimeoutMS,
socketTimeoutMS: config.socketTimeoutMS,
...(config.isLocal ? {} : { tls: true, tlsAllowInvalidCertificates: true, retryWrites: false }),
});
await mongoClient.connect();
}
Expand Down
1 change: 0 additions & 1 deletion backend/_examples/nodejs-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"name": "nodejs-service",
"description": "Hello World",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"main": "index.js",
"scripts": {
Expand Down
18 changes: 11 additions & 7 deletions backend/_examples/python-service/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@
f"connect_timeout=15"
)

# MongoDB configuration loaded from environment variables with sensible defaults
# MongoDB configuration loaded from environment variables.
# None when MONGO_HOST is not set (e.g. DocumentDB disabled on AWS).
_mongo_host = os.getenv("MONGO_HOST", "")
_mongo_user = os.getenv("MONGO_USER", "")
_mongo_pass = os.getenv("MONGO_PASS", "")
_is_local = os.getenv("IS_LOCAL", "false") == "true"
MONGO_CONFIG = {
"host": os.getenv("MONGO_HOST", "localhost"),
"host": _mongo_host,
"port": int(os.getenv("MONGO_PORT", "27017")),
"user": os.getenv("MONGO_USER", "test"),
"password": os.getenv("MONGO_PASS", "test"),
"database": os.getenv("MONGO_NAME", "test"),
"serverSelectionTimeoutMS": 5000,
"socketTimeoutMS": 45000,
}
**({"username": _mongo_user, "password": _mongo_pass, "authSource": os.getenv("MONGO_NAME", "admin")} if _mongo_user else {}),
**({"tls": True, "tlsAllowInvalidCertificates": True, "retryWrites": False} if not _is_local else {}),
} if _mongo_host else None

def handler(event=None, context=None):
"""
Expand All @@ -53,7 +57,7 @@ def handler(event=None, context=None):
try:
# Retrieve versions from both databases
pg_version = get_postgres_version(PG_CONFIG)
mongo_version = get_mongo_version(MONGO_CONFIG)
mongo_version = get_mongo_version(MONGO_CONFIG) if MONGO_CONFIG else None

# Log retrieved versions for debugging
logger.info("PostgreSQL Version: %s", pg_version)
Expand Down
25 changes: 16 additions & 9 deletions bin/deploy-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,31 @@ echo "INFO: Environment - $ENVIRONMENT"
# Change to infrastructure directory
cd "$INFRA_DIR"

# Load participant configuration (provides PARTICIPANT_ID, TF_VAR_aws_app_code, etc.)
if [ -f "$ENVIRONMENT_CONFIG" ]; then
echo "INFO: Loading participant environment configuration..."
source $ENVIRONMENT_CONFIG
fi

# AWS Deployment Configuration
if [ "$ENVIRONMENT" = "aws" ]; then
echo "INFO: Using AWS deployment (terraform)..."

# Setup participant if config is missing
$SCRIPT_DIR/setup-participant.sh

# Load participant-specific configuration if available
if [ -f "$ENVIRONMENT_CONFIG" ]; then
echo "INFO: Loading participant environment configuration..."
source $ENVIRONMENT_CONFIG
else
echo "WARNING: $ENVIRONMENT_CONFIG is missing"
if [ -z "$PARTICIPANT_ID" ]; then
$SCRIPT_DIR/setup-participant.sh
if [ -f "$ENVIRONMENT_CONFIG" ]; then
source $ENVIRONMENT_CONFIG
fi
fi
else
# Local development configuration
# Local development configuration — override credentials for LocalStack
export AWS_ENDPOINT_URL="http://localhost:4566"
export AWS_ENDPOINT_URL_S3="http://s3.localhost.localstack.cloud:4566"
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_REGION=us-east-1
unset AWS_SESSION_TOKEN

BUCKET_NAME="coding-workshop-tfstate-${PARTICIPANT_ID:-abcd1234}"
if ! aws s3 ls | grep -q "$BUCKET_NAME"; then
Expand Down
47 changes: 34 additions & 13 deletions bin/generate-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,50 @@ ENVIRONMENT_CONFIG="$FRONTEND_DIR/.env.local"
# Change to infrastructure directory to retrieve Terraform outputs
cd "$INFRA_DIR"

# Load participant configuration
PARTICIPANT_CONFIG="$PROJECT_ROOT/ENVIRONMENT.config"
if [ -f "$PARTICIPANT_CONFIG" ]; then
source "$PARTICIPANT_CONFIG"
fi

# Detect environment (local with LocalStack or AWS)
if command -v tflocal > /dev/null 2>&1 && tflocal output -raw api_base_url >/dev/null 2>&1; then
# LocalStack available and configured for local development
if curl -s http://localhost:4566/_localstack/health > /dev/null 2>&1; then
# LocalStack is running — use it
ENVIRONMENT="local"
TF_CMD="tflocal"

# Set LocalStack AWS credentials
export AWS_REGION=us-east-1
export AWS_ENDPOINT_URL="http://localhost:4566"
export AWS_ENDPOINT_URL_S3="http://s3.localhost.localstack.cloud:4566"
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_REGION=us-east-1
unset AWS_SESSION_TOKEN
BUCKET_NAME="coding-workshop-tfstate-${PARTICIPANT_ID:-abcd1234}"
else
# AWS deployment environment
ENVIRONMENT="aws"
TF_CMD="terraform"
BUCKET_NAME="coding-workshop-tfstate-${PARTICIPANT_ID:-abcd1234}"
fi

# Initialize terraform with the correct backend so outputs come from the right state
terraform init -reconfigure \
-backend-config="bucket=$BUCKET_NAME" \
-backend-config="region=${AWS_REGION:-us-east-1}" \
> /dev/null 2>&1

# Retrieve API base URL from Terraform outputs
API_BASE_URL=$($TF_CMD output -raw api_base_url 2>/dev/null || echo "ERROR")
ALL_OUTPUTS=$(terraform output -json 2>/dev/null || echo "{}")
API_BASE_URL=$(echo "$ALL_OUTPUTS" | grep -o '"api_base_url":{[^}]*}' | grep -o '"value":"[^"]*"' | cut -d'"' -f4 || echo "")

# Verify Terraform output was retrieved successfully
if [ "$API_BASE_URL" = "ERROR" ]; then
echo "WARNING: Could not get api_base_url from Terraform outputs"
if [ -z "$ALL_OUTPUTS" ] || [ "$ALL_OUTPUTS" = "{}" ]; then
echo "WARNING: Could not get outputs from Terraform"
echo "Make sure infrastructure is deployed first with: ./bin/deploy-backend.sh"
exit 1
fi

# Fallback using terraform output -raw (suppress stderr warnings)
if [ -z "$API_BASE_URL" ]; then
API_BASE_URL=$(terraform output -raw api_base_url 2>/dev/null || echo "")
fi

# Handle empty API base URL (valid for local development - uses direct Lambda URLs)
if [ -z "$API_BASE_URL" ]; then
echo "API Base URL: (empty - using direct Lambda Function URLs)"
Expand All @@ -77,8 +95,9 @@ else
echo "API Base URL: $API_BASE_URL"
fi

# Retrieve API endpoints configuration from Terraform outputs
API_ENDPOINTS=$($TF_CMD output -json api_endpoints 2>/dev/null || echo "{}")
# Retrieve API endpoints and Lambda URLs from Terraform outputs
API_ENDPOINTS=$(terraform output -json api_endpoints 2>/dev/null || echo "{}")
LAMBDA_URLS=$(terraform output -json lambda_urls 2>/dev/null || echo "{}")

# Generate .env.local configuration file for React frontend
cat > "$ENVIRONMENT_CONFIG" << EOF
Expand All @@ -87,8 +106,10 @@ cat > "$ENVIRONMENT_CONFIG" << EOF
# Environment: $ENVIRONMENT
REACT_APP_API_URL=$API_BASE_URL
REACT_APP_API_ENDPOINTS='$API_ENDPOINTS'
REACT_APP_LAMBDA_URLS='$LAMBDA_URLS'
VITE_API_URL=$API_BASE_URL
VITE_API_ENDPOINTS='$API_ENDPOINTS'
VITE_LAMBDA_URLS='$LAMBDA_URLS'
EOF

echo ""
Expand Down
Loading
Loading