Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

how are you meant to pass the API URL for use within a lambda function? #2448

Open
cookiejest opened this issue Jan 24, 2025 · 7 comments
Open
Labels
pending-response Issue is pending response from author pending-triage Incoming issues that need categorization question Question or confusion about some aspect of the product

Comments

@cookiejest
Copy link

Environment information

I need to provide the API URL to a third party service via an API call from within a lambda function. How can I get the api execution URL?

I can get it in amplify_outputs.json but that is not importable into a lambda function it seems. I also tried using backend.fetchRequisition.addEnvironment("API_ENDPOINT", httpApi.url) but it causes a circular dependency.

What is the best way to do this?

Describe the bug

API URL not accessible inside a lambda function

Reproduction steps

  1. Create an API via backend.ts
const httpApi = new HttpApi(apiStack, "HttpApi", {
  apiName: "CloudslurpHTTPApi",
  corsPreflight: {
    // Modify the CORS settings below to match your specific requirements
    allowMethods: [
      CorsHttpMethod.GET,
      CorsHttpMethod.POST,
      CorsHttpMethod.PUT,
      CorsHttpMethod.DELETE,
    ],
    // Restrict this to domains you trust
    allowOrigins: ["*"],
    // Specify only the headers you need to allow
    allowHeaders: ["*"],
  },
  createDefaultStage: true,
});

const apistring = httpApi.url
backend.fetchRequisition.addEnvironment("HTTP_API_ENDPOINT", apistring); // ENV

Any example please?

@cookiejest cookiejest added the pending-triage Incoming issues that need categorization label Jan 24, 2025
@ykethan
Copy link
Member

ykethan commented Jan 24, 2025

Hey @cookiejest, thank you for reaching out. The documentation has been recently updated providing additional information on this. Could you refer to the following documentation: https://docs.amplify.aws/react/build-a-backend/troubleshooting/circular-dependency/

@ykethan ykethan added question Question or confusion about some aspect of the product pending-response Issue is pending response from author labels Jan 24, 2025
@cookiejest
Copy link
Author

Unfortunately that's not particularly useful. It's a lot of text with no real workable solution to it. In summary it says 'put all your stuff in the same stack.. but if you still get the error there is an issue'

If that page was to be helpful there should be an example and how to actually fix it.

@github-actions github-actions bot removed the pending-response Issue is pending response from author label Jan 25, 2025
@ykethan
Copy link
Member

ykethan commented Jan 27, 2025

@cookiejest could you provide us the full error message and information on the resources the lambda function is currently being used in?

@ykethan ykethan added the pending-response Issue is pending response from author label Jan 27, 2025
@cookiejest
Copy link
Author

I have ended up going around it by setting the api URL as an environment variable. Not ideal

@github-actions github-actions bot removed the pending-response Issue is pending response from author label Jan 29, 2025
@ykethan
Copy link
Member

ykethan commented Jan 29, 2025

@cookiejest if the error message indicates the circular dependency found between nested stacks when using a custom resource, for example [data1234ABCD, function5678XYZ, customapiStack0123AB].
To mitigate the issue, i would suggest grouping the resource in the same stack. for example:

// add custom resource to the data stack
const httpApi = new HttpApi(backend.data.stack, "HttpApi", {})

// add function to data stack 
defineFunction({
 resourceGroupName: "data"
})

if you could provide us information on your existing architecture and error message, this would help us in diving into the issue.

@ykethan ykethan added the pending-response Issue is pending response from author label Jan 29, 2025
@cookiejest
Copy link
Author

both of those are complete opposite solutions. Here is my backend.ts

import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
import { storage } from './storage/resource';
import { generateHaikuFunction } from './functions/generate-haiku/resource';
import { deleteParquet } from "./functions/delete-parquet/resource";
import { Stack } from "aws-cdk-lib";
import { Policy, PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
import { StartingPosition, EventSourceMapping, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";
import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
import { connectionExpireReminder } from './jobs/connection-expire-reminder/resource';
import { stripeCheckout } from './functions/stripe-checkout/resource';
import { AuthType } from 'aws-cdk-lib/aws-stepfunctions-tasks';
import { checkConnectionAccess } from './functions/check-connection-access/resource';
import { runQuery } from './functions/run-query/resource';
import { mailContactForm } from './functions/mail-contact-form/resource';
import { fetchAccountData } from './functions/fetch-account-data/resource';
//import { runQueryInternal } from './functions/run-query-internal/resource';
import { sayHelloFunctionHandler } from './functions/hello-python/resource';
import {plaidInitSync} from './functions/plaid-init-sync/resource';

import {
  CorsHttpMethod,
  HttpApi,
  HttpMethod,
} from "aws-cdk-lib/aws-apigatewayv2";

import {
  HttpIamAuthorizer,
  HttpUserPoolAuthorizer,
} from "aws-cdk-lib/aws-apigatewayv2-authorizers";

import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
import { myApiFunction } from "./functions/api-function/resource";
import { fetchRequisition } from './functions/fetch-requisition/resource';



const backend = defineBackend({
  auth,
  data,
  storage,
  generateHaikuFunction,
  deleteParquet,
  connectionExpireReminder,
  stripeCheckout,
  checkConnectionAccess,
  sayHelloFunctionHandler,
  runQuery,
  mailContactForm,
  fetchAccountData,
  myApiFunction,
  fetchRequisition,
  plaidInitSync
});


///API FOR EXTERNAL CALLING START
// create a new API stack
const apiStack = backend.createStack("api-stack");

// create a IAM authorizer
const iamAuthorizer = new HttpIamAuthorizer();

// create a User Pool authorizer
const userPoolAuthorizer = new HttpUserPoolAuthorizer(
  "userPoolAuth",
  backend.auth.resources.userPool,
  {
    userPoolClients: [backend.auth.resources.userPoolClient],
  }
);

const httpLambdaIntegration = new HttpLambdaIntegration(
  "LambdaIntegration",
  backend.myApiFunction.resources.lambda
);

const httpLambdaIntegrationStripeCheckout = new HttpLambdaIntegration(
  "LambdaIntegration",
  backend.stripeCheckout.resources.lambda
);

const httpLambdaIntegrationFetchAccountData = new HttpLambdaIntegration(
  "LambdaIntegration",
  backend.fetchAccountData.resources.lambda
);

const httpLambdaIntegrationRunQuery = new HttpLambdaIntegration(
  "LambdaIntegration",
  backend.runQuery.resources.lambda
);




// create a new HTTP API with IAM as default authorizer
const httpApi = new HttpApi(apiStack, "HttpApi", {
  apiName: "CloudslurpHTTPApi",
  corsPreflight: {
    // Modify the CORS settings below to match your specific requirements
    allowMethods: [
      CorsHttpMethod.GET,
      CorsHttpMethod.POST,
      CorsHttpMethod.PUT,
      CorsHttpMethod.DELETE,
    ],
    // Restrict this to domains you trust
    allowOrigins: ["*"],
    // Specify only the headers you need to allow
    allowHeaders: ["*"],
  },
  createDefaultStage: true,
});


// add routes to the API with a IAM authorizer and different methods
httpApi.addRoutes({
  path: "/testapi",
  methods: [HttpMethod.GET, HttpMethod.PUT, HttpMethod.POST, HttpMethod.DELETE],
  integration: httpLambdaIntegration
});


// add routes to the API with a IAM authorizer and different methods
httpApi.addRoutes({
  path: "/stripewebhook",
  methods: [HttpMethod.GET, HttpMethod.PUT, HttpMethod.POST, HttpMethod.DELETE],
  integration: httpLambdaIntegrationStripeCheckout
});

// add routes to the API with a IAM authorizer and different methods
httpApi.addRoutes({
  path: "/data",
  methods: [HttpMethod.GET, HttpMethod.PUT, HttpMethod.POST, HttpMethod.DELETE],
  integration: httpLambdaIntegrationRunQuery
});

// add routes to the API with a IAM authorizer and different methods
httpApi.addRoutes({
  path: "/plaidwebhook",
  methods: [HttpMethod.GET, HttpMethod.PUT, HttpMethod.POST, HttpMethod.DELETE],
  integration: httpLambdaIntegrationFetchAccountData
});


// create a new IAM policy to allow Invoke access to the API
const apiPolicy = new Policy(apiStack, "ApiPolicy", {
  statements: [
    new PolicyStatement({
      actions: ["execute-api:Invoke"],
      resources: [
        `${httpApi.arnForExecuteApi("*", "/items")}`,
        `${httpApi.arnForExecuteApi("*", "/items/*")}`,
        `${httpApi.arnForExecuteApi("*", "/cognito-auth-path")}`,
      ],
    }),
  ],
});

// attach the policy to the authenticated and unauthenticated IAM roles
backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(apiPolicy);
backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(apiPolicy);

//

// add outputs to the configuration file
backend.addOutput({
  custom: {
    API: {
      [httpApi.httpApiName!]: {
        endpoint: httpApi.url,
        region: Stack.of(httpApi).region,
        apiName: httpApi.httpApiName,
      },
    },
  },
});



backend.stripeCheckout.resources.lambda.addToRolePolicy(
  new PolicyStatement({
    effect: Effect.ALLOW,
    actions: ["ses:*"],
    resources: ['*']
  })
)

backend.mailContactForm.resources.lambda.addToRolePolicy(
  new PolicyStatement({
    effect: Effect.ALLOW,
    actions: ["ses:*"],
    resources: ['*']
  })
)

backend.generateHaikuFunction.resources.lambda.addToRolePolicy(
  new PolicyStatement({
    effect: Effect.ALLOW,
    actions: ["bedrock:InvokeModel"],
    resources: [
      `arn:aws:bedrock:*::foundation-model/meta.llama3-70b-instruct-v1:0`,
    ],
  })
);



backend.connectionExpireReminder.resources.lambda.addToRolePolicy(
  new PolicyStatement({
    effect: Effect.ALLOW,
    actions: ["ses:*"],
    resources: ['*']
  })
)


const { cfnResources } = backend.data.resources;

cfnResources.amplifyDynamoDbTables["Connection"].timeToLiveAttribute = {
  attributeName: "connection_ttl",
  enabled: true
};


//Creates a DynamoDB event source for the Run table
const eventSource = new DynamoEventSource(backend.data.resources.tables["Connection"], {
  startingPosition: StartingPosition.LATEST,
  batchSize: 1
});


backend.deleteParquet.resources.lambda.addEventSource(eventSource);
backend.checkConnectionAccess.resources.lambda.addEventSource(eventSource);


const { cfnIdentityPool } = backend.auth.resources.cfnResources;
cfnIdentityPool.allowUnauthenticatedIdentities = true;

@github-actions github-actions bot removed the pending-response Issue is pending response from author label Jan 29, 2025
@ykethan
Copy link
Member

ykethan commented Jan 30, 2025

Hey @cookiejest, thank you for providing the information on the resource. On creating a minimal reproduction, currently CloudFormation throws The CloudFormation deployment failed due to circular dependency found between nested stacks [auth179371D7, apistack7B433BC7, function1351588B].

minimal reproduction:

const apiStack = backend.createStack("api-stack");

new HttpIamAuthorizer();

new HttpUserPoolAuthorizer("userPoolAuth", backend.auth.resources.userPool, {
  userPoolClients: [backend.auth.resources.userPoolClient],
});

const httpLambdaIntegration = new HttpLambdaIntegration(
  "LambdaIntegration",
  backend.apiFunction.resources.lambda
);

const httpApi = new HttpApi(apiStack, "HttpApi", {
  apiName: "CloudslurpHTTPApi",
  corsPreflight: {
    // Modify the CORS settings below to match your specific requirements
    allowMethods: [
      CorsHttpMethod.GET,
      CorsHttpMethod.POST,
      CorsHttpMethod.PUT,
      CorsHttpMethod.DELETE,
    ],
    allowOrigins: ["*"],
    allowHeaders: ["*"],
  },
  createDefaultStage: true,
});


httpApi.addRoutes({
  path: "/testapi",
  methods: [HttpMethod.GET, HttpMethod.PUT, HttpMethod.POST, HttpMethod.DELETE],
  integration: httpLambdaIntegration,
});

const apiPolicy = new Policy(apiStack, "ApiPolicy", {
  statements: [
    new PolicyStatement({
      actions: ["execute-api:Invoke"],
      resources: [
        `${httpApi.arnForExecuteApi("*", "/items")}`,
        `${httpApi.arnForExecuteApi("*", "/items/*")}`,
        `${httpApi.arnForExecuteApi("*", "/cognito-auth-path")}`,
      ],
    }),
  ],
});

// adds a dependency on auth
backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(apiPolicy);
backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(apiPolicy);

backend.addOutput({
  custom: {
    API: {
      [httpApi.httpApiName!]: {
        endpoint: httpApi.url,
        region: Stack.of(httpApi).region,
        apiName: httpApi.httpApiName,
      },
    },
  },
});

//adds dependency on function
backend.apiFunction.addEnvironment("API_URL", httpApi.url ?? "");

To break the circular dependency, I was able to group the api resource and the function in the auth stack

// backend.ts
const httpApi = new HttpApi(backend.auth.stack, "HttpApi", {}) // add the api resource in auth stack

// apifunction/resource.ts
defineFunction({
  name: "apifunction",
  entry: "./handler.ts",
  resourceGroupName: "auth", // groups the function with auth stack
});

@ykethan ykethan added the pending-response Issue is pending response from author label Jan 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pending-response Issue is pending response from author pending-triage Incoming issues that need categorization question Question or confusion about some aspect of the product
Projects
None yet
Development

No branches or pull requests

2 participants