aws-amplify / aws-amplify/amplify-hosting
Unable to access env variables in backend build creation
- Dominant language
- Dockerfile
- Stars
- 481
- Forks
- 123
- PR merge metrics
- No merged PRs in 30d
Description
### Environment information
```plain text
User:
arn:aws:sts::5***9:assumed-role/amplify-windninjas-SumitB-amplifyAuthunauthenticate-viTx2zIlvlSh/CognitoIdentityCredentials is not authorized to perform: rum:PutRumEvents on resource: 8fe-**97e because no identity-based policy allows the rum:PutRumEvents action
amplify-windninjas-SumitBaghel-sandbox-****f | 11:40:42 PM | UPDATE_FAILED | AWS::CloudFormation::Stack | cloudWatchRum.NestedStack/cloudWatchRum.NestedStackResource (cloudWatchR***) Embedded stack arn:aws:cloudformation:ap-south-1:5***:stack/amplify-windninjas-SumitBaghel-sandbox-****b was not successfully updated. Currently in UPDATE_ROLLBACK_IN_PROGRESS with reason: Validation failed for following resources: [cloudWatchRum***]
The CloudFormation deployment has failed.
```
### Describe the bug
want them to dynamic based on branch name
process.env.CUSTOM_GET_APP_MONITO_FN
process.env.DOMAIN_NAME
also want application id from amplify outputs
// rum init.ts
```
const config: AwsRumConfig = {
sessionSampleRate: 1,
identityPoolId: outputs.auth.identity_pool_id,
endpoint: "https://dataplane.rum.ap-south-1.amazonaws.com",
telemetries: ["errors", "http", "performance"],
allowCookies: true,
enableXRay: true,
signing: true // If you have a public resource policy and wish to send unsigned requests please set this to false
};
const APPLICATION_ID: string = '8f***97e';
const APPLICATION_VERSION: string = '1.0.0';
const APPLICATION_REGION: string = 'ap*1';
const awsRum: AwsRum = new AwsRum(
APPLICATION_ID,
APPLICATION_VERSION,
APPLICATION_REGION,
config
);
```
// backend.ts
```
new CloudwatchRum(backend.createStack("cloudWatchRum"), "cloudWatchRum", {
guestRole: backend.auth.resources.unauthenticatedUserIamRole,
identityPoolId: backend.auth.resources.cfnResources.cfnIdentityPool.attrId,
domain: process.env.DOMAIN_NAME, // Replace with your domain as needed
});
```
// cfn-response.ts
```
import type { CloudFormationCustomResourceEvent, Context } from "aws-lambda";
import https from "node:https";
import url from "node:url";
const SUCCESS = "SUCCESS";
const FAILED = "FAILED";
const send = async (
event: CloudFormationCustomResourceEvent,
context: Context,
responseStatus: string,
responseData: Record,
physicalResourceId: string,
noEcho?: boolean,
) => {
const responseBody = JSON.stringify({
Status: responseStatus,
Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
PhysicalResourceId: physicalResourceId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: noEcho || false,
Data: responseData,
});
const parsedUrl = new url.URL(event.ResponseURL);
const options = {
hostname: parsedUrl.hostname,
port: 443,
path: `${parsedUrl.pathname}${parsedUrl.search}`,
method: "PUT",
headers: {
"content-type": "",
"content-length": responseBody.length,
},
};
return new Promise((resolve, reject) => {
const request = https.request(options, (response) => {
resolve(response);
});
request.on("error", (error) => {
reject(error);
});
request.write(responseBody);
request.end();
});
};
export { send, SUCCESS, FAILED };
```
// index.ts
```
import type { CloudFormationCustomResourceHandler } from "aws-lambda";
import { Logger } from "@aws-lambda-powertools/logger";
import { RUMClient, GetAppMonitorCommand } from "@aws-sdk/client-rum";
import { send, SUCCESS, FAILED } from "./cfn-response";
const MAX_RETRIES = 5;
const logger = new Logger({
serviceName: "appMonitorIdRetrieveService",
logLevel: "DEBUG",
});
const client = new RUMClient();
export const handler: CloudFormationCustomResourceHandler = async (event, context) => {
logger.addContext(context);
logger.debug("event", { event });
const { RequestType: requestType, ResourceProperties: properties } = event;
if (requestType === "Delete") {
logger.info("Delete");
await send(event, context, SUCCESS, {}, "CustomFunction");
} else if (requestType === "Create" || requestType === "Update") {
logger.info("Create/Update");
const { appMonitorName } = properties;
if (appMonitorName === undefined) {
logger.error("Event not supported, no appMonitorName", {
details: event,
});
await send(event, context, FAILED, {}, "CustomFunction");
}
let retryIdx = 1;
let appMonitorId: string | undefined = undefined;
while (appMonitorId === undefined) {
if (retryIdx === MAX_RETRIES) {
logger.error("Reached max retry limit, appMonitor not found.");
await send(event, context, FAILED, {}, "CustomFunction");
}
try {
const res = await client.send(
new GetAppMonitorCommand({
Name: appMonitorName,
}),
);
appMonitorId = res?.AppMonitor?.Id;
} catch (err) {
logger.error("An error occurred", { error: err });
const waitMs = 1000 * retryIdx;
logger.info(`Trying again in ${waitMs}ms`);
retryIdx++;
// Basic back-off mechanism
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
try {
const responseData = { AppMonitorId: appMonitorId };
logger.debug("Response data", { details: responseData });
await send(event, context, SUCCESS, responseData, "CustomFunction");
} catch (err) {
logger.error("An error occurred", { error: err });
await send(event, context, FAILED, {}, "CustomFunction");
}
} else {
logger.error("Event not supported", { details: event });
await send(event, context, FAILED, {}, "CustomFunction");
}
};
```
// resource.ts
```
import url from "node:url";
import { Construct } from "constructs";
import { RemovalPolicy, Stack, CustomResource, CfnOutput } from "aws-cdk-lib";
import { CfnAppMonitor } from "aws-cdk-lib/aws-rum";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Runtime, Architecture } from "aws-cdk-lib/aws-lambda";
import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs";
import { PolicyStatement, type IRole, Role, Policy, Effect } from "aws-cdk-lib/aws-iam";
interface CloudwatchRumProps {
guestRole: IRole;
identityPoolId: string;
domain: string;
}
export class CloudwatchRum extends Construct {
constructor(scope: Construct, id: string, props: CloudwatchRumProps) {
super(scope, id);
const { guestRole, identityPoolId, domain } = props;
// Create CloudWatch RUM AppMonitor
const appMonitorName = `app-monitor-${Stack.of(this).stackName}`;
const appmonitorArn = `arn:aws:rum:${Stack.of(this).region}:${Stack.of(this).account}:appmonitor/${appMonitorName}`;
const appMonitor = new CfnAppMonitor(this, "AppMonitor", {
domain,
name: appMonitorName,
appMonitorConfiguration: {
allowCookies: true,
enableXRay: true,
guestRoleArn: guestRole.roleArn,
identityPoolId,
sessionSampleRate: 1,
telemetries: ["errors", "http", "performance"],
},
cwLogEnabled: false,
});
// Create custom function to get AppMonitorId
const functionName = "CustomGetAppMonitorFn";
const logGroup = new LogGroup(this, "CustomGetAppMonitorFnLogGroup", {
logGroupName: `/aws/lambda/${functionName}`,
removalPolicy: RemovalPolicy.DESTROY,
retention: RetentionDays.ONE_DAY,
});
const customGetAppMonitorFn = new NodejsFunction(this, process.env.CUSTOM_GET_APP_MONITO_FN!, {
functionName,
entry: url.fileURLToPath(new URL("index.ts", import.meta.url)),
runtime: Runtime.NODEJS_20_X,
architecture: Architecture.ARM_64,
logGroup,
});
customGetAppMonitorFn.addToRolePolicy(
new PolicyStatement({
actions: ["rum:GetAppMonitor"],
resources: [appmonitorArn],
}),
);
const customResource = new CustomResource(this, "CustomResource", {
serviceToken: customGetAppMonitorFn.functionArn,
properties: {
appMonitorName,
},
});
customResource.node.addDependency(appMonitor);
// Attach an inline policy to the guest role to allow it to send events to the AppMonitor
const guestRoleResource = Role.fromRoleArn(this, "GuestRole", guestRole.roleArn);
guestRoleResource.attachInlinePolicy(
new Policy(this, "CwRumPolicy", {
policyName: "CwRumPolicy",
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["rum:PutRumEvents"],
resources: [appmonitorArn],
}),
],
}),
);
// Set the resources as outputs for easy access
new CfnOutput(this, "AppMonitorName", {
value: appMonitorName,
});
new CfnOutput(this, "AppMonitorId", {
value: customResource.getAtt("AppMonitorId").toString(),
});
new CfnOutput(this, "GuestRoleArn", {
value: guestRole.roleArn,
});
new CfnOutput(this, "IdentityPoolId", {
value: identityPoolId,
});
}
}
```
### Reproduction steps
Came while deplying on pipline from another branch
Contributor guide
Research direction
Start with backend.ts and resource.ts to trace how branch deployment values reach the CloudWatch RUM construct, then inspect the custom resource entry point in index.ts and its cfn-response.ts helper. Reproduce the pipeline deployment described in the issue and verify that the dynamic values and AppMonitorId are available to the backend build without the reported RUM authorization failure.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, node.js, typescript
- Domain
- backend, ci-cd, cloud
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100