I'm trying to create a custom resource in AWS Amplify, using AWS CDK. And, I'm trying to use an existing lambda function as a provider event handler.
When I do amplify push the resource creation fails with no useful information. What am I doing wrong here? How can I troubleshoot this?
import * as cdk from '#aws-cdk/core';
import * as AmplifyHelpers from '#aws-amplify/cli-extensibility-helper';
import * as cr from "#aws-cdk/custom-resources";
import * as logs from "#aws-cdk/aws-logs";
import * as lambda from '#aws-cdk/aws-lambda';
import { AmplifyDependentResourcesAttributes } from "../../types/amplify-dependent-resources-ref"
export class cdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps, amplifyResourceProps?: AmplifyHelpers.AmplifyResourceProps) {
super(scope, id, props);
/* Do not remove - Amplify CLI automatically injects the current deployment environment in this input parameter */
new cdk.CfnParameter(this, 'env', {
type: 'String',
description: 'Current Amplify CLI env name',
});
const dependencies: AmplifyDependentResourcesAttributes = AmplifyHelpers.addResourceDependency(this,
amplifyResourceProps.category,
amplifyResourceProps.resourceName,
[{
category: "function",
resourceName: "myFunction"
}]
);
const myFunctionArn = cdk.Fn.ref(dependencies.function.myFunction.Arn);
const importedLambda = lambda.Function.fromFunctionArn(this, "importedLambda", myFunctionArn);
const provider = new cr.Provider(this, "MyCustomResourceProvider", {
onEventHandler: importedLambda,
logRetention: logs.RetentionDays.ONE_DAY,
})
new cdk.CustomResource(this, "MyCustomResource", {
serviceToken: provider.serviceToken
})
}
}
Here's the error I get:
CREATE_FAILED custommyCustomResourceXXXX AWS::CloudFormation::Stack Parameters: [AssetParametersXXXX, .....] must have values.
I got a response from AWS support team. It looks like the AssetParameters error is caused by the fact that Amplify CLI currently doesn't support a high level construct of Custom Resource Provider inside the custom resource category in Amplify CLI. The resource should be created this way:
import * as cdk from '#aws-cdk/core';
import * as AmplifyHelpers from '#aws-amplify/cli-extensibility-helper';
import * as lambda from '#aws-cdk/aws-lambda';
import { AmplifyDependentResourcesAttributes } from "../../types/amplify-dependent-resources-ref"
export class cdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps, amplifyResourceProps?: AmplifyHelpers.AmplifyResourceProps) {
super(scope, id, props);
/* Do not remove - Amplify CLI automatically injects the current deployment environment in this input parameter */
new cdk.CfnParameter(this, 'env', {
type: 'String',
description: 'Current Amplify CLI env name',
});
const dependencies: AmplifyDependentResourcesAttributes = AmplifyHelpers.addResourceDependency(this,
amplifyResourceProps.category,
amplifyResourceProps.resourceName,
[{
category: "function",
resourceName: "myFunction"
}]
);
const myFunctionArn = cdk.Fn.ref(dependencies.function.myFunction.Arn);
const importedLambda = lambda.Function.fromFunctionArn(this, "importedLambda", myFunctionArn);
new cdk.CustomResource(this, "MyCustomResource", {
serviceToken: importedLambda.functionArn
})
}
}
Related
I am trying to deploy NextJs and NextAuth.js to AWS using CDK (Cloud Development Kit). I have cloned the NextAuth.js example project (https://github.com/nextauthjs/next-auth-example) and
installed "serverless-http" for handling the binding from Lambda to NextJs. I attempted to follow this guide https://remaster.com/blog/nextjs-lambda-serverless-framework but using AWS CDK instead of the serverless.yml file as I am integrating it with existing infrastructure.
next.config.js:
/** #type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
unoptimized: true
},
output: 'standalone'
}
module.exports = nextConfig
[...nextauth].ts (From the example but using a simple credentials provider that always resolves):
import NextAuth, { NextAuthOptions } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials";
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
return { id: "1", name: "J Smith", email: "jsmith#example.com" }
}
})
],
theme: {
colorScheme: "light",
},
callbacks: {
async jwt({ token }) {
token.userRole = "admin"
return token
},
},
}
export default NextAuth(authOptions)
server.ts:
import { NextConfig } from "next";
import NextServer from "next/dist/server/next-server";
import serverless from "serverless-http";
// #ts-ignore
import { config } from "./.next/required-server-files.json";
const nextServer = new NextServer({
hostname: "localhost",
port: 3000,
dir: __dirname,
dev: false,
conf: {
...(config as NextConfig),
},
});
export const handler = serverless(nextServer.getRequestHandler());
It is being built using the following script:
#!/bin/bash
BUILD_FOLDER=.dist
yarn build
rm -rf $BUILD_FOLDER
mv .next/standalone/ $BUILD_FOLDER/
cp -r .next/static $BUILD_FOLDER/.next
rm $BUILD_FOLDER/server.js
cp -r next.config.js $BUILD_FOLDER/
cp -r node_modules/serverless-http $BUILD_FOLDER/node_modules/serverless-http
tsc server.ts --outDir .dist --esModuleInterop true
cp -r public $BUILD_FOLDER/
This is deployed using AWS written in CDK C#. Primarily using a HttpApi and a single Lambda. Each configured as shown below:
Lambda:
var function = new Function(this, "nextjs-function", new FunctionProps
{
Code = Code.FromAsset(...".dist"),
Handler = "server.handler",
Runtime = Runtime.NODEJS_16_X,
...
Environment = new Dictionary<string, string>
{
{ "NEXTAUTH_URL", "https://myDomainName.com" },
{ "NEXTAUTH_SECRET", portalSecret },
}
});
HttpApi:
var httpApi = new HttpApi(this, "http-api", new HttpApiProps
{
DisableExecuteApiEndpoint = true,
DefaultIntegration = new HttpLambdaIntegration("nextjs-route", function),
DefaultDomainMapping = new DomainMappingOptions
{
DomainName = "myDomainName.com"
}
});
Opening the deployed webpage and clicking the "Sign In" button at the top, I get taken to /api/auth/signin?callbackUrl=%2F with a form. Without touching the credentials I click "Sign in with credentials". This results in the page reloading and nothing happening. Expected behaviour should be a session and a redirect back to the home page (/) as is happening when running it locally using either yarn dev or yarn build && yarn start.
I get no errors client/server-side thus leaving me in the dark.
I suspect that it has to do with domain configuration but I am unable to find the problem. I tested with another NextJs/NextAuth project using a AWS Cognito provider. This also had problems as when I clicked the sign in button I got an "Unexpected token" error due to the underlying signIn(...) function (from the NextAuth library) trying to parse the fetched page as JSON, which turned out to be the sign-in-page. Thus my suspicion of something domain-related.
Goal: To monitor the Lambda Throttles with CloudWatch Alarms. Lambdas are built in the CLI using amplify add function. The code below was implemented following the Amplify Documentation, running amplify add custom and then CDK.
Code:
/* AWS CDK code goes here - learn more: https://docs.aws.amazon.com/cdk/latest/guide/home.html */
import * as cdk from "#aws-cdk/core";
import * as AmplifyHelpers from "#aws-amplify/cli-extensibility-helper";
import { AmplifyDependentResourcesAttributes } from "../../types/amplify-dependent-resources-ref";
import { Alarm } from "#aws-cdk/aws-cloudwatch";
import { lambdaThrottles } from "./alarm-props";
import { ComparisonOperator, AlarmProps } from '#aws-cdk/aws-cloudwatch';
export class cdkStack extends cdk.Stack {
private readonly ApplicationToExpiredApplicationTableAlarm: Alarm;
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps, amplifyResourceProps?: AmplifyHelpers.AmplifyResourceProps) {
super(scope, id, props);
// 🔽 Do not remove - Amplify CLI automatically injects the current deployment environment in this input parameter
new cdk.CfnParameter(this, "env", { type: "String", description: "Current Amplify CLI env name" });
// 🔽 Obtaining the project information
const amplifyProjectInfo = AmplifyHelpers.getProjectInfo();
const environment = cdk.Fn.ref('env');
// 🔽 Obtaining the Lambda Functions from Amplify
const dependencies: AmplifyDependentResourcesAttributes = AmplifyHelpers.addResourceDependency(this, amplifyResourceProps.category, amplifyResourceProps.resourceName, [
{ category: "function", resourceName: "ApplicationToExpiredApplicationTable" },
]);
// 🔽 Attempting to access the Lambda Function directly
const ApplicationToExpiredApplicationTable = dependencies.function["ApplicationToExpiredApplicationTable"];
// 🔽 Defining Alarm Props
const lambdaThrottles = (lambdaFunction: any): AlarmProps => {
return {
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
threshold: 1,
evaluationPeriods: 1,
metric: lambdaFunction.metricThrottles({ label : lambdaFunction.functionName}),
actionsEnabled: true,
alarmDescription: `This lambda has throttled ${lambdaFunction.functionName}`,
alarmName: `${lambdaFunction.functionMae}-Throttles`
}
};
// 🔽 Building the Alarms
this.ApplicationToExpiredApplicationTableAlarm = new Alarm(this, `${environment}-ApplicationToExpiredApplicationTableAlarm-${amplifyProjectInfo.projectName}`, lambdaThrottles(ApplicationToExpiredApplicationTable))
}
}
Current Error:
I cannot access the function like I would be able to with the CDK Function, and thus cannot write lambdaFunction.metricThrottles because it is not available.
I understand how to get the Arn or the Name following the Amplify Documentation, but this still does not allude to accessing the function itself, just properties of it.
I am trying to deploy a next.js (ssr) application in AWS' Amplify using the CDK but Amplify fails to identify the app as next.js ssr. When I do it manually though, using AWS UI, app is identified as SSR and works as expected.
This is generated by aws-cdk/aws-amplify v118 as:
import * as cdk from '#aws-cdk/core';
import * as amplify from '#aws-cdk/aws-amplify';
import codebuild = require('#aws-cdk/aws-codebuild');
export class AmplifyStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const sourceCodeProvider = new amplify.GitHubSourceCodeProvider({
owner: '.....',
repository: '....',
oauthToken: cdk.SecretValue.secretsManager('github-token'),
});
const buildSpec = codebuild.BuildSpec.fromObjectToYaml(
{
version: 1,
applications: [
{
frontend: {
phases: {
preBuild: {
commands: [
"npm install"
]
},
build: {
commands: [
"npm run build"
]
}
},
artifacts: {
baseDirectory: ".next",
files: [
"**/*"
]
},
cache: {
paths: [
"node_modules/**/*"
]
}
}
}
]
}
);
const amplifyApp = new amplify.App(this, "cdk-nf-web-app", {
sourceCodeProvider: sourceCodeProvider,
buildSpec: buildSpec
});
amplifyApp.addBranch('develop', {
basicAuth: amplify.BasicAuth.fromGeneratedPassword('dev')
});
amplifyApp.addCustomRule({
source: "</^[^.]+$|\\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|ttf|map|json)$)([^.]+$)/>",
target: "/index.html",
status: amplify.RedirectStatus.REWRITE
});
}
}
Which is identical to what AWS has generated when I do it manually from UI. The difference here is the lack of Framework identification as shown in picture. Any ideas?
To answer my own question, I was missing the role as without it aws won't create the necessary resources. (role: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-iam-readme.html)
Edit to elaborate on how i fixed it:
Added a new role that can be used by amplify
const role = new iam.Role(this, 'amplify-role-webapp-'+props.environment, {
assumedBy: new iam.ServicePrincipal('amplify.amazonaws.com'),
description: 'Custom role permitting resources creation from Amplify',
});
and assigned a policy (AdministratorAccess) that role
let iManagedPolicy = iam.ManagedPolicy.fromAwsManagedPolicyName(
'AdministratorAccess',
);
role.addManagedPolicy(iManagedPolicy)
Then upon creating the app, i assigned the role to the app:
const amplifyApp = new amplify.App(this, "cdk-nf-web-app", {
sourceCodeProvider: sourceCodeProvider,
buildSpec: buildSpec,
role: role <--- this line here
});
The amplify app requires authorisation to create the relevant resources:
// This is for demonstrations purposes only; Do not give full access for production usage!
amplifyApp.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
resources: ["*"],
actions: ['*'],
}))
Source Code Showcase
I'm wondering why is the Timestamp object is not working as I expect?
It works in test environment (I use Mocha), but throws error when it has been deployed.
index.ts
import { Timestamp, QuerySnapshot } from "#google-cloud/firestore";
....
async someFunction() {
let col = firestore.collection("mycollection");
let now = Timestamp.now();
let twentyMinsAgo = Timestamp.fromMillis(now.toMillis() - (1200 * 1000));
return col
.where('LastEdited', '>=', twentyMinsAgo) //This throws error
.get()
}
Stack Trace
Argument "value" is not a valid QueryValue.
Detected an object of type "Timestamp" that doesn't match the expected instance.
Please ensure that the Firestore types you are using are from the same NPM package.
at Validator.(anonymous function).err [as isQueryValue] (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/firestore/build/src/validate.js:99:27)
at CollectionReference.where (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/firestore/build/src/reference.js:940:25)
package.json
"dependencies": {
....
"#google-cloud/firestore": "^0.16.0",
"firebase-admin": "~6.0.0",
"firebase-functions": "^2.0.5"
}
Now I get it why it throws an error. Because I import Firestore object separately, whereas I should just use Firestore object from Firebase Admin SDK.
What I changed:
remove "#google-cloud/firestore" dependency from package.json
Use admin.firestore.Timestamp object.
index.ts
async someFunction() {
let col = firestore.collection("mycollection");
let now = admin.firestore.Timestamp.now();
let twentyMinsAgo = admin.firestore.Timestamp.fromMillis(now.toMillis() - (1200 * 1000));
col.where('LastEdited', '>=', twentyMinsAgo) //Now ok
.get()
}
when using firebase admin avoid importing and using any of the client side package directly
so instead of
import * as admin from "firebase-admin";
import firebase from "firebase/app";
admin.firestore().collection("name").set({
date: firebase.firestore.Timestamp.now()
})
use this instead
import * as admin from "firebase-admin";
admin.firestore().collection("name").set({
date: admin.firestore.Timestamp.now()
})
2022 EDIT:
For newer version of Firebase ^10.0.2; Timestamp can be imported directly
import * as admin from "firebase-admin";
import { Timestamp } from "firebase-admin/firestore";
admin.firestore().collection("name").set({
date: Timestamp.now(),
})
There is a difference in how the client side and server side packages format Timestamp for some reason.
client
import { Timestamp } from '#angular/fire/firestore';
[
"LastEdited": {
"seconds": 1709272799,
"nanoseconds": 999000000
}
]
server
import { Timestamp } from 'firebase-admin/firestore';
[
"LastEdited": {
"_seconds": 1709272799,
"_nanoseconds": 999000000
}
]
client solution when querying from server
manually create the Timestamp from the return
const timestamp = new Timestamp(data.LastEdited._seconds, data.LastEdited._nanoseconds);
I am trying to correctly type a react component with flow and apollo graphql. I keep getting a flow error message. I am using react-apollo 2.0.1 and flow 0.53.1
// #flow
/**
*
* CompanyName
*
*/
import React from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import type { OperationComponent, ChildProps } from 'react-apollo';
type Company = {
id: string,
name: string,
};
type Response = {
company: Company,
};
type Props = {
className: ?string,
};
class CompanyName extends React.Component<ChildProps<Props, Response>> {
render() {
return (
<span className={this.props.className}>
{!!this.props.data.company && this.props.data.company.name}
</span>
);
}
}
const query = gql`
query {
company {
id
name
}
}
`;
const withCompanyName: OperationComponent<Response, Props> = graphql(query);
export default withCompanyName(CompanyName); // this line gives a flow error
I get an error on the last line of code saying that the type is incompatible. Everything else validates correctly.
The error message is: CompanyName (class type: CompanyName) This type is incompatible with StatelessComponent (union: type application of polymorphic type: type StatelessComponent | class type: type application of identifier React$Component)
I am able to get it to work properly if I change it to a stateless functional component, but I need to get this working for class components.
Update:
Here is my .flowconfig
[ignore]
[include]
[libs]
[options]
module.name_mapper='.*\(.gql\)' -> 'empty/object'
I am using the empty package to prevent gql imports from causing a flow error.