https://pulumi.com logo
Title
g

gifted-terabyte-92288

12/08/2020, 3:51 PM
👋 Hey guys, I had a question about module dependency patterns. I noticed that many of the examples have lambda or application code in the same file as the Pulumi setup. A lot of times, there's quite a bit of code and other stuff going on so separating it into folders makes sense. However, doing that I ran into this scenario:
// file 1 - pulumi.ts
import lambdaFn from './lambda.ts';
const bucket = new aws.s3.Bucket('blake-lambda-bucket');
const endpoint = new awsx.apigateway.API('create', {
  routes: [
    {
      path: '/',
      method: 'POST',
      eventHandler: lambdaFn
    }
  ]
});

export const lambdaUrl = endpoint.url;
export const bucketId = bucket.id

// file 2 - lambda.ts
import { bucketId } from './pulumi.ts';

async function putStuffInBucket() {
  // do things
}

export default putStuffInBucket
File 1 depends on the lambda code in file 2. File 2 depends on file 1 for the bucket name. That seems like a circular dependency, no? I've seen some examples where a string relative path is used to point to a file -- is that the preferred pattern? Can that point to a directory? I've also seen environment variables being set for some lambdas from the Pulumi file and those being consumed via lambda code. Just looking for some guidance on best practices / preferred patterns - thanks!
b

brave-planet-10645

12/08/2020, 4:02 PM
the way I've done it before is to have all your infra (so s3,
putStuffInBucket
etc) in one file, have the api gateway code in another, have the lambda code in another and then your index.tx can orchestrate it all together.
It means your lambda code (so business logic in other words) is together, the S3 logic is together and the api gateway resources are together. Don't forget you can use something like a ComponentResource to group similar resources together as well
g

gifted-terabyte-92288

12/08/2020, 4:08 PM
Thanks, @brave-planet-10645! Is there an example project that follows that paradigm in the examples repo that you know of by chance?
b

brave-planet-10645

12/08/2020, 4:34 PM
I've just put together a quick demo here: https://github.com/pierskarsenbarg/pulumi-demos/tree/main/s3-lambda
🙌 1
g

gifted-terabyte-92288

12/08/2020, 4:41 PM
Thank you - this is very helpful!
f

freezing-finland-22895

12/08/2020, 10:47 PM
I create all my resources in functions that take arguments and return
pulumi.Output
wrapped values for results, instead of top-level exports. Makes it much easier to mix & match your resources as needed (and no weird import-order dependencies).
g

gifted-terabyte-92288

12/09/2020, 2:45 PM
@freezing-finland-22895 Can you please show an example of that?
I think the piece I'm not following is the wrapping in
pulumi.Output
, but if you have a very simple example of a couple files that'd be super helpful and appreciated!
b

brave-planet-10645

12/09/2020, 4:56 PM
I think this is what you're referring to: to use the value from an
Output
you can use apply and get to the value of it. It's because at run time we might not know what the value is (because the resource may not have been created yet) so you can use apply as a callback.
f

freezing-finland-22895

12/09/2020, 5:05 PM
Here's a simple function that takes two arguments, representing a domain name and a subdomain, and creates a Route53 DNS record. The resulting fully-qualified name is returned as an https url. As @brave-planet-10645 mentioned, I used
apply
on the value of
resourcesDomain.fqdn
in order to construct the URL.
apply
is only necessary if you need to work with the value, tho. It could just as is easily return
fqdn
directly (which would still have type
Output<string>
). The key thing is that, by wrapping everything in a function, I control when these resources are created:
export async function createResourcesSite(apexDomain: string, apiDomain: string): Promise<pulumi.Output<string>> {
  const zone = await aws.route53.getZone({ name: apexDomain }),
    resourcesDomain = new aws.route53.Record(`...`, {
      type: "CNAME",
      ttl: 30,
      zoneId: zone.zoneId,
      records: [apiDomain]
    })

  return resourcesDomain.fqdn.apply((fqdn) => `https://${fqdn}`)
}
Hope that helps!
👏 1
🙌 1
g

gifted-terabyte-92288

12/09/2020, 5:26 PM
That is helpful, @freezing-finland-22895 - thanks! How would you go about setting up a lambda that required a provisioned resource within the Lambda itself? As an example, if I had a lambda that needed to know the id of a bucket or a connection string for Kafka to create messages? I like the functional approach, and that's the direction I've been going, but I end up doing something like:
// file 1
function x(bucketId) {
  return function y(event) {
    // connect to bucket
    // derive some data for bucket based on event input
   // return lambda result
  }
}

// file 2
// when creating the gateway, I end up something like:
const gateway = createApiGateway('gateway', [
    {
      path: '/',
      method: 'GET',
      eventHandler: x(bucket.id)
    }
  ]);
Do you run into similar scenarios, or are you grouping the creation of things like that inside your functions?
f

freezing-finland-22895

12/09/2020, 6:41 PM
In the lambda function itself, you can access
Output
values using the
get
method. So if
bucket.id
has type
Output<string>
, then you could write:
function (bucketId: Output<string>) {
  return function y(event) {
    const bucket = bucketId.get()
  }
}
When the lambda runs (executes the body of
y
), then
bucketId.get()
will return the ID of the S3 resource.
g

gifted-terabyte-92288

12/09/2020, 6:47 PM
Ok cool. I just wanted to make sure that using a pattern like that works. Thanks so much for your advice and example!
👍 1