big-family-16359
09/15/2025, 7:34 PMerror: awsx:ec2:Vpc resource 'enterprise-dev-main-vpc' has a problem: Cannot read properties of undefined (reading 'length')
error: awsx:ec2:Vpc resource 'shared-dev-main-vpc' has a problem: Cannot read properties of undefined (reading 'length')
error: TypeError: Cannot read properties of undefined (reading 'length')
at Vpc.getDefaultAzs (/snapshot/awsx/bin/ec2/vpc.js:285:26)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at Vpc.initialize (/snapshot/awsx/bin/ec2/vpc.js:51:103)
error: TypeError: Cannot read properties of undefined (reading 'length')
at Vpc.getDefaultAzs (/snapshot/awsx/bin/ec2/vpc.js:285:26)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at Vpc.initialize (/snapshot/awsx/bin/ec2/vpc.js:51:103)
I'll leave some code in the threads to prevent clogging the main channel, but, for debugging reasons, when I isolate what is actually causing issues, it's only when I remove my provider that things start working.
I'm using AWS OUs for my multi-tenanted architecture and created a parent organization with SCPs. The SCP then uses a Provider to create a VPC in an OU.big-family-16359
09/15/2025, 7:35 PM// Step 1: Create the AWS Organizations structure
const orgStructure = createOrganizationsStructure("<mailto:aws-tools@chaeth.com|aws-tools@chaeth.com>")
// Step 2: Create and attach Service Control Policies
const policies = createServiceControlPolicies()
attachSCPsToOUs(policies, orgStructure.toolsOU, orgStructure.tenantsParentOU)
// Step 3: Create provider for Tools account
const toolsProvider = new aws.Provider("tools-provider", {
region: region,
assumeRoles: [{
roleArn: orgStructure.toolsAccount.id.apply(
id => `arn:aws:iam::${id}:role/OrganizationAccountAccessRole`
),
}],
})
// Step 4: Deploy central resources in Tools account
const toolsResources = createCentralDeployer(
"ChaethAI",
"LibreChat",
undefined,
toolsProvider
)
// Step 5: Create tenant accounts and deploy infrastructure
const tenantOutputs: Record<string, any> = {}
for (const tenant of tenants) {
// Create tenant OU and account
const tenantAccountInfo = createTenantOUAndAccount(
tenant,
orgStructure.tenantsParentOU.id,
)
// Create provider for this tenant account
const tenantProvider = new aws.Provider(`${tenant.tenantId}-provider`, {
region: region,
assumeRoles: [
{
roleArn: tenantAccountInfo.tenantAccount.id.apply(
id => `arn:aws:iam::${id}:role/OrganizationAccountAccessRole`
),
}
],
}, {
dependsOn: [tenantAccountInfo.tenantAccount],
})
// Deploy tenant IAM resources
const tenantIAMResources = createTenantIAMResources(
tenant.tenantId,
orgStructure.toolsAccount.id,
tenantAccountInfo.tenantOU,
tenantProvider
)
// Deploy tenant infrastructure
const tenantInfra = new TenantInfrastructure(
`${tenant.tenantId}-infra`,
{
tenant: {
...tenant,
region: region,
},
environment: stackName,
sharedConfig: {
...sharedConfig,
libreChatEcrUrl: toolsResources.ecrRepository.repositoryUrl,
},
},
{
parent: tenantAccountInfo.tenantOU,
provider: tenantProvider,
dependsOn: [tenantAccountInfo.tenantOU, tenantProvider],
}
)
export function createOrganizationsStructure(
toolsAccountEmail: string,
parent?: pulumi.ComponentResource,
): OrganizationsOutputs {
// Create or get the organization (this should already exist)
const organization = new aws.organizations.Organization(
"chaeth",
{
awsServiceAccessPrincipals: [
"<http://cloudtrail.amazonaws.com|cloudtrail.amazonaws.com>",
"<http://config.amazonaws.com|config.amazonaws.com>",
"<http://sso.amazonaws.com|sso.amazonaws.com>",
],
featureSet: "ALL",
},
{ parent },
)
const rootId = organization.roots[0].id
// Create Tools OU for centralized resources
const toolsOU = new aws.organizations.OrganizationalUnit(
"tools-ou",
{
name: "Tools",
parentId: rootId,
tags: {
Name: "Tools OU",
Purpose: "Centralized tools and ECR",
ManagedBy: "Pulumi",
},
},
{ parent, dependsOn: [organization] },
)
// Create Tools account
const toolsAccount = new aws.organizations.Account(
"tools-account",
{
name: "Chaeth Tools",
email: toolsAccountEmail,
parentId: toolsOU.id,
roleName: "OrganizationAccountAccessRole",
tags: {
Name: "Tools Account",
Purpose: "Central ECR and deployment tools",
ManagedBy: "Pulumi",
},
},
{
parent,
dependsOn: [toolsOU],
customTimeouts: {
create: "20m",
},
},
)
// Create Tenants OU as parent for all tenant OUs
const tenantsParentOU = new aws.organizations.OrganizationalUnit(
"tenants-parent-ou",
{
name: "Tenants",
parentId: rootId,
tags: {
Name: "Tenants Parent OU",
Purpose: "Parent OU for all tenant OUs",
ManagedBy: "Pulumi",
},
},
{ parent, dependsOn: [organization] },
)
return {
organization,
rootId,
toolsOU,
toolsAccount,
tenantsParentOU,
}
}
export function attachSCPsToOUs(
policies: { [key: string]: aws.organizations.Policy },
toolsOU: aws.organizations.OrganizationalUnit,
tenantsParentOU: aws.organizations.OrganizationalUnit,
parent?: pulumi.ComponentResource,
): void {
// Attach tools policy to tools OU
new aws.organizations.PolicyAttachment(
"tools-scp-attachment",
{
policyId: policies.tools.id,
targetId: toolsOU.id,
},
{ parent },
)
// Attach tenant policy to tenants parent OU (will inherit to all tenant OUs)
new aws.organizations.PolicyAttachment(
"tenant-scp-attachment",
{
policyId: policies.tenant.id,
targetId: tenantsParentOU.id,
},
{ parent },
)
}big-family-16359
09/15/2025, 7:36 PMexport function createTenantOUAndAccount(
tenant: TenantConfig,
tenantsParentOUId: pulumi.Output<string>,
parent?: pulumi.ComponentResource,
): TenantAccountOutputs {
// Create OU for the tenant
const tenantOU = new aws.organizations.OrganizationalUnit(
`${tenant.tenantId}-ou`,
{
name: `Tenant-${tenant.tenantId}`,
parentId: tenantsParentOUId,
tags: {
Name: `${tenant.tenantId} OU`,
TenantId: tenant.tenantId,
TenantName: tenant.tenantName,
ManagedBy: "Pulumi",
},
},
{ parent },
)
// Create AWS account for the tenant
const accountEmail = `aws-tenant+${tenant.tenantId}@chaeth.com`
const tenantAccount = new aws.organizations.Account(
`${tenant.tenantId}-account`,
{
name: `${tenant.tenantName} (${tenant.tenantId})`,
email: accountEmail,
parentId: tenantOU.id,
roleName: "OrganizationAccountAccessRole",
tags: {
TenantId: tenant.tenantId,
TenantName: tenant.tenantName,
Environment: "prod",
ManagedBy: "Pulumi",
},
},
{
parent,
dependsOn: [tenantOU],
customTimeouts: {
create: "20m",
},
},
)
return {
tenantOU,
tenantAccount,
}
}
export interface TenantIAMOutputs {
tenantDeployerRole: aws.iam.Role
tenantExecutionRole: aws.iam.Role
tenantTaskRole: aws.iam.Role
}
/**
* Creates IAM resources within a tenant account for cross-account deployment
*/
export function createTenantIAMResources(
tenantId: string,
toolsAccountId: pulumi.Output<string>,
parent?: pulumi.Resource,
provider?: pulumi.ProviderResource,
): TenantIAMOutputs {
const providerOpts = provider ? { provider } : {}
// ========================================
// TenantDeployer Role (for CI/CD)
// ========================================
// Trust policy: Only CentralDeployer from tools account can assume
const tenantDeployerAssumePolicy = aws.iam.getPolicyDocumentOutput({
statements: [
{
effect: "Allow",
principals: [
{
type: "AWS",
identifiers: [toolsAccountId.apply(id => `arn:aws:iam::${id}:role/CentralDeployer`)],
},
],
actions: ["sts:AssumeRole"],
conditions: [
{
test: "StringEquals",
variable: "sts:ExternalId",
values: ["chaeth-ci"],
},
],
},
],
})
const tenantDeployerRole = new aws.iam.Role(
`${tenantId}-TenantDeployer`,
{
name: "TenantDeployer",
assumeRolePolicy: tenantDeployerAssumePolicy.json,
tags: {
Name: "TenantDeployer",
TenantId: tenantId,
Purpose: "CrossAccountDeployment",
ManagedBy: "Pulumi",
},
},
{ parent, ...providerOpts },
)
// Full admin permissions for deployment
new aws.iam.RolePolicyAttachment(
`${tenantId}-TenantDeployer-AdminPolicy`,
{
role: tenantDeployerRole.name,
policyArn: "arn:aws:iam::aws:policy/AdministratorAccess",
},
{ parent, ...providerOpts },
)
// ========================================
// TenantExecutionRole (for ECS tasks)
// ========================================
const tenantExecutionRole = new aws.iam.Role(
`${tenantId}-TenantExecutionRole`,
{
name: "TenantExecutionRole",
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "<http://ecs-tasks.amazonaws.com|ecs-tasks.amazonaws.com>",
},
},
],
}),
tags: {
Name: "TenantExecutionRole",
TenantId: tenantId,
Purpose: "ECSTaskExecution",
ManagedBy: "Pulumi",
},
},
{ parent, ...providerOpts },
)
// Attach AWS managed policy for ECS task execution
new aws.iam.RolePolicyAttachment(
`${tenantId}-TenantExecutionRole-EcsPolicy`,
{
role: tenantExecutionRole.name,
policyArn: "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
},
{ parent, ...providerOpts },
)
// Add ECR pull permissions for central repository
const executionRoleEcrPolicy = new aws.iam.Policy(
`${tenantId}-TenantExecutionRoleEcrPolicy`,
{
name: "TenantExecutionRoleEcrPolicy",
policy: aws.iam
.getPolicyDocumentOutput({
statements: [
{
sid: "ECRCrossAccountPull",
effect: "Allow",
actions: [
"ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:GetAuthorizationToken",
],
resources: [
"*", // Authorization token requires *
toolsAccountId.apply(id => `arn:aws:ecr:*:${id}:repository/librechat`),
],
},
],
})
.json,
tags: {
Name: "TenantExecutionRoleEcrPolicy",
TenantId: tenantId,
ManagedBy: "Pulumi",
},
},
{ parent, ...providerOpts },
)
new aws.iam.RolePolicyAttachment(
`${tenantId}-TenantExecutionRole-EcrPolicy`,
{
role: tenantExecutionRole.name,
policyArn: executionRoleEcrPolicy.arn,
},
{ parent, ...providerOpts },
)
// ========================================
// TenantTaskRole (for ECS task runtime)
// ========================================
const tenantTaskRole = new aws.iam.Role(
`${tenantId}-TenantTaskRole`,
{
name: "TenantTaskRole",
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "<http://ecs-tasks.amazonaws.com|ecs-tasks.amazonaws.com>",
},
},
],
}),
tags: {
Name: "TenantTaskRole",
TenantId: tenantId,
Purpose: "ECSTaskRuntime",
ManagedBy: "Pulumi",
},
},
{ parent, ...providerOpts },
)
// Task role permissions (Secrets Manager, Parameter Store, etc.)
const taskRolePolicy = new aws.iam.Policy(
`${tenantId}-TenantTaskRolePolicy`,
{
name: "TenantTaskRolePolicy",
policy: aws.iam
.getPolicyDocumentOutput({
statements: [
{
sid: "ParameterStore",
effect: "Allow",
actions: [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
],
resources: [`arn:aws:ssm:*:*:parameter/*/${tenantId}/*`],
},
{
sid: "SecretsManager",
effect: "Allow",
actions: ["secretsmanager:GetSecretValue"],
resources: [`arn:aws:secretsmanager:*:*:secret:*`],
},
{
sid: "CloudWatchLogs",
effect: "Allow",
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
resources: ["*"],
},
{
sid: "S3Access",
effect: "Allow",
actions: [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
],
resources: [
`arn:aws:s3:::*`,
`arn:aws:s3:::*/*`,
],
},
],
})
.json,
tags: {
Name: "TenantTaskRolePolicy",
TenantId: tenantId,
ManagedBy: "Pulumi",
},
},
{ parent, ...providerOpts },
)
new aws.iam.RolePolicyAttachment(
`${tenantId}-TenantTaskRole-Policy`,
{
role: tenantTaskRole.name,
policyArn: taskRolePolicy.arn,
},
{ parent, ...providerOpts },
)
return {
tenantDeployerRole,
tenantExecutionRole,
tenantTaskRole,
}
}big-family-16359
09/15/2025, 7:36 PMexport class TenantInfrastructure extends pulumi.ComponentResource {
public readonly outputs: TenantInfrastructureOutputs
constructor(
name: string,
args: {
tenant: TenantConfig
environment: string
sharedConfig: SharedInfraConfig
},
opts?: pulumi.ComponentResourceOptions,
) {
super("chaeth:infrastructure:Tenant", name, {}, opts)
const { tenant, environment, sharedConfig } = args
const vpc = createTenantVpc(tenant, environment, this)
export function createTenantVpc(
tenant: TenantConfig,
environment: string,
parent?: pulumi.ComponentResource,
): VpcOutputs {
const namePrefix = `${tenant.tenantId}-${environment}`
const vpc = new awsx.ec2.Vpc(
`${namePrefix}-main-vpc`,
{
cidrBlock: "10.0.0.0/16",
numberOfAvailabilityZones: 2,
enableDnsHostnames: true,
enableDnsSupport: true,
natGateways: {
strategy:
environment === "prod"
? awsx.ec2.NatGatewayStrategy.OnePerAz
: awsx.ec2.NatGatewayStrategy.Single,
},
tags: {
...tenant.tags,
Name: "main-vpc",
TenantId: tenant.tenantId,
Environment: environment,
ManagedBy: "Pulumi",
},
},
{ parent },
)big-family-16359
09/15/2025, 7:43 PMlittle-cartoon-10569
09/15/2025, 11:26 PMbig-family-16359
09/16/2025, 1:09 AMaws.Provider The only way to check if the account was created was with
const toolsAccount = await awsNative.organizations.getAccount({
accountId: orgStructure.toolsAccount.id.get()
})
if (toolsAccount.status === "ACTIVE") { /* Perform VPC creation */ }
Unfortunately, I'm now blocked on figuring out how to configure the await (https://github.com/pulumi/pulumi/issues/5161) and nothing seems to work hahalittle-cartoon-10569
09/16/2025, 1:47 AMlittle-cartoon-10569
09/16/2025, 1:48 AMbig-family-16359
09/18/2025, 12:24 AMlittle-cartoon-10569
09/18/2025, 2:44 AMgetDefaultAzs().little-cartoon-10569
09/18/2025, 2:45 AMlittle-cartoon-10569
09/18/2025, 2:48 AMconst result = await aws.getAvailabilityZones(undefined, { parent: this });
if (result.names.length < desiredCount) {
...
Checking in the AWS SDK what result.names is supposed to contain when getAvailabilityZones(undefined) is called.little-cartoon-10569
09/18/2025, 2:58 AMaws:index/getAvailabilityZones:getAvailabilityZones with an empty arg object. Which I presume is the same as calling aws ec2 describe-availability-zones? What happens if you run that using the same profile / creds as Pulumi is using?big-family-16359
09/18/2025, 5:04 AMbig-family-16359
09/18/2025, 5:06 AMlittle-cartoon-10569
09/18/2025, 5:24 AMlittle-cartoon-10569
09/18/2025, 5:24 AMbig-family-16359
09/18/2025, 5:24 AMbig-family-16359
09/18/2025, 5:25 AMlittle-cartoon-10569
09/18/2025, 7:52 AMlittle-cartoon-10569
09/18/2025, 7:52 AMlittle-cartoon-10569
09/18/2025, 7:53 AMNo matter how you like to participate in developer communities, Pulumi wants to meet you there. If you want to meet other Pulumi users to share use-cases and best practices, contribute code or documentation, see us at an event, or just tell a story about something cool you did with Pulumi, you are part of our community.
Powered by