Infrastructure as Code for ChatGPT Apps (Terraform, Pulumi)
Managing infrastructure manually for ChatGPT applications becomes unsustainable at scale. When you're deploying MCP servers, API gateways, databases, and CDN configurations across multiple environments, infrastructure as code (IaC) transforms chaos into reproducible, version-controlled automation.
This guide provides production-ready Terraform and Pulumi implementations specifically designed for ChatGPT applications. You'll learn how to provision cloud resources, manage state securely, integrate with CI/CD pipelines, and deploy infrastructure changes with confidence.
Why Infrastructure as Code for ChatGPT Apps?
Traditional manual infrastructure management fails when you need to:
- Deploy identical environments across development, staging, and production
- Scale horizontally by provisioning multiple regional deployments
- Maintain compliance with auditable infrastructure changes
- Recover from disasters by rebuilding infrastructure from code
- Collaborate effectively with team members using version control
ChatGPT applications have unique infrastructure requirements: MCP server deployments, OAuth provider configurations, widget CDN distribution, and database schemas for user state. IaC ensures these components are consistently provisioned and properly connected.
Terraform vs Pulumi: Making the Choice
Terraform uses HashiCorp Configuration Language (HCL), offering:
- Mature ecosystem with 3,000+ providers
- Declarative syntax optimized for infrastructure
- Strong state management and locking
- Extensive community resources
Pulumi uses general-purpose languages (TypeScript, Python, Go), providing:
- Familiar programming constructs (loops, functions, classes)
- Type safety and IDE autocomplete
- Easier testing with standard unit testing frameworks
- Native component abstractions
Both tools excel at managing ChatGPT app infrastructure. Choose Terraform for teams preferring declarative configuration or Pulumi for developers wanting programming language flexibility.
IaC Fundamentals for ChatGPT Applications
Declarative vs Imperative Approaches
Declarative (Terraform): You define the desired end state. The tool determines how to achieve it.
# Terraform - declare desired state
resource "aws_lambda_function" "mcp_server" {
function_name = "chatgpt-mcp-server"
runtime = "nodejs20.x"
memory_size = 512
}
Imperative (Pulumi with programmatic control): You write code that executes to create resources.
// Pulumi - programmatic resource creation
const mcpServer = new aws.lambda.Function("chatgpt-mcp-server", {
runtime: "nodejs20.x",
memorySize: 512,
handler: "index.handler"
});
Both approaches produce identical infrastructure, but differ in how changes are expressed.
State Management Architecture
IaC tools maintain a state file mapping your code to actual cloud resources. For ChatGPT apps, state includes:
- MCP server Lambda/Cloud Function ARNs
- API Gateway endpoints and route configurations
- Database connection strings and schemas
- OAuth client IDs and redirect URIs
- CDN distribution IDs for widget hosting
Critical: Never store state files in version control. Always use remote backends (S3, GCS, Terraform Cloud) with encryption and state locking to prevent concurrent modifications.
Modules and Reusability
Create reusable modules for common ChatGPT app patterns:
- MCP Server Module: Lambda/Cloud Function + API Gateway + IAM roles
- OAuth Module: Provider configuration + redirect handling + token storage
- Database Module: Firestore/DynamoDB setup + indexes + security rules
- CDN Module: CloudFront/Cloud CDN + widget distribution + SSL certificates
Modules eliminate duplication and enforce organizational standards across multiple ChatGPT applications.
Terraform Implementation for ChatGPT Apps
AWS Infrastructure with Terraform
This complete Terraform configuration provisions a production-ready ChatGPT app on AWS:
# terraform/aws/main.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "chatgpt-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "ChatGPT-App"
Environment = var.environment
ManagedBy = "Terraform"
}
}
}
# MCP Server Lambda Function
resource "aws_lambda_function" "mcp_server" {
function_name = "${var.app_name}-mcp-server-${var.environment}"
runtime = "nodejs20.x"
handler = "index.handler"
memory_size = 512
timeout = 30
filename = var.lambda_zip_path
source_code_hash = filebase64sha256(var.lambda_zip_path)
role = aws_iam_role.lambda_execution.arn
environment {
variables = {
NODE_ENV = var.environment
DYNAMODB_TABLE = aws_dynamodb_table.app_state.name
OAUTH_CLIENT_ID = var.oauth_client_id
OAUTH_CLIENT_SECRET = var.oauth_client_secret
API_BASE_URL = "https://${aws_api_gateway_domain_name.api.domain_name}"
}
}
tracing_config {
mode = "Active"
}
vpc_config {
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.lambda.id]
}
}
# Lambda IAM Role
resource "aws_iam_role" "lambda_execution" {
name = "${var.app_name}-lambda-execution-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_vpc" {
role = aws_iam_role.lambda_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
resource "aws_iam_role_policy" "lambda_dynamodb" {
name = "dynamodb-access"
role = aws_iam_role.lambda_execution.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
"dynamodb:Scan"
]
Resource = [
aws_dynamodb_table.app_state.arn,
"${aws_dynamodb_table.app_state.arn}/index/*"
]
}]
})
}
# API Gateway REST API
resource "aws_api_gateway_rest_api" "chatgpt_api" {
name = "${var.app_name}-api-${var.environment}"
description = "ChatGPT App MCP Server API"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_resource" "mcp" {
rest_api_id = aws_api_gateway_rest_api.chatgpt_api.id
parent_id = aws_api_gateway_rest_api.chatgpt_api.root_resource_id
path_part = "mcp"
}
resource "aws_api_gateway_method" "mcp_post" {
rest_api_id = aws_api_gateway_rest_api.chatgpt_api.id
resource_id = aws_api_gateway_resource.mcp.id
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "mcp_lambda" {
rest_api_id = aws_api_gateway_rest_api.chatgpt_api.id
resource_id = aws_api_gateway_resource.mcp.id
http_method = aws_api_gateway_method.mcp_post.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.mcp_server.invoke_arn
}
resource "aws_lambda_permission" "api_gateway" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.mcp_server.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.chatgpt_api.execution_arn}/*/*"
}
# DynamoDB Table for App State
resource "aws_dynamodb_table" "app_state" {
name = "${var.app_name}-state-${var.environment}"
billing_mode = "PAY_PER_REQUEST"
hash_key = "userId"
range_key = "sessionId"
attribute {
name = "userId"
type = "S"
}
attribute {
name = "sessionId"
type = "S"
}
attribute {
name = "createdAt"
type = "N"
}
global_secondary_index {
name = "createdAt-index"
hash_key = "userId"
range_key = "createdAt"
projection_type = "ALL"
}
point_in_time_recovery {
enabled = true
}
server_side_encryption {
enabled = true
}
tags = {
Name = "${var.app_name}-state"
}
}
# CloudFront Distribution for Widgets
resource "aws_cloudfront_distribution" "widgets" {
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
price_class = "PriceClass_100"
origin {
domain_name = aws_s3_bucket.widgets.bucket_regional_domain_name
origin_id = "S3-${aws_s3_bucket.widgets.id}"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.widgets.cloudfront_access_identity_path
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${aws_s3_bucket.widgets.id}"
viewer_protocol_policy = "redirect-to-https"
compress = true
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
# Outputs
output "mcp_server_url" {
value = "https://${aws_api_gateway_domain_name.api.domain_name}/mcp"
description = "MCP Server API endpoint"
}
output "widget_cdn_url" {
value = "https://${aws_cloudfront_distribution.widgets.domain_name}"
description = "Widget CDN distribution URL"
}
output "dynamodb_table_name" {
value = aws_dynamodb_table.app_state.name
description = "DynamoDB table for app state"
}
GCP Infrastructure with Terraform
Deploy the same ChatGPT app architecture on Google Cloud Platform:
# terraform/gcp/main.tf
terraform {
required_version = ">= 1.6.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
backend "gcs" {
bucket = "chatgpt-terraform-state"
prefix = "production/terraform.tfstate"
}
}
provider "google" {
project = var.project_id
region = var.region
}
# Cloud Function for MCP Server
resource "google_cloudfunctions2_function" "mcp_server" {
name = "${var.app_name}-mcp-server-${var.environment}"
location = var.region
description = "ChatGPT MCP Server"
build_config {
runtime = "nodejs20"
entry_point = "handler"
source {
storage_source {
bucket = google_storage_bucket.function_source.name
object = google_storage_bucket_object.function_code.name
}
}
}
service_config {
max_instance_count = 100
available_memory = "512Mi"
timeout_seconds = 60
service_account_email = google_service_account.mcp_server.email
environment_variables = {
NODE_ENV = var.environment
FIRESTORE_PROJECT = var.project_id
OAUTH_CLIENT_ID = var.oauth_client_id
}
secret_environment_variables {
key = "OAUTH_CLIENT_SECRET"
project_id = var.project_id
secret = google_secret_manager_secret.oauth_secret.secret_id
version = "latest"
}
}
}
# Service Account for Cloud Function
resource "google_service_account" "mcp_server" {
account_id = "${var.app_name}-mcp-server"
display_name = "MCP Server Service Account"
}
resource "google_project_iam_member" "mcp_firestore" {
project = var.project_id
role = "roles/datastore.user"
member = "serviceAccount:${google_service_account.mcp_server.email}"
}
# API Gateway
resource "google_api_gateway_api" "chatgpt_api" {
provider = google-beta
api_id = "${var.app_name}-api"
}
resource "google_api_gateway_api_config" "chatgpt_api" {
provider = google-beta
api = google_api_gateway_api.chatgpt_api.api_id
api_config_id = "${var.app_name}-config-${var.environment}"
openapi_documents {
document {
path = "openapi.yaml"
contents = base64encode(templatefile("${path.module}/openapi.yaml", {
function_url = google_cloudfunctions2_function.mcp_server.service_config[0].uri
}))
}
}
}
resource "google_api_gateway_gateway" "chatgpt_gateway" {
provider = google-beta
gateway_id = "${var.app_name}-gateway"
api_config = google_api_gateway_api_config.chatgpt_api.id
region = var.region
}
# Firestore Database (assuming already created)
resource "google_firestore_index" "user_sessions" {
collection = "sessions"
fields {
field_path = "userId"
order = "ASCENDING"
}
fields {
field_path = "createdAt"
order = "DESCENDING"
}
}
# Cloud CDN for Widgets
resource "google_compute_backend_bucket" "widgets" {
name = "${var.app_name}-widgets-backend"
bucket_name = google_storage_bucket.widgets.name
enable_cdn = true
cdn_policy {
cache_mode = "CACHE_ALL_STATIC"
client_ttl = 3600
default_ttl = 86400
max_ttl = 31536000
negative_caching = true
serve_while_stale = 86400
}
}
resource "google_storage_bucket" "widgets" {
name = "${var.app_name}-widgets-${var.environment}"
location = var.region
force_destroy = false
uniform_bucket_level_access = true
cors {
origin = ["https://chatgpt.com"]
method = ["GET", "HEAD"]
response_header = ["Content-Type"]
max_age_seconds = 3600
}
}
# Outputs
output "mcp_server_url" {
value = google_api_gateway_gateway.chatgpt_gateway.default_hostname
description = "MCP Server API Gateway URL"
}
output "widget_cdn_url" {
value = "https://${google_compute_backend_bucket.widgets.name}"
description = "Widget CDN URL"
}
output "firestore_project" {
value = var.project_id
description = "Firestore project ID"
}
Reusable Terraform Modules
Create a standardized MCP server module for consistent deployments:
# modules/mcp-server/main.tf
variable "app_name" {
type = string
description = "Application name"
}
variable "environment" {
type = string
description = "Environment (dev, staging, production)"
}
variable "runtime" {
type = string
default = "nodejs20.x"
description = "Lambda runtime"
}
variable "memory_size" {
type = number
default = 512
description = "Lambda memory in MB"
}
variable "source_code_path" {
type = string
description = "Path to Lambda deployment package"
}
variable "environment_variables" {
type = map(string)
default = {}
description = "Environment variables for Lambda"
}
# Lambda Function
resource "aws_lambda_function" "mcp_server" {
function_name = "${var.app_name}-mcp-${var.environment}"
runtime = var.runtime
handler = "index.handler"
memory_size = var.memory_size
timeout = 30
filename = var.source_code_path
source_code_hash = filebase64sha256(var.source_code_path)
role = aws_iam_role.execution.arn
environment {
variables = merge(
var.environment_variables,
{
APP_NAME = var.app_name
ENVIRONMENT = var.environment
}
)
}
tracing_config {
mode = "Active"
}
}
# IAM Role with minimal permissions
resource "aws_iam_role" "execution" {
name = "${var.app_name}-mcp-execution-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "basic_execution" {
role = aws_iam_role.execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "mcp_server" {
name = "/aws/lambda/${aws_lambda_function.mcp_server.function_name}"
retention_in_days = 30
}
# Outputs
output "function_arn" {
value = aws_lambda_function.mcp_server.arn
description = "Lambda function ARN"
}
output "function_name" {
value = aws_lambda_function.mcp_server.function_name
description = "Lambda function name"
}
output "invoke_arn" {
value = aws_lambda_function.mcp_server.invoke_arn
description = "Lambda invoke ARN for API Gateway"
}
# Usage in root module:
# module "production_mcp" {
# source = "./modules/mcp-server"
#
# app_name = "chatgpt-fitness-assistant"
# environment = "production"
# source_code_path = "../dist/mcp-server.zip"
#
# environment_variables = {
# DYNAMODB_TABLE = aws_dynamodb_table.state.name
# OAUTH_CLIENT_ID = var.oauth_client_id
# }
# }
Pulumi Implementation for ChatGPT Apps
AWS with Pulumi TypeScript
Pulumi brings type safety and programming language features to infrastructure:
// pulumi/aws/index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as path from "path";
const config = new pulumi.Config();
const environment = pulumi.getStack();
const appName = config.require("appName");
// DynamoDB Table for App State
const appStateTable = new aws.dynamodb.Table("app-state", {
name: `${appName}-state-${environment}`,
billingMode: "PAY_PER_REQUEST",
hashKey: "userId",
rangeKey: "sessionId",
attributes: [
{ name: "userId", type: "S" },
{ name: "sessionId", type: "S" },
{ name: "createdAt", type: "N" },
],
globalSecondaryIndexes: [{
name: "createdAt-index",
hashKey: "userId",
rangeKey: "createdAt",
projectionType: "ALL",
}],
pointInTimeRecovery: { enabled: true },
serverSideEncryption: { enabled: true },
tags: {
Environment: environment,
ManagedBy: "Pulumi",
},
});
// IAM Role for Lambda
const lambdaRole = new aws.iam.Role("mcp-lambda-role", {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
Service: "lambda.amazonaws.com",
}),
});
new aws.iam.RolePolicyAttachment("lambda-basic-execution", {
role: lambdaRole.name,
policyArn: aws.iam.ManagedPolicy.AWSLambdaBasicExecutionRole,
});
// DynamoDB access policy
const dynamodbPolicy = new aws.iam.RolePolicy("lambda-dynamodb", {
role: lambdaRole.id,
policy: pulumi.all([appStateTable.arn]).apply(([tableArn]) =>
JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Action: [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
"dynamodb:Scan",
],
Resource: [tableArn, `${tableArn}/index/*`],
}],
})
),
});
// Lambda Function for MCP Server
const mcpServerLambda = new aws.lambda.Function("mcp-server", {
name: `${appName}-mcp-server-${environment}`,
runtime: "nodejs20.x",
handler: "index.handler",
memorySize: 512,
timeout: 30,
code: new pulumi.asset.FileArchive("../../dist/mcp-server.zip"),
role: lambdaRole.arn,
environment: {
variables: {
NODE_ENV: environment,
DYNAMODB_TABLE: appStateTable.name,
OAUTH_CLIENT_ID: config.requireSecret("oauthClientId"),
OAUTH_CLIENT_SECRET: config.requireSecret("oauthClientSecret"),
},
},
tracingConfig: {
mode: "Active",
},
});
// CloudWatch Log Group
const logGroup = new aws.cloudwatch.LogGroup("mcp-server-logs", {
name: pulumi.interpolate`/aws/lambda/${mcpServerLambda.name}`,
retentionInDays: 30,
});
// API Gateway REST API
const api = new aws.apigateway.RestApi("chatgpt-api", {
name: `${appName}-api-${environment}`,
description: "ChatGPT App MCP Server API",
});
const mcpResource = new aws.apigateway.Resource("mcp-resource", {
restApi: api.id,
parentId: api.rootResourceId,
pathPart: "mcp",
});
const mcpMethod = new aws.apigateway.Method("mcp-post-method", {
restApi: api.id,
resourceId: mcpResource.id,
httpMethod: "POST",
authorization: "NONE",
});
const mcpIntegration = new aws.apigateway.Integration("mcp-integration", {
restApi: api.id,
resourceId: mcpResource.id,
httpMethod: mcpMethod.httpMethod,
integrationHttpMethod: "POST",
type: "AWS_PROXY",
uri: mcpServerLambda.invokeArn,
});
const lambdaPermission = new aws.lambda.Permission("api-gateway-invoke", {
action: "lambda:InvokeFunction",
function: mcpServerLambda.name,
principal: "apigateway.amazonaws.com",
sourceArn: pulumi.interpolate`${api.executionArn}/*/*`,
});
const deployment = new aws.apigateway.Deployment("api-deployment", {
restApi: api.id,
stageName: environment,
}, { dependsOn: [mcpIntegration] });
// S3 Bucket for Widgets
const widgetsBucket = new aws.s3.Bucket("widgets-bucket", {
bucket: `${appName}-widgets-${environment}`,
corsRules: [{
allowedHeaders: ["*"],
allowedMethods: ["GET", "HEAD"],
allowedOrigins: ["https://chatgpt.com"],
maxAgeSeconds: 3600,
}],
});
// CloudFront Distribution
const originAccessIdentity = new aws.cloudfront.OriginAccessIdentity("widgets-oai", {
comment: "OAI for ChatGPT widgets",
});
const bucketPolicy = new aws.s3.BucketPolicy("widgets-bucket-policy", {
bucket: widgetsBucket.id,
policy: pulumi.all([widgetsBucket.arn, originAccessIdentity.iamArn]).apply(
([bucketArn, oaiArn]) =>
JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: { AWS: oaiArn },
Action: "s3:GetObject",
Resource: `${bucketArn}/*`,
}],
})
),
});
const cdn = new aws.cloudfront.Distribution("widgets-cdn", {
enabled: true,
isIpv6Enabled: true,
defaultRootObject: "index.html",
priceClass: "PriceClass_100",
origins: [{
domainName: widgetsBucket.bucketRegionalDomainName,
originId: "S3-widgets",
s3OriginConfig: {
originAccessIdentity: originAccessIdentity.cloudfrontAccessIdentityPath,
},
}],
defaultCacheBehavior: {
allowedMethods: ["GET", "HEAD", "OPTIONS"],
cachedMethods: ["GET", "HEAD"],
targetOriginId: "S3-widgets",
viewerProtocolPolicy: "redirect-to-https",
compress: true,
forwardedValues: {
queryString: false,
cookies: { forward: "none" },
},
minTtl: 0,
defaultTtl: 86400,
maxTtl: 31536000,
},
restrictions: {
geoRestriction: {
restrictionType: "none",
},
},
viewerCertificate: {
cloudfrontDefaultCertificate: true,
},
});
// Exports
export const mcpServerUrl = pulumi.interpolate`${deployment.invokeUrl}/mcp`;
export const widgetCdnUrl = pulumi.interpolate`https://${cdn.domainName}`;
export const dynamodbTableName = appStateTable.name;
export const lambdaFunctionArn = mcpServerLambda.arn;
GCP with Pulumi TypeScript
Deploy to Google Cloud Platform using Pulumi:
// pulumi/gcp/index.ts
import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";
const config = new pulumi.Config();
const environment = pulumi.getStack();
const appName = config.require("appName");
const projectId = config.require("gcpProjectId");
const region = config.get("region") || "us-central1";
// Service Account for Cloud Function
const mcpServiceAccount = new gcp.serviceaccount.Account("mcp-service-account", {
accountId: `${appName}-mcp-server`,
displayName: "MCP Server Service Account",
});
// Grant Firestore access
const firestoreBinding = new gcp.projects.IAMMember("mcp-firestore-access", {
project: projectId,
role: "roles/datastore.user",
member: pulumi.interpolate`serviceAccount:${mcpServiceAccount.email}`,
});
// Secret Manager for OAuth credentials
const oauthSecret = new gcp.secretmanager.Secret("oauth-client-secret", {
secretId: `${appName}-oauth-secret`,
replication: {
auto: {},
},
});
const oauthSecretVersion = new gcp.secretmanager.SecretVersion("oauth-secret-version", {
secret: oauthSecret.id,
secretData: config.requireSecret("oauthClientSecret"),
});
const secretAccessBinding = new gcp.secretmanager.SecretIamMember("secret-access", {
secretId: oauthSecret.id,
role: "roles/secretmanager.secretAccessor",
member: pulumi.interpolate`serviceAccount:${mcpServiceAccount.email}`,
});
// Storage Bucket for Function Source Code
const sourceBucket = new gcp.storage.Bucket("function-source", {
name: `${appName}-function-source-${environment}`,
location: region,
uniformBucketLevelAccess: true,
});
const functionSource = new gcp.storage.BucketObject("mcp-server-source", {
bucket: sourceBucket.name,
source: new pulumi.asset.FileArchive("../../dist/mcp-server.zip"),
});
// Cloud Function Gen 2
const mcpFunction = new gcp.cloudfunctionsv2.Function("mcp-server", {
name: `${appName}-mcp-server-${environment}`,
location: region,
description: "ChatGPT MCP Server",
buildConfig: {
runtime: "nodejs20",
entryPoint: "handler",
source: {
storageSource: {
bucket: sourceBucket.name,
object: functionSource.name,
},
},
},
serviceConfig: {
maxInstanceCount: 100,
availableMemory: "512Mi",
timeoutSeconds: 60,
serviceAccountEmail: mcpServiceAccount.email,
environmentVariables: {
NODE_ENV: environment,
FIRESTORE_PROJECT: projectId,
OAUTH_CLIENT_ID: config.require("oauthClientId"),
},
secretEnvironmentVariables: [{
key: "OAUTH_CLIENT_SECRET",
projectId: projectId,
secret: oauthSecret.secretId,
version: "latest",
}],
},
});
// Allow unauthenticated invocations (API Gateway will handle auth)
const invoker = new gcp.cloudfunctionsv2.FunctionIamMember("mcp-invoker", {
location: mcpFunction.location,
cloudFunction: mcpFunction.name,
role: "roles/cloudfunctions.invoker",
member: "allUsers",
});
// Firestore Composite Index
const sessionIndex = new gcp.firestore.Index("session-index", {
collection: "sessions",
fields: [
{ fieldPath: "userId", order: "ASCENDING" },
{ fieldPath: "createdAt", order: "DESCENDING" },
],
});
// Storage Bucket for Widgets
const widgetsBucket = new gcp.storage.Bucket("widgets", {
name: `${appName}-widgets-${environment}`,
location: region,
uniformBucketLevelAccess: true,
cors: [{
origins: ["https://chatgpt.com"],
methods: ["GET", "HEAD"],
responseHeaders: ["Content-Type"],
maxAgeSeconds: 3600,
}],
});
// Make widgets publicly readable
const widgetsBucketIam = new gcp.storage.BucketIAMBinding("widgets-public-read", {
bucket: widgetsBucket.name,
role: "roles/storage.objectViewer",
members: ["allUsers"],
});
// Cloud CDN Backend Bucket
const widgetsBackend = new gcp.compute.BackendBucket("widgets-backend", {
name: `${appName}-widgets-backend`,
bucketName: widgetsBucket.name,
enableCdn: true,
cdnPolicy: {
cacheMode: "CACHE_ALL_STATIC",
clientTtl: 3600,
defaultTtl: 86400,
maxTtl: 31536000,
negativeCaching: true,
serveWhileStale: 86400,
},
});
// Exports
export const mcpServerUrl = mcpFunction.serviceConfig.uri;
export const widgetsBucketName = widgetsBucket.name;
export const widgetCdnUrl = pulumi.interpolate`https://storage.googleapis.com/${widgetsBucket.name}`;
export const functionServiceAccount = mcpServiceAccount.email;
Pulumi Component Resources
Create reusable component abstractions:
// pulumi/components/McpServerComponent.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
export interface McpServerArgs {
appName: string;
environment: string;
runtime?: string;
memorySize?: number;
sourceCodePath: string;
environmentVariables?: Record<string, pulumi.Input<string>>;
dynamodbTableArn?: pulumi.Input<string>;
}
export class McpServerComponent extends pulumi.ComponentResource {
public readonly lambdaFunction: aws.lambda.Function;
public readonly logGroup: aws.cloudwatch.LogGroup;
public readonly apiGateway: aws.apigatewayv2.Api;
public readonly apiUrl: pulumi.Output<string>;
constructor(name: string, args: McpServerArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:chatgpt:McpServer", name, {}, opts);
const defaultOpts = { parent: this };
// IAM Role
const role = new aws.iam.Role(`${name}-role`, {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
Service: "lambda.amazonaws.com",
}),
}, defaultOpts);
new aws.iam.RolePolicyAttachment(`${name}-basic-execution`, {
role: role.name,
policyArn: aws.iam.ManagedPolicy.AWSLambdaBasicExecutionRole,
}, defaultOpts);
// DynamoDB access if table provided
if (args.dynamodbTableArn) {
new aws.iam.RolePolicy(`${name}-dynamodb`, {
role: role.id,
policy: pulumi.output(args.dynamodbTableArn).apply(tableArn =>
JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Action: [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
],
Resource: [tableArn, `${tableArn}/index/*`],
}],
})
),
}, defaultOpts);
}
// Lambda Function
this.lambdaFunction = new aws.lambda.Function(`${name}-function`, {
name: `${args.appName}-mcp-${args.environment}`,
runtime: args.runtime || "nodejs20.x",
handler: "index.handler",
memorySize: args.memorySize || 512,
timeout: 30,
code: new pulumi.asset.FileArchive(args.sourceCodePath),
role: role.arn,
environment: {
variables: {
...args.environmentVariables,
APP_NAME: args.appName,
ENVIRONMENT: args.environment,
},
},
tracingConfig: { mode: "Active" },
}, defaultOpts);
// CloudWatch Logs
this.logGroup = new aws.cloudwatch.LogGroup(`${name}-logs`, {
name: pulumi.interpolate`/aws/lambda/${this.lambdaFunction.name}`,
retentionInDays: 30,
}, defaultOpts);
// API Gateway HTTP API (simpler than REST API)
this.apiGateway = new aws.apigatewayv2.Api(`${name}-api`, {
protocolType: "HTTP",
name: `${args.appName}-api-${args.environment}`,
}, defaultOpts);
const integration = new aws.apigatewayv2.Integration(`${name}-integration`, {
apiId: this.apiGateway.id,
integrationType: "AWS_PROXY",
integrationUri: this.lambdaFunction.arn,
payloadFormatVersion: "2.0",
}, defaultOpts);
const route = new aws.apigatewayv2.Route(`${name}-route`, {
apiId: this.apiGateway.id,
routeKey: "POST /mcp",
target: pulumi.interpolate`integrations/${integration.id}`,
}, defaultOpts);
const stage = new aws.apigatewayv2.Stage(`${name}-stage`, {
apiId: this.apiGateway.id,
name: "$default",
autoDeploy: true,
}, defaultOpts);
new aws.lambda.Permission(`${name}-api-permission`, {
action: "lambda:InvokeFunction",
function: this.lambdaFunction.name,
principal: "apigateway.amazonaws.com",
sourceArn: pulumi.interpolate`${this.apiGateway.executionArn}/*/*`,
}, defaultOpts);
this.apiUrl = pulumi.interpolate`${this.apiGateway.apiEndpoint}/mcp`;
this.registerOutputs({
lambdaFunction: this.lambdaFunction,
logGroup: this.logGroup,
apiGateway: this.apiGateway,
apiUrl: this.apiUrl,
});
}
}
// Usage:
// const mcpServer = new McpServerComponent("production-mcp", {
// appName: "chatgpt-fitness-assistant",
// environment: "production",
// sourceCodePath: "../dist/mcp-server.zip",
// environmentVariables: {
// OAUTH_CLIENT_ID: config.requireSecret("oauthClientId"),
// },
// dynamodbTableArn: appStateTable.arn,
// });
//
// export const mcpUrl = mcpServer.apiUrl;
CI/CD Integration
Terraform GitHub Actions Workflow
Automate Terraform deployments with comprehensive validation:
# .github/workflows/terraform-deploy.yml
name: Terraform Deploy
on:
push:
branches: [main]
paths:
- 'terraform/**'
- '.github/workflows/terraform-deploy.yml'
pull_request:
branches: [main]
paths:
- 'terraform/**'
env:
TF_VERSION: 1.6.0
AWS_REGION: us-east-1
jobs:
validate:
name: Validate Terraform
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Format Check
run: terraform fmt -check -recursive terraform/
- name: Terraform Init
working-directory: terraform/aws
run: terraform init -backend=false
- name: Terraform Validate
working-directory: terraform/aws
run: terraform validate
- name: Run tflint
uses: terraform-linters/setup-tflint@v4
with:
tflint_version: latest
- name: Init tflint
working-directory: terraform/aws
run: tflint --init
- name: Run tflint
working-directory: terraform/aws
run: tflint
plan:
name: Terraform Plan
runs-on: ubuntu-latest
needs: validate
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
working-directory: terraform/aws
run: terraform init
- name: Terraform Plan
working-directory: terraform/aws
run: |
terraform plan \
-var="oauth_client_id=${{ secrets.OAUTH_CLIENT_ID }}" \
-var="oauth_client_secret=${{ secrets.OAUTH_CLIENT_SECRET }}" \
-out=tfplan
- name: Upload Plan
uses: actions/upload-artifact@v4
with:
name: terraform-plan
path: terraform/aws/tfplan
retention-days: 5
- name: Comment Plan on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('terraform/aws/tfplan.txt', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Terraform Plan\n\`\`\`hcl\n${plan}\n\`\`\``
});
apply:
name: Terraform Apply
runs-on: ubuntu-latest
needs: validate
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
working-directory: terraform/aws
run: terraform init
- name: Terraform Apply
working-directory: terraform/aws
run: |
terraform apply -auto-approve \
-var="oauth_client_id=${{ secrets.OAUTH_CLIENT_ID }}" \
-var="oauth_client_secret=${{ secrets.OAUTH_CLIENT_SECRET }}"
- name: Output Results
working-directory: terraform/aws
run: terraform output -json > outputs.json
- name: Upload Outputs
uses: actions/upload-artifact@v4
with:
name: terraform-outputs
path: terraform/aws/outputs.json
Pulumi GitHub Actions Workflow
Deploy with Pulumi's preview and update workflow:
# .github/workflows/pulumi-deploy.yml
name: Pulumi Deploy
on:
push:
branches: [main]
paths:
- 'pulumi/**'
- '.github/workflows/pulumi-deploy.yml'
pull_request:
branches: [main]
paths:
- 'pulumi/**'
env:
PULUMI_VERSION: 3.100.0
NODE_VERSION: 20
jobs:
preview:
name: Pulumi Preview
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
working-directory: pulumi/aws
run: npm ci
- name: Setup Pulumi
uses: pulumi/actions@v5
with:
pulumi-version: ${{ env.PULUMI_VERSION }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Pulumi Preview
uses: pulumi/actions@v5
with:
command: preview
stack-name: production
work-dir: pulumi/aws
comment-on-pr: true
github-token: ${{ secrets.GITHUB_TOKEN }}
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
deploy:
name: Pulumi Update
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
working-directory: pulumi/aws
run: npm ci
- name: Setup Pulumi
uses: pulumi/actions@v5
with:
pulumi-version: ${{ env.PULUMI_VERSION }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Pulumi Update
uses: pulumi/actions@v5
with:
command: up
stack-name: production
work-dir: pulumi/aws
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
- name: Export Stack Outputs
working-directory: pulumi/aws
run: pulumi stack output --json > outputs.json
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
- name: Upload Outputs
uses: actions/upload-artifact@v4
with:
name: pulumi-outputs
path: pulumi/aws/outputs.json
Infrastructure Validation Script
Validate infrastructure changes before deployment:
#!/bin/bash
# scripts/validate-infrastructure.sh
set -e
echo "🔍 Validating infrastructure configuration..."
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check if Terraform is installed
if command -v terraform &> /dev/null; then
echo -e "${GREEN}✓${NC} Terraform installed: $(terraform version -json | jq -r '.terraform_version')"
else
echo -e "${RED}✗${NC} Terraform not installed"
exit 1
fi
# Validate Terraform configuration
echo ""
echo "Validating Terraform configuration..."
cd terraform/aws
terraform fmt -check -recursive . || {
echo -e "${RED}✗${NC} Terraform formatting issues detected"
echo "Run: terraform fmt -recursive ."
exit 1
}
echo -e "${GREEN}✓${NC} Terraform formatting validated"
terraform init -backend=false
terraform validate || {
echo -e "${RED}✗${NC} Terraform validation failed"
exit 1
}
echo -e "${GREEN}✓${NC} Terraform configuration valid"
# Check for security issues with tfsec
if command -v tfsec &> /dev/null; then
echo ""
echo "Running tfsec security scan..."
tfsec . --minimum-severity MEDIUM || {
echo -e "${YELLOW}⚠${NC} Security issues detected"
}
else
echo -e "${YELLOW}⚠${NC} tfsec not installed, skipping security scan"
fi
# Estimate cost with Infracost (if available)
if command -v infracost &> /dev/null; then
echo ""
echo "Estimating infrastructure cost..."
infracost breakdown --path . --format table
else
echo -e "${YELLOW}⚠${NC} Infracost not installed, skipping cost estimation"
fi
cd ../..
echo ""
echo -e "${GREEN}✅ Infrastructure validation complete${NC}"
State Management Best Practices
Remote State Configuration
Never store state locally in production. Configure remote backends:
# terraform/backend.tf
terraform {
backend "s3" {
bucket = "chatgpt-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
# Enable versioning for state history
versioning = true
}
}
# Create S3 bucket and DynamoDB table for state management
resource "aws_s3_bucket" "terraform_state" {
bucket = "chatgpt-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
lifecycle {
prevent_destroy = true
}
}
State Locking and Versioning
State locking prevents concurrent modifications that could corrupt your infrastructure:
- Terraform: DynamoDB table provides automatic locking
- Pulumi: Built-in locking through Pulumi Service or self-hosted backend
State versioning enables rollback to previous infrastructure states:
# Terraform: Restore previous state version
terraform state pull > backup.tfstate
aws s3 cp s3://chatgpt-terraform-state/production/terraform.tfstate.backup current.tfstate
terraform state push current.tfstate
# Pulumi: Rollback to previous update
pulumi stack history
pulumi stack select production
pulumi up --target-dependents --refresh=false
Production Deployment Checklist
Before deploying infrastructure changes to production:
- Test in staging environment with identical configuration
- Review Terraform plan or Pulumi preview for unexpected changes
- Validate security rules (Firestore, IAM, API Gateway authorization)
- Check resource quotas (Lambda concurrent executions, API Gateway rate limits)
- Verify OAuth redirect URIs match ChatGPT connector platform requirements
- Confirm CDN cache invalidation for widget updates
- Enable monitoring (CloudWatch alarms, GCP monitoring alerts)
- Document rollback procedures for each resource type
- Schedule deployment during low-traffic windows
- Notify team members of deployment timing
Conclusion
Infrastructure as code transforms ChatGPT app deployment from manual error-prone processes to automated, reproducible workflows. Whether you choose Terraform's declarative HCL or Pulumi's programming language flexibility, both tools provide production-ready solutions for managing MCP servers, API gateways, databases, and CDN distributions.
The code examples in this guide provide complete, working implementations you can adapt to your ChatGPT application architecture. Start with a single environment, validate your configuration, then expand to multi-region deployments and complex workflows.
Ready to automate your ChatGPT app infrastructure? MakeAIHQ provides infrastructure templates, CI/CD integration guides, and deployment automation for ChatGPT applications. Sign up for a free trial and deploy your first ChatGPT app with infrastructure as code in under 48 hours.
Related Resources
- Multi-Region Deployment Strategies for ChatGPT Apps - Geographic distribution and failover
- Zero-Downtime Deployments for ChatGPT Applications - Blue-green and canary deployments
- Enterprise ChatGPT Applications - Production-grade ChatGPT app development
- ChatGPT Applications Guide - Comprehensive development guide
- Terraform Best Practices - Official Terraform recommendations
- Pulumi Documentation - Complete Pulumi guides and tutorials
- Infrastructure as Code Patterns - AWS guidance on IaC tools