Hey new to Pulumi and looking for some best practi...
# typescript
h
Hey new to Pulumi and looking for some best practices around cross stack references that go beyond the simple examples. The docs seem to heavily suggest using many small stacks but there are limited examples actually doing this. I’m attempting to create a VPC stack and a EKS stack and link resources across them
Copy code
// VPC Stack
import * as awsx from '@pulumi/awsx';

const vpc = new awsx.ec2.Vpc('application-vpc', {
  cidrBlock: '10.128.0.0/19',
  enableDnsSupport: true,
  enableDnsHostnames: true,
  subnets: [...Array(3).keys()].map((i) => ({ name: `private-${i}`, type: 'private' })),
  numberOfNatGateways: 1,
});

export const { id, privateSubnetIds } = vpc;

// EKS Stack - Stack References Only
import * as awsx from '@pulumi/awsx';
import * as eks from '@pulumi/eks';
import * as pulumi from '@pulumi/pulumi';
import { last } from 'lodash';

const env = last(pulumi.getStack().split('.'));

const vpcStack = new pulumi.StackReference(`infra.vpc.${env}`);
const vpcId = vpcStack.getOutput('vpcId');
const privateSubnetIds = vpcStack.getOutput('privateSubnetIds');

export const eksCluster = new eks.Cluster('applicatin-eks-cluster', {
  vpcId,
  privateSubnetIds,
});

// EKS Stack - Only pass minimal references (ideal)
import * as awsx from '@pulumi/awsx';
import * as eks from '@pulumi/eks';
import * as pulumi from '@pulumi/pulumi';
import { last } from 'lodash';

const env = last(pulumi.getStack().split('.'));

const vpcStack = new pulumi.StackReference(`infra.vpc.${env}`);
const vpcId = vpcStack.getOutput('vpcId');

const vpc = awsx.ec2.Vpc.fromExistingIds('application-vpc', {
  vpcId,
});

export const eksCluster = new eks.Cluster('applicatin-eks-cluster', {
  vpcId: vpc.id,
  privateSubnetIds: vpc.privateSubnetIds,
});
Option 1 works but outputting every will get messy quickly with increased complexity. Option 2 doesn’t actually work because
fromExistingIds
doesn’t actually hydrate the subnets. Any suggestions or better examples?
w
Hey new to Pulumi and looking for some best practices around cross stack references that go beyond the simple examples.
Welcome!
The docs seem to heavily suggest using many small stacks
This doesn't sound intentional - curious which doc pointed you that direction? In general, I suggest starting off with a single stack, and breaking it up if/when you know there are layers that are truly independent and are owned/versioned by different teams. It is definitely more constraining to introduce cross-stack boundaries in an application - so I wouldn't suggest this unless you know you need it.
Option 1 works but outputting every will get messy quickly with increased complexity.
Yes - you will in general want to draw boundaries between layers such that there are not a huge number of cross-stack dependencies (that is, such that the two layers are not overly coupled to one another). In many cases, just a couple of pieces of information are enough to re-hydrate what you need in the other stack, and those should be the outputs and the higher-level stack.
Option 2 doesn’t actually work because 
fromExistingIds
 doesn’t actually hydrate the subnets.
I'll have to look into this one. What do you see in this case for you example?
h
Thanks for the quick reply @white-balloon-205. For the last part I think the problem is actually https://github.com/pulumi/pulumi-awsx/issues/357
w
Ahh - got it - but you could export the subnet ids as well (just two additional arrays of stack outputs) and use that?
h
that’s what I’m trying to do now but running into some Output vs Input issues
and its less useful if we have to output everything anyways
some background might help… I’m looking to migrate an existing lerna monorepo with some shared infra (vpc & eks) and a bunch of micro services that “own” their own infra and get deployed into eks
w
Re: docs - yeah - those are a little more rosy on "micro-stacks" (and play into the "micro-services" vs. "monoliths" idea in a way which I'm not sure applies 100% here). They probably should make more clear that the monolith approach is much easier and should be used until you know where you need to break things down and introduce boundaries between stacks (which will introduce cost and constraints at those boundaries) (which indeed is not unlike the truth of "monoliths" vs. "micro-services" 😄).
migrate an existing lerna monorepo with some shared infra (vpc & eks) and a bunch of micro services that “own” their own infra and get deployed into eks
Okay - so keeping that same breakdown totally makes sense in Pulumi. One additional thing you can do in Pulumi is introduce a shared JS library that populates information in the higher-level stacks from the outputs available from the lower-level stack. That library can then be used by all the higher-level stacks as a single simple way to bootstrap themselves by referencing the vpc+eks stack they want to build on. You would still need to export all of the true surface area of the VPC+EKS stack - but ideally that's still just a handful of exports. Did your existing setup avoid needing to have as much on the boundary between these components?
h
Did your existing setup avoid needing to have as much on the boundary between these components?
Its loosely coupled now which has its own problems 🙂 and is why we’re looking at Pulumi
I was imagining a similar option to the shared library. Is the suggested method to pass stack output/references as strings and import them in higher level stacks? Ideally I think we’d want to pass the minimum identifiers and then hydrate read-only objects
I believe
aws-cdk
works closer to the later option
w
Ideally I think we’d want to pass the minimum identifiers and then hydrate read-only objects
Yes - this is what you would want to do. I think it's roughly: vpcID, subnetIDs, kubeconfig. Everything else should be able to be re-constructed from those.
h
So that would look like
Copy code
const vpcId = vpcStack.getOutput('id');
const privateSubnetIds = vpcStack.getOutput('privateSubnetIds');
const vpc = awsx.ec2.Vpc.fromExistingIds('application-vpc', {
  vpcId,
  privateSubnetIds,
});
w
Right. And you could put that in a shared library you use across all your higher-level service stacks.
👍 1
h
is there a trick for converting outputs to inputs?
the above snippets errors on
Copy code
Type 'Output<any>' is not assignable to type 'Input<string>[] | undefined'.
  Type 'OutputInstance<any>' is missing the following properties from type 'Input<string>[]': length, pop, push, concat, and 26 more
w
type Input<T> = Output<T> | T
Ahh - so in that case you are trying to assign an Output to an Array.
h
Yup privateSubnetIds ends up being
Copy code
pulumi.Output<string[]>
w
(...taking a quick look at the awsx API)
Okay - looks like it's not as simple as I hoped - I'll get you an example in a sec. But first - is there any particular reason you need an
awsx
VPC in the higher-level stacks? Or can you just use the raw VPC and Subnets? How are you using those IDs from the higher-level stacks?
h
general flexibility and decoupling same as passing object refs in normal programming
low level stack has an s3 bucket. high level stacks can get a real read-only reference to the bucket and configure event subscriptions
in the VPC+EKS case the VPC could be used for several purposes or own by a different team. having the VPC in a separate stack and allowing higher stacks to import a full reference allows independent deployments/updates
w
Definitely agreed. I think https://github.com/pulumi/pulumi/issues/4133 is the root of what you are looking for. That would provide seemless re-hydration of rich objects from
StackRefrences
.
h
Definitely sounds like what we’re looking for
w
BTW - for the specific problem of using
fromExistingIds
with
StackReference
- the following works, though how useful it is depends on what you are planning to use the
vpc
for ultiamtely.
Copy code
const vpcStack = new pulumi.StackReference("vpc");
const vpcId = vpcStack.getOutput("vpcId") as pulumi.Output<string>;
const publicSubnets = vpcStack.getOutput("publicSubnetIds") as pulumi.Output<string[]>;
const privateSubnets = vpcStack.getOutput("privateSubnetIds") as pulumi.Output<string[]>;

const vpc = pulumi.all([publicSubnets, privateSubnets]).apply(([publicSubnetIds, privateSubnetIds]) => {
    return awsx.ec2.Vpc.fromExistingIds("name", {
        vpcId,
        publicSubnetIds,
        privateSubnetIds,
    })    
});
h
great and more for my understanding why does
publicSubnets
need the
apply()
logic but
vpcId
does not?
w
Because
fromExistingIds
currently accepts
vpcId: Input<string>
but
publicSubnetIds: Array<Input<string>>
. That means you can pass an
Output<string>
to the former, but not an
Output<Array<string>>
to the latter (since you need an
Array<Output<string>>
. A key question is "is the length known yet"? For an
Output<Array<string>>
the length might not be known yet, and can only be queried inside an
.apply
. For an
Array<Output<string>>
the length is always known. This is important when you need to promptly loop over the input ( to create one resource per item in the loop).
I've opened https://github.com/pulumi/pulumi-awsx/issues/522 to see if we can improve this.
h
that makes sense. thanks!