Hi Pulumi Community, Am Ramesh. Just starting on P...
# aws
m
Hi Pulumi Community, Am Ramesh. Just starting on Pulumi. I am working on AWS deployment but facing an issue when we trigger the git action which calls the pulumi to create tenant account, infra structure and applycation deployment. AWS has 10 subnet ids but Pulumi stack state showing only 6 and trying to create the subnet again and AWS throwing conflict. Below is the error details. Please suggest how to resove this issue. error: 1 error occurred: 143 * creating EC2 Subnet: InvalidSubnet.Conflict: The CIDR '172.31.128.0/20' conflicts with another subnet 144 status code: 400, request id: 1ccf5ebe-23bb-4964-baa8-71158a46a692 145 awsec2Subnet (gien-us-east-1c-subnet): 146 error: 1 error occurred: 147 * creating EC2 Subnet: InvalidSubnet.Conflict: The CIDR '172.31.96.0/20' conflicts with another subnet 148 status code: 400, request id: 1dbd471c-b19c-4796-902b-577b8c698db8 149 awsec2Subnet (gien-us-east-1f-subnet): 150 error: 1 error occurred: 151 * creating EC2 Subnet: InvalidSubnet.Conflict: The CIDR '172.31.144.0/20' conflicts with another subnet 152 status code: 400, request id: 98c013e2-9117-4f6f-b893-1a6f83368096 153 awsec2Subnet (gien-us-east-1d-subnet): 154 error: 1 error occurred: 155 * creating EC2 Subnet: InvalidSubnet.Conflict: The CIDR '172.31.112.0/20' conflicts with another subnet 156 status code: 400, request id: bf39dad4-5568-4827-b57e-b111c040046e 10:52 lets connect when you start today.
s
Can you post a snippet of your code (in code fences - triple backticks, please so it's more readable)?
m
Hi @stocky-restaurant-98004, thanks for the reply.
Copy code
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
import * as awsx from "@pulumi/awsx";
import { resourceNamePrefixer as p } from "@pick-mybrain/pmb-pulumi";

const adminStack = new pulumi.StackReference('pickmybrain/glob-infra/administrative');

const DEV_TLD_NAME = '<http://pmb.mobi|pmb.mobi>'
const DEV_TLD_ZONE_ID = 'Z0402468DWE5FS0XKI3T'

const PROD_TLD_NAME = 'pickmybrain.world'
const PROD_TLD_ZONE_ID = 'Z0178833WNXYMG3SH06P'

const availabilityZones = pulumi.output(aws.getAvailabilityZones({}));

// SHARED SERVICE INFRASTRUCTURE START
const vpc = new awsx.ec2.DefaultVpc(p('default-vpc'));

let currentCidr = 48;
const privateSubnets = availabilityZones.names.apply(zones =>
    zones.map(zoneName => {
        currentCidr += 16;
        if (currentCidr >= 256) {
            throw new Error("CIDR block third octet out of range");
        }
        return new aws.ec2.Subnet(p(`${zoneName}-subnet`), {
            vpcId: vpc.vpcId,
            cidrBlock: `172.31.${currentCidr}.0/20`,
            availabilityZone: zoneName,
        });
    })
);

// SHARED SERVICE INFRASTRUCTURE END

const devRootZone = new aws.route53.Zone(DEV_TLD_NAME, {
    name: DEV_TLD_NAME,
	comment: 'HostedZone created by Route53 Registrar'
}, {
	import: DEV_TLD_ZONE_ID, protect: true
});


const prodRootZone = new aws.route53.Zone(PROD_TLD_NAME, {
    name: PROD_TLD_NAME,
	comment: 'HostedZone from DNSSimple'
}, {
	import: PROD_TLD_ZONE_ID, protect: true
});

const prodRedirectBucket = new aws.s3.Bucket('prod-s3-redirect', {
    bucket: `${PROD_TLD_NAME}`,
    website: {
        redirectAllRequestsTo: `<https://www>.${PROD_TLD_NAME}`
		
    }
});

const prodRedirectAccessBlock = new aws.s3.BucketPublicAccessBlock('prod-s3-redirect-block', {
    bucket: prodRedirectBucket.id,
    blockPublicAcls: false,
    blockPublicPolicy: false,
});

const prodRedirectBucketAccessPolicy = new aws.s3.BucketPolicy(`yevai-www-s3-policy`, {
    bucket: prodRedirectBucket.id,
    policy: {
        Version: "2012-10-17",
        Statement: [
        {
            Effect: "Allow",
            Principal: "*",
            Action: ["s3:GetObject"],
            Resource: pulumi.interpolate`${prodRedirectBucket.arn}/*`
        }
    ]
    }
}, { dependsOn: prodRedirectAccessBlock });

const prodRedirectCdnCert = new aws.acm.Certificate('prod-s3-redirect', {
    domainName: PROD_TLD_NAME,
    validationMethod: "DNS"
});

let prodAcmValidationRecord = new aws.route53.Record('prod-acm-validation-record', {
    name: prodRedirectCdnCert.domainValidationOptions[0].resourceRecordName,
    records: [prodRedirectCdnCert.domainValidationOptions[0].resourceRecordValue],
    ttl: 60,
    type: prodRedirectCdnCert.domainValidationOptions[0].resourceRecordType,
    zoneId: prodRootZone.id,
}, { dependsOn: [prodRedirectCdnCert] });

let prodRedirectCdnCertValidation = new aws.acm.CertificateValidation('prod-s3-redirect-validation', {
    certificateArn: prodRedirectCdnCert.arn,
    validationRecordFqdns: [prodAcmValidationRecord.fqdn],
}, { dependsOn: [prodAcmValidationRecord] });

const prodRedirectCloudFront = new aws.cloudfront.Distribution('prod-redirect-cdn', {
    origins: prodRedirectBucket.websiteEndpoint.apply(endpoint => {
		return [{
			domainName: endpoint,
			originId: prodRedirectBucket.arn,
			customOriginConfig: {
				originProtocolPolicy: "http-only",
				httpPort: 80,
				httpsPort: 443,
				originSslProtocols: ['TLSv1.2']
			},
		}]
	}),
    enabled: true,
    isIpv6Enabled: true,
    aliases: [PROD_TLD_NAME],
	comment: "prod redirect CDN - do not touch",
    defaultCacheBehavior: {
        allowedMethods: ["GET", "HEAD"],
        cachedMethods: ["GET", "HEAD"],
        targetOriginId: prodRedirectBucket.arn,
        viewerProtocolPolicy: "redirect-to-https",
		forwardedValues: {
            cookies: { forward: "none" },
            queryString: false,
        },
    },
    priceClass: "PriceClass_100",
    viewerCertificate: {
        acmCertificateArn: prodRedirectCdnCert.arn,
        sslSupportMethod: "sni-only",
        minimumProtocolVersion: "TLSv1.2_2021",
    },
    restrictions: {
        geoRestriction: {
            restrictionType: "none",
        },
    },
}, { dependsOn: [prodRedirectCdnCertValidation]});

const prodRedirectAlias = new aws.route53.Record('prod-redirect-alias', {
    zoneId: prodRootZone.id,
    name: PROD_TLD_NAME,
    type: 'A',
    aliases: [{
        name: prodRedirectCloudFront.domainName,
        zoneId: prodRedirectCloudFront.hostedZoneId,
        evaluateTargetHealth: false,
    }]
});

// Refactor this to be:
// accept list of target arns
// accept list of target permissions
// bind to cross account roles / policies.

const route53CrossAccountTargetArns = [
	// devRootZone.arn, - TODO: deploy to this zone if the stack mode in ESC config is not production.
	prodRootZone.arn,
	"arn:aws:route53:::change/*"
]

const route53CrossAccountAllowedActions = [
	"route53:ChangeResourceRecordSets",
	"route53:GetChange",
	"route53:GetHostedZone",
	"route53:ListHostedZones",
	"route53:ListResourceRecordSets"
]

const crossAccountAllowedArns = adminStack.getOutput('crossAccountPermissionPolicyTargets');

const route53AccessRole = new aws.iam.Role('r53-access-role', {
    name: 'r53-access-role', // binds all target Arns to assume this specific role
    assumeRolePolicy: crossAccountAllowedArns.apply(sourceArns => { 
		return JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Effect: "Allow",
            Principal: {
                AWS: sourceArns
            },
            Action: "sts:AssumeRole"
        }]
    })
})});

const route53AccessPolicy = new aws.iam.Policy('r53-access-policy', { 
    name: 'r53-access-policy', // This should basically be 
	policy: pulumi.all(route53CrossAccountTargetArns).apply(targetArns => {
		return JSON.stringify({
			"Version": "2012-10-17",
			"Statement": [
				{
					"Effect": "Allow",
					"Action": route53CrossAccountAllowedActions,
					"Resource": targetArns
				}
			]
		})
	})
});

const route53AccessPolicyAttachment = new aws.iam.RolePolicyAttachment('r53-access-attachment', {
    role: route53AccessRole.name,
    policyArn: route53AccessPolicy.arn
});

interface TenantInfo {
    awsAccountId: pulumi.Output<string>;
    awsAccountName: pulumi.Output<string>;
    iamRoleArn: pulumi.Output<string>;
    iamUserArn: pulumi.Output<string>;
}

interface ActiveTenantInfo {
    [key: string]: pulumi.Output<TenantInfo>;
}

export const prodRootZoneId = prodRootZone.id;
export const prodRootZoneArn = prodRootZone.arn;
export const route53AccessRoleArn = route53AccessRole.arn;

// Hydrate ESC "glob-infra-administrative" tenant AWS account info with "glob-infra:shared" keyof ESC "glob-infra-engineering"
const engineeringSharedServicesConfig = (new pulumi.Config()).requireObject("shared") as {[key: string]: {}};
export const activeSharedInfo = pulumi.all(adminStack.getOutput('activeSharedInfo') as unknown as ActiveTenantInfo).apply(o => {
    return Object.keys(o).map(tenantKey => {
        return { [`${tenantKey}`]: {
                ...o[tenantKey],
                ...engineeringSharedServicesConfig[tenantKey]
            }
        };
    })
}).apply(accounts => accounts.reduce((accumulator, current) => {
	accumulator[Object.keys(current)[0]] = current[Object.keys(current)[0]]
	return accumulator;
})); 

// Hydrate ESC "glob-infra-administrative" shared AWS account info with "glob-infra:tenants" keyof ESC "glob-infra-engineering"
const engineeringTenantConfig = (new pulumi.Config()).requireObject("tenants") as {[key: string]: {}};
export const activeTenantInfo = pulumi.all(adminStack.getOutput('activeTenantInfo') as unknown as ActiveTenantInfo).apply(o => {
    return Object.keys(o).map(tenantKey => {
        return { [`${tenantKey}`]: {
                ...o[tenantKey],
                ...engineeringTenantConfig[tenantKey]
            }
        };
    })
}).apply(accounts => accounts.reduce((accumulator, current) => {
	accumulator[Object.keys(current)[0]] = current[Object.keys(current)[0]]
	return accumulator;
})); 

module.exports['vpcPrivateSubnetIds'] = privateSubnets.apply(subnets => subnets.map(subnet => subnet.id))
module.exports['vpcPublicSubnetIds'] = vpc.publicSubnetIds;
above is the code, we are facing subnet duplicate issue.
s
Do you have to use the default VPC? If not, try just creating a separate VPC since it'll do the IP calculations for you. IP calculations are really, really hard to get right. Source: Me, since I wrote the first version of awsx.ec2.Vpc. [12:21 PM] Josh Kodroff I'm only scanning your code, but you might want to consider putting each tenant in a separate VPC. You can use centralized egress with Transit Gateway to avoid needing a NAT Gateway for each tenant.
m
Hi @stocky-restaurant-98004, thanks for the reply. Each tenant has its own AWS account. With in each account, the Default VPC is being used. is there any chance to connect via slack meeting
s
Unfortunately, I don't have the bandwidth to connect on Slack meeting, but I would go back to my earlier recommendation to create a dedicated VPC for your tenant if they are each in their own account.
m
hi @stocky-restaurant-98004, thanks for the reply. is there a paid support in pulumi to get our issue resolved?
s
We offer support for Enterprise customers: https://www.pulumi.com/pricing/
m
HI @stocky-restaurant-98004, the our teananat is using free account. problem we are facing, SUbnet are in pulumi state and AWS but still pulumi is trying to delete the subset. I think this is a Pulumi Bug. below snapshot of state which shows the subnets
image.png
image.png
those subset found but still trying to delete
if subnet aviable in AWS and in pulumi state, it should not delete right?
is it a Pulumi Bug?
I think so
@stocky-restaurant-98004, Good morning. we need a solution for not deleting the subnets which are already in pulumi state. We also tried protecting the subnet, but Pulumi still trying to delete the subnets. Please refer the below snapshot